Migrate to LiveData

Benefit includes: no more flush after changing nickname. Yep.
This commit is contained in:
Mygod
2019-01-26 14:13:19 +08:00
parent e1e44f468a
commit 94114f7a4b
12 changed files with 68 additions and 63 deletions

View File

@@ -14,7 +14,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get import androidx.lifecycle.get
import be.mygod.vpnhotspot.client.Client
import be.mygod.vpnhotspot.client.ClientViewModel import be.mygod.vpnhotspot.client.ClientViewModel
import be.mygod.vpnhotspot.client.ClientsFragment import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding import be.mygod.vpnhotspot.databinding.ActivityMainBinding
@@ -45,6 +44,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.setLifecycleOwner(this)
binding.navigation.setOnNavigationItemSelectedListener(this) binding.navigation.setOnNavigationItemSelectedListener(this)
if (savedInstanceState == null) displayFragment(TetheringFragment()) if (savedInstanceState == null) displayFragment(TetheringFragment())
badge = QBadgeView(this) badge = QBadgeView(this)
@@ -55,7 +55,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
badge.setGravityOffset(16f, 0f, true) badge.setGravityOffset(16f, 0f, true)
val model = ViewModelProviders.of(this).get<ClientViewModel>() val model = ViewModelProviders.of(this).get<ClientViewModel>()
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class) if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class)
model.clients.observe(this, Observer<List<Client>> { badge.badgeNumber = it.size }) model.clients.observe(this, Observer { badge.badgeNumber = it.size })
SmartSnackbar.Register(lifecycle, binding.fragmentHolder) SmartSnackbar.Register(lifecycle, binding.fragmentHolder)
} }

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.client
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
import androidx.lifecycle.Transformations
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R 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.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.lookup
import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.onEmpty import be.mygod.vpnhotspot.util.onEmpty
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.*
abstract class Client { open class Client(val mac: String, val iface: String) {
companion object DiffCallback : DiffUtil.ItemCallback<Client>() { companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) = override fun areItemsTheSame(oldItem: Client, newItem: Client) =
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem
} }
abstract val iface: String
abstract val mac: String
private val macIface get() = "$mac%$iface" private val macIface get() = "$mac%$iface"
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator) val ip = TreeMap<InetAddress, IpNeighbour.State>(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 open val icon get() = TetherType.ofInterface(iface).icon
val title: CharSequence get() { val title = Transformations.map(record) { record ->
val result = SpannableStringBuilder(record.nickname.onEmpty(macIface)) SpannableStringBuilder(record.nickname.onEmpty(macIface)).apply {
if (record.blocked) result.setSpan(StrikethroughSpan(), 0, result.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) if (record.blocked) setSpan(StrikethroughSpan(), 0, 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))
} }
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 { override fun equals(other: Any?): Boolean {
@@ -57,9 +58,9 @@ abstract class Client {
if (iface != other.iface) return false if (iface != other.iface) return false
if (mac != other.mac) return false if (mac != other.mac) return false
if (ip != other.ip) return false if (ip != other.ip) return false
if (record != other.record) return false if (record.value != other.record.value) return false
return true return true
} }
override fun hashCode() = Objects.hash(iface, mac, ip, record) override fun hashCode() = Objects.hash(iface, mac, ip, record.value)
} }

View File

@@ -40,7 +40,7 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
var client = clients[key] var client = clients[key]
if (client == null) { if (client == null) {
if (!tetheredInterfaces.contains(neighbour.dev)) continue if (!tetheredInterfaces.contains(neighbour.dev)) continue
client = TetheringClient(neighbour) client = Client(neighbour.lladdr, neighbour.dev)
clients[key] = client clients[key] = client
} }
client.ip += Pair(neighbour.ip, neighbour.state) client.ip += Pair(neighbour.ip, neighbour.state)

View File

@@ -25,8 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.versionedparcelable.VersionedParcelable import androidx.versionedparcelable.VersionedParcelable
import be.mygod.vpnhotspot.AlertDialogFragment import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.Empty import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
@@ -53,11 +53,9 @@ class ClientsFragment : Fragment() {
} }
override fun onClick(dialog: DialogInterface?, which: Int) { 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<EditText>(android.R.id.edit).text nickname = this@NicknameDialogFragment.dialog!!.findViewById<EditText>(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), private inner class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener, PopupMenu.OnMenuItemClickListener { View.OnClickListener, PopupMenu.OnMenuItemClickListener {
init { init {
binding.setLifecycleOwner(this@ClientsFragment) // todo some way better?
binding.root.setOnClickListener(this) binding.root.setOnClickListener(this)
} }
override fun onClick(v: View) { override fun onClick(v: View) {
PopupMenu(binding.root.context, binding.root).apply { PopupMenu(binding.root.context, binding.root).apply {
menuInflater.inflate(R.menu.popup_client, menu) 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) setOnMenuItemClickListener(this@ClientViewHolder)
show() show()
} }
@@ -111,7 +110,7 @@ class ClientsFragment : Fragment() {
return when (item?.itemId) { return when (item?.itemId) {
R.id.nickname -> { R.id.nickname -> {
val client = binding.client ?: return false 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") .show(fragmentManager ?: return false, "NicknameDialogFragment")
true true
} }
@@ -119,7 +118,9 @@ class ClientsFragment : Fragment() {
val client = binding.client ?: return false val client = binding.client ?: return false
val wasWorking = TrafficRecorder.isWorking(client.mac.macToLong()) val wasWorking = TrafficRecorder.isWorking(client.mac.macToLong())
client.record.apply { 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() IpNeighbourMonitor.instance?.flush()
if (!wasWorking && item.itemId == R.id.block) { if (!wasWorking && item.itemId == R.id.block) {
@@ -129,7 +130,7 @@ class ClientsFragment : Fragment() {
} }
R.id.stats -> { R.id.stats -> {
val client = binding.client ?: return false 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()))) AppDatabase.instance.trafficRecordDao.queryStats(client.mac.macToLong())))
.show(fragmentManager ?: return false, "StatsDialogFragment") .show(fragmentManager ?: return false, "StatsDialogFragment")
true true
@@ -186,6 +187,7 @@ class ClientsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_clients, container, false) binding = DataBindingUtil.inflate(inflater, R.layout.fragment_clients, container, false)
binding.setLifecycleOwner(this)
binding.clients.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.clients.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
binding.clients.itemAnimator = DefaultItemAnimator() binding.clients.itemAnimator = DefaultItemAnimator()
binding.clients.adapter = adapter binding.clients.adapter = adapter
@@ -194,7 +196,7 @@ class ClientsFragment : Fragment() {
IpNeighbourMonitor.instance?.flush() IpNeighbourMonitor.instance?.flush()
} }
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this, ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this,
Observer<List<Client>> { adapter.submitList(it.toMutableList()) }) Observer { adapter.submitList(it.toMutableList()) })
return binding.root return binding.root
} }

View File

@@ -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
}

View File

@@ -3,8 +3,6 @@ package be.mygod.vpnhotspot.client
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client() { class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client(p2p.deviceAddress ?: "", p2pInterface) {
override val iface = p2pInterface
override val mac = p2p.deviceAddress ?: ""
override val icon: Int get() = TetherType.WIFI_P2P.icon override val icon: Int get() = TetherType.WIFI_P2P.icon
} }

View File

@@ -96,6 +96,7 @@ class TetheringFragment : Fragment(), ServiceConnection {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false) binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false)
binding.setLifecycleOwner(this)
binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) binding.interfaces.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter binding.interfaces.adapter = adapter

View File

@@ -8,7 +8,6 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.lookup
import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.util.computeIfAbsentCompat import be.mygod.vpnhotspot.util.computeIfAbsentCompat
@@ -173,7 +172,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) :
val toRemove = HashSet(clients.keys) val toRemove = HashSet(clients.keys)
for (neighbour in neighbours) { for (neighbour in neighbours) {
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address || 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) toRemove.remove(neighbour.ip)
try { try {
clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) } clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) }

View File

@@ -6,7 +6,6 @@ import be.mygod.vpnhotspot.DebugHelper
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.TrafficRecord import be.mygod.vpnhotspot.room.TrafficRecord
import be.mygod.vpnhotspot.room.insert
import be.mygod.vpnhotspot.room.macToLong import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.Event2 import be.mygod.vpnhotspot.util.Event2
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.RootSession

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.room package be.mygod.vpnhotspot.room
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
@Entity @Entity
@@ -8,14 +9,23 @@ data class ClientRecord(@PrimaryKey
var nickname: CharSequence = "", var nickname: CharSequence = "",
var blocked: Boolean = false) { var blocked: Boolean = false) {
@androidx.room.Dao @androidx.room.Dao
interface Dao { abstract class Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @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<ClientRecord>
@Insert(onConflict = OnConflictStrategy.REPLACE) @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)

View File

@@ -7,7 +7,8 @@ import java.net.InetAddress
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
class Converters { object Converters {
@JvmStatic
@TypeConverter @TypeConverter
fun persistCharSequence(cs: CharSequence): ByteArray { fun persistCharSequence(cs: CharSequence): ByteArray {
val p = Parcel.obtain() val p = Parcel.obtain()
@@ -19,6 +20,7 @@ class Converters {
} }
} }
@JvmStatic
@TypeConverter @TypeConverter
fun unpersistCharSequence(data: ByteArray): CharSequence { fun unpersistCharSequence(data: ByteArray): CharSequence {
val p = Parcel.obtain() val p = Parcel.obtain()
@@ -31,9 +33,11 @@ class Converters {
} }
} }
@JvmStatic
@TypeConverter @TypeConverter
fun persistInetAddress(address: InetAddress): ByteArray = address.address fun persistInetAddress(address: InetAddress): ByteArray = address.address
@JvmStatic
@TypeConverter @TypeConverter
fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data) fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data)
} }

View File

@@ -38,9 +38,13 @@ data class TrafficRecord(
*/ */
val previousId: Long? = null) { val previousId: Long? = null) {
@androidx.room.Dao @androidx.room.Dao
interface Dao { abstract class Dao {
@Insert @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(""" @Query("""
SELECT MIN(TrafficRecord.timestamp) AS timestamp, 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 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 */ /* 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""") 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( data class ClientStats(
val timestamp: Long = 0, val timestamp: Long = 0,
val count: Long = 0, val count: Long = 0,