From 94114f7a4b590c1e1f6412a414c6840a0a192e5a Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 26 Jan 2019 14:13:19 +0800 Subject: [PATCH] Migrate to LiveData Benefit includes: no more flush after changing nickname. Yep. --- .../java/be/mygod/vpnhotspot/MainActivity.kt | 4 +- .../java/be/mygod/vpnhotspot/client/Client.kt | 45 ++++++++++--------- .../vpnhotspot/client/ClientViewModel.kt | 2 +- .../vpnhotspot/client/ClientsFragment.kt | 20 +++++---- .../vpnhotspot/client/TetheringClient.kt | 8 ---- .../mygod/vpnhotspot/client/WifiP2pClient.kt | 4 +- .../vpnhotspot/manage/TetheringFragment.kt | 1 + .../java/be/mygod/vpnhotspot/net/Routing.kt | 3 +- .../vpnhotspot/net/monitor/TrafficRecorder.kt | 1 - .../be/mygod/vpnhotspot/room/ClientRecord.kt | 22 ++++++--- .../be/mygod/vpnhotspot/room/Converters.kt | 6 ++- .../be/mygod/vpnhotspot/room/TrafficRecord.kt | 15 +++---- 12 files changed, 68 insertions(+), 63 deletions(-) delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt index a5855fd7..39c1b6af 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.get -import be.mygod.vpnhotspot.client.Client import be.mygod.vpnhotspot.client.ClientViewModel import be.mygod.vpnhotspot.client.ClientsFragment import be.mygod.vpnhotspot.databinding.ActivityMainBinding @@ -45,6 +44,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.setLifecycleOwner(this) binding.navigation.setOnNavigationItemSelectedListener(this) if (savedInstanceState == null) displayFragment(TetheringFragment()) badge = QBadgeView(this) @@ -55,7 +55,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS badge.setGravityOffset(16f, 0f, true) val model = ViewModelProviders.of(this).get() if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class) - model.clients.observe(this, Observer> { badge.badgeNumber = it.size }) + model.clients.observe(this, Observer { badge.badgeNumber = it.size }) SmartSnackbar.Register(lifecycle, binding.fragmentHolder) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt index 56ddf56f..98d96b0b 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt @@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.client import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StrikethroughSpan +import androidx.lifecycle.Transformations import androidx.recyclerview.widget.DiffUtil import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.R @@ -10,42 +11,42 @@ import be.mygod.vpnhotspot.net.InetAddressComparator import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.room.AppDatabase -import be.mygod.vpnhotspot.room.lookup import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.util.onEmpty import java.net.InetAddress import java.util.* -abstract class Client { +open class Client(val mac: String, val iface: String) { companion object DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Client, newItem: Client) = oldItem.iface == newItem.iface && oldItem.mac == newItem.mac override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem } - abstract val iface: String - abstract val mac: String private val macIface get() = "$mac%$iface" val ip = TreeMap(InetAddressComparator) - val record by lazy { AppDatabase.instance.clientRecordDao.lookup(mac.macToLong()) } + val record = AppDatabase.instance.clientRecordDao.lookupSync(mac.macToLong()) + + val nickname get() = record.value?.nickname ?: "" + val blocked get() = record.value?.blocked == true open val icon get() = TetherType.ofInterface(iface).icon - val title: CharSequence get() { - val result = SpannableStringBuilder(record.nickname.onEmpty(macIface)) - if (record.blocked) result.setSpan(StrikethroughSpan(), 0, result.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) - return result - } - val description: String get() { - val result = StringBuilder(if (record.nickname.isEmpty()) "" else "$macIface\n") - ip.entries.forEach { (ip, state) -> - result.appendln(app.getString(when (state) { - IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete - IpNeighbour.State.VALID -> R.string.connected_state_valid - IpNeighbour.State.FAILED -> R.string.connected_state_failed - else -> throw IllegalStateException("Invalid IpNeighbour.State: $state") - }, ip.hostAddress)) + val title = Transformations.map(record) { record -> + SpannableStringBuilder(record.nickname.onEmpty(macIface)).apply { + if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) } - return result.toString().trimEnd() + } + val description = Transformations.map(record) { record -> + StringBuilder(if (record.nickname.isEmpty()) "" else "$macIface\n").apply { + ip.entries.forEach { (ip, state) -> + appendln(app.getString(when (state) { + IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete + IpNeighbour.State.VALID -> R.string.connected_state_valid + IpNeighbour.State.FAILED -> R.string.connected_state_failed + else -> throw IllegalStateException("Invalid IpNeighbour.State: $state") + }, ip.hostAddress)) + } + }.toString().trimEnd() } override fun equals(other: Any?): Boolean { @@ -57,9 +58,9 @@ abstract class Client { if (iface != other.iface) return false if (mac != other.mac) return false if (ip != other.ip) return false - if (record != other.record) return false + if (record.value != other.record.value) return false return true } - override fun hashCode() = Objects.hash(iface, mac, ip, record) + override fun hashCode() = Objects.hash(iface, mac, ip, record.value) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt index 5565cec8..d43812bb 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientViewModel.kt @@ -40,7 +40,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb var client = clients[key] if (client == null) { if (!tetheredInterfaces.contains(neighbour.dev)) continue - client = TetheringClient(neighbour) + client = Client(neighbour.lladdr, neighbour.dev) clients[key] = client } client.ip += Pair(neighbour.ip, neighbour.state) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt index 7486945a..0bb2df26 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt @@ -25,8 +25,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.versionedparcelable.VersionedParcelable import be.mygod.vpnhotspot.AlertDialogFragment import be.mygod.vpnhotspot.App.Companion.app -import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.Empty +import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.databinding.FragmentClientsBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor @@ -53,11 +53,9 @@ class ClientsFragment : Fragment() { } override fun onClick(dialog: DialogInterface?, which: Int) { - AppDatabase.instance.clientRecordDao.lookup(arg.mac.macToLong()).apply { + AppDatabase.instance.clientRecordDao.upsert(arg.mac.macToLong()) { nickname = this@NicknameDialogFragment.dialog!!.findViewById(android.R.id.edit).text - AppDatabase.instance.clientRecordDao.update(this) } - IpNeighbourMonitor.instance?.flush() } } @@ -95,13 +93,14 @@ class ClientsFragment : Fragment() { private inner class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener, PopupMenu.OnMenuItemClickListener { init { + binding.setLifecycleOwner(this@ClientsFragment) // todo some way better? binding.root.setOnClickListener(this) } override fun onClick(v: View) { PopupMenu(binding.root.context, binding.root).apply { menuInflater.inflate(R.menu.popup_client, menu) - menu.removeItem(if (binding.client!!.record.blocked) R.id.block else R.id.unblock) + menu.removeItem(if (binding.client!!.blocked) R.id.block else R.id.unblock) setOnMenuItemClickListener(this@ClientViewHolder) show() } @@ -111,7 +110,7 @@ class ClientsFragment : Fragment() { return when (item?.itemId) { R.id.nickname -> { val client = binding.client ?: return false - NicknameDialogFragment().withArg(NicknameArg(client.mac, client.record.nickname)) + NicknameDialogFragment().withArg(NicknameArg(client.mac, client.nickname)) .show(fragmentManager ?: return false, "NicknameDialogFragment") true } @@ -119,7 +118,9 @@ class ClientsFragment : Fragment() { val client = binding.client ?: return false val wasWorking = TrafficRecorder.isWorking(client.mac.macToLong()) client.record.apply { - AppDatabase.instance.clientRecordDao.update(ClientRecord(mac, nickname, !blocked)) + val value = value ?: ClientRecord(client.mac.macToLong()) + value.blocked = !value.blocked + AppDatabase.instance.clientRecordDao.update(value) } IpNeighbourMonitor.instance?.flush() if (!wasWorking && item.itemId == R.id.block) { @@ -129,7 +130,7 @@ class ClientsFragment : Fragment() { } R.id.stats -> { val client = binding.client ?: return false - StatsDialogFragment().withArg(StatsArg(client.title, + StatsDialogFragment().withArg(StatsArg(client.title.value!!, // todo? AppDatabase.instance.trafficRecordDao.queryStats(client.mac.macToLong()))) .show(fragmentManager ?: return false, "StatsDialogFragment") true @@ -186,6 +187,7 @@ class ClientsFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_clients, container, false) + binding.setLifecycleOwner(this) binding.clients.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.clients.itemAnimator = DefaultItemAnimator() binding.clients.adapter = adapter @@ -194,7 +196,7 @@ class ClientsFragment : Fragment() { IpNeighbourMonitor.instance?.flush() } ViewModelProviders.of(requireActivity()).get().clients.observe(this, - Observer> { adapter.submitList(it.toMutableList()) }) + Observer { adapter.submitList(it.toMutableList()) }) return binding.root } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt deleted file mode 100644 index c6d65092..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/TetheringClient.kt +++ /dev/null @@ -1,8 +0,0 @@ -package be.mygod.vpnhotspot.client - -import be.mygod.vpnhotspot.net.IpNeighbour - -class TetheringClient(private val neighbour: IpNeighbour) : Client() { - override val iface get() = neighbour.dev - override val mac get() = neighbour.lladdr -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt index 46f4c4d6..5c0904e1 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/WifiP2pClient.kt @@ -3,8 +3,6 @@ package be.mygod.vpnhotspot.client import android.net.wifi.p2p.WifiP2pDevice import be.mygod.vpnhotspot.net.TetherType -class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client() { - override val iface = p2pInterface - override val mac = p2p.deviceAddress ?: "" +class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client(p2p.deviceAddress ?: "", p2pInterface) { override val icon: Int get() = TetherType.WIFI_P2P.icon } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt index d0b4d8a9..4c8d1d91 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt @@ -96,6 +96,7 @@ class TetheringFragment : Fragment(), ServiceConnection { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false) + binding.setLifecycleOwner(this) binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.adapter = adapter diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt index d40b6686..d49e9c2c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt @@ -8,7 +8,6 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.TrafficRecorder import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.room.AppDatabase -import be.mygod.vpnhotspot.room.lookup import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.computeIfAbsentCompat @@ -173,7 +172,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : val toRemove = HashSet(clients.keys) for (neighbour in neighbours) { if (neighbour.dev != downstream || neighbour.ip !is Inet4Address || - AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr.macToLong()).blocked) continue + AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr.macToLong())?.blocked == true) continue toRemove.remove(neighbour.ip) try { clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt index b214bcc1..04fa1880 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TrafficRecorder.kt @@ -6,7 +6,6 @@ import be.mygod.vpnhotspot.DebugHelper import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.TrafficRecord -import be.mygod.vpnhotspot.room.insert import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.util.Event2 import be.mygod.vpnhotspot.util.RootSession diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt index 43d882e7..1d4fc093 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt @@ -1,5 +1,6 @@ package be.mygod.vpnhotspot.room +import androidx.lifecycle.LiveData import androidx.room.* @Entity @@ -8,14 +9,23 @@ data class ClientRecord(@PrimaryKey var nickname: CharSequence = "", var blocked: Boolean = false) { @androidx.room.Dao - interface Dao { + abstract class Dao { @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") - fun lookupOrNull(mac: Long): ClientRecord? + abstract fun lookup(mac: Long): ClientRecord? + + fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac) + + @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") + abstract fun lookupSync(mac: Long): LiveData @Insert(onConflict = OnConflictStrategy.REPLACE) - fun updateInternal(value: ClientRecord): Long + protected abstract fun updateInternal(value: ClientRecord): Long + fun update(value: ClientRecord) = check(updateInternal(value) == value.mac) + + @Transaction + open fun upsert(mac: Long, operation: ClientRecord.() -> Unit) = lookupOrDefault(mac).apply { + operation() + update(this) + } } } - -fun ClientRecord.Dao.lookup(mac: Long) = lookupOrNull(mac) ?: ClientRecord(mac) -fun ClientRecord.Dao.update(value: ClientRecord) = check(updateInternal(value) == value.mac) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt index dcf2a074..5aee1339 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt @@ -7,7 +7,8 @@ import java.net.InetAddress import java.nio.ByteBuffer import java.nio.ByteOrder -class Converters { +object Converters { + @JvmStatic @TypeConverter fun persistCharSequence(cs: CharSequence): ByteArray { val p = Parcel.obtain() @@ -19,6 +20,7 @@ class Converters { } } + @JvmStatic @TypeConverter fun unpersistCharSequence(data: ByteArray): CharSequence { val p = Parcel.obtain() @@ -31,9 +33,11 @@ class Converters { } } + @JvmStatic @TypeConverter fun persistInetAddress(address: InetAddress): ByteArray = address.address + @JvmStatic @TypeConverter fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt index 9fcf8aae..451a3a20 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt @@ -38,9 +38,13 @@ data class TrafficRecord( */ val previousId: Long? = null) { @androidx.room.Dao - interface Dao { + abstract class Dao { @Insert - fun insertInternal(value: TrafficRecord): Long + protected abstract fun insertInternal(value: TrafficRecord): Long + fun insert(value: TrafficRecord) { + check(value.id == null) + value.id = insertInternal(value) + } @Query(""" SELECT MIN(TrafficRecord.timestamp) AS timestamp, @@ -52,15 +56,10 @@ data class TrafficRecord( FROM TrafficRecord LEFT JOIN TrafficRecord AS Next ON TrafficRecord.id = Next.previousId /* We only want to find the last record for each chain so that we don't double count */ WHERE TrafficRecord.mac = :mac AND Next.id IS NULL""") - fun queryStats(mac: Long): ClientStats + abstract fun queryStats(mac: Long): ClientStats } } -fun TrafficRecord.Dao.insert(value: TrafficRecord) { - check(value.id == null) - value.id = insertInternal(value) -} - data class ClientStats( val timestamp: Long = 0, val count: Long = 0,