Implement MAC lookup (#68)

* Implement MAC lookup

* Refine error processing

* Use long to store MAC consistently

* Link back to macvendors.co

* Undo some havoc

* Do not show mac spans for TV

* Show MAC and IP in a consistent order

* Add IP spans by ipinfo.io

* Add SpanFormatter

* Fix IPv6 ipinfo.io link

* Refine SpanFormatter

* Fix pressing the link
This commit is contained in:
Mygod
2019-01-26 21:20:40 +08:00
committed by GitHub
parent 94114f7a4b
commit d4208affbb
38 changed files with 562 additions and 112 deletions

View File

@@ -4,6 +4,7 @@ import android.content.DialogInterface
import android.os.Bundle
import android.text.format.DateUtils
import android.text.format.Formatter
import android.text.method.LinkMovementMethod
import android.util.LongSparseArray
import android.view.LayoutInflater
import android.view.MenuItem
@@ -31,18 +32,27 @@ import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
import be.mygod.vpnhotspot.room.*
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientStats
import be.mygod.vpnhotspot.room.TrafficRecord
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.MainScope
import be.mygod.vpnhotspot.util.SpanFormatter
import be.mygod.vpnhotspot.util.computeIfAbsentCompat
import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.text.NumberFormat
class ClientsFragment : Fragment() {
data class NicknameArg(val mac: String, val nickname: CharSequence) : VersionedParcelable
class ClientsFragment : Fragment(), MainScope by MainScope.Supervisor() {
data class NicknameArg(val mac: Long, val nickname: CharSequence) : VersionedParcelable
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setView(R.layout.dialog_nickname)
setTitle(getString(R.string.clients_nickname_title, arg.mac))
setTitle(getString(R.string.clients_nickname_title, arg.mac.macToString()))
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
}
@@ -53,8 +63,11 @@ class ClientsFragment : Fragment() {
}
override fun onClick(dialog: DialogInterface?, which: Int) {
AppDatabase.instance.clientRecordDao.upsert(arg.mac.macToLong()) {
nickname = this@NicknameDialogFragment.dialog!!.findViewById<EditText>(android.R.id.edit).text
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
MacLookup.abort(arg.mac)
AppDatabase.instance.clientRecordDao.upsert(arg.mac) {
nickname = this@NicknameDialogFragment.dialog!!.findViewById<EditText>(android.R.id.edit).text
}
}
}
}
@@ -62,7 +75,7 @@ class ClientsFragment : Fragment() {
data class StatsArg(val title: CharSequence, val stats: ClientStats) : VersionedParcelable
class StatsDialogFragment : AlertDialogFragment<StatsArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(getString(R.string.clients_stats_title, arg.title))
setTitle(SpanFormatter.format(getString(R.string.clients_stats_title), arg.title))
val context = context
val resources = resources
val format = NumberFormat.getIntegerInstance(resources.configuration.locale)
@@ -93,8 +106,9 @@ 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.setLifecycleOwner(this@ClientsFragment)
binding.root.setOnClickListener(this)
binding.description.movementMethod = LinkMovementMethod.getInstance()
}
override fun onClick(v: View) {
@@ -116,11 +130,10 @@ class ClientsFragment : Fragment() {
}
R.id.block, R.id.unblock -> {
val client = binding.client ?: return false
val wasWorking = TrafficRecorder.isWorking(client.mac.macToLong())
client.record.apply {
val value = value ?: ClientRecord(client.mac.macToLong())
value.blocked = !value.blocked
AppDatabase.instance.clientRecordDao.update(value)
val wasWorking = TrafficRecorder.isWorking(client.mac)
client.obtainRecord().apply {
blocked = !blocked
AppDatabase.instance.clientRecordDao.update(this)
}
IpNeighbourMonitor.instance?.flush()
if (!wasWorking && item.itemId == R.id.block) {
@@ -129,10 +142,13 @@ class ClientsFragment : Fragment() {
true
}
R.id.stats -> {
val client = binding.client ?: return false
StatsDialogFragment().withArg(StatsArg(client.title.value!!, // todo?
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.macToLong())))
.show(fragmentManager ?: return false, "StatsDialogFragment")
binding.client?.let { client ->
launch(start = CoroutineStart.UNDISPATCHED) {
StatsDialogFragment().withArg(StatsArg(client.title.value!!,
AppDatabase.instance.trafficRecordDao.queryStats(client.mac)))
.show(fragmentManager ?: return@launch, "StatsDialogFragment")
}
}
true
}
else -> false
@@ -153,7 +169,7 @@ class ClientsFragment : Fragment() {
val client = getItem(position)
holder.binding.client = client
holder.binding.rate =
rates.computeIfAbsentCompat(Pair(client.iface, client.mac.macToLong())) { TrafficRate() }
rates.computeIfAbsentCompat(Pair(client.iface, client.mac)) { TrafficRate() }
holder.binding.executePendingBindings()
}
@@ -211,4 +227,9 @@ class ClientsFragment : Fragment() {
TrafficRecorder.foregroundListeners -= this
super.onStop()
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
}