Move MAC utils to MacAddressCompat

This commit is contained in:
Mygod
2020-05-30 02:06:41 -04:00
parent d328215764
commit e8fb62a0b3
15 changed files with 97 additions and 61 deletions

View File

@@ -16,7 +16,10 @@ android {
targetCompatibility = javaVersion targetCompatibility = javaVersion
} }
compileSdkVersion("android-R") compileSdkVersion("android-R")
kotlinOptions.jvmTarget = javaVersion.toString() kotlinOptions {
freeCompilerArgs = listOf("-XXLanguage:+InlineClasses")
jvmTarget = javaVersion.toString()
}
defaultConfig { defaultConfig {
applicationId = "be.mygod.vpnhotspot" applicationId = "be.mygod.vpnhotspot"
minSdkVersion(21) minSdkVersion(21)

View File

@@ -16,6 +16,7 @@ import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
@@ -23,7 +24,6 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupI
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -381,7 +381,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
check(routingManager == null) check(routingManager == null)
routingManager = object : RoutingManager.LocalOnly(this, group.`interface`!!) { routingManager = object : RoutingManager.LocalOnly(this, group.`interface`!!) {
override fun ifaceHandler(iface: NetworkInterface) { override fun ifaceHandler(iface: NetworkInterface) {
iface.hardwareAddress?.asIterable()?.macToString()?.let { lastMac = it } iface.hardwareAddress?.let { lastMac = MacAddressCompat.bytesToString(it) }
} }
}.apply { start() } }.apply { start() }
status = Status.ACTIVE status = Status.ACTIVE

View File

@@ -9,16 +9,16 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.InetAddressComparator import be.mygod.vpnhotspot.net.InetAddressComparator
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
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.ClientRecord import be.mygod.vpnhotspot.room.ClientRecord
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.makeIpSpan import be.mygod.vpnhotspot.util.makeIpSpan
import be.mygod.vpnhotspot.util.makeMacSpan import be.mygod.vpnhotspot.util.makeMacSpan
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.*
open class Client(val mac: Long, val iface: String) { open class Client(val mac: MacAddressCompat, 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
@@ -26,7 +26,7 @@ open class Client(val mac: Long, val iface: String) {
} }
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator) val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator)
val macString by lazy { mac.macToString() } val macString by lazy { mac.toString() }
private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac) private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac)
private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply { private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply {
append('%') append('%')
@@ -65,7 +65,7 @@ open class Client(val mac: Long, val iface: String) {
}.trimEnd() }.trimEnd()
} }
fun obtainRecord() = record.value ?: ClientRecord(mac) fun obtainRecord() = record.value ?: ClientRecord(mac.addr)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -10,11 +10,11 @@ import androidx.lifecycle.ViewModel
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback { class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
@@ -31,15 +31,15 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
val clients = MutableLiveData<List<Client>>() val clients = MutableLiveData<List<Client>>()
private fun populateClients() { private fun populateClients() {
val clients = HashMap<Pair<String, Long>, Client>() val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
val group = repeater?.group val group = repeater?.group
val p2pInterface = group?.`interface` val p2pInterface = group?.`interface`
if (p2pInterface != null) { if (p2pInterface != null) {
for (client in p2p) clients[Pair(p2pInterface, client.deviceAddress.macToLong())] = for (client in p2p) clients[p2pInterface to MacAddressCompat.fromString(client.deviceAddress)] =
WifiP2pClient(p2pInterface, client) WifiP2pClient(p2pInterface, client)
} }
for (neighbour in neighbours) { for (neighbour in neighbours) {
val key = Pair(neighbour.dev, neighbour.lladdr) val key = neighbour.dev to neighbour.lladdr
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

View File

@@ -30,13 +30,13 @@ import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R 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.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor 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.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientStats import be.mygod.vpnhotspot.room.ClientStats
import be.mygod.vpnhotspot.room.TrafficRecord import be.mygod.vpnhotspot.room.TrafficRecord
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.SpanFormatter import be.mygod.vpnhotspot.util.SpanFormatter
import be.mygod.vpnhotspot.util.toPluralInt import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -46,11 +46,11 @@ import java.text.NumberFormat
class ClientsFragment : Fragment() { class ClientsFragment : Fragment() {
@Parcelize @Parcelize
data class NicknameArg(val mac: Long, val nickname: CharSequence) : Parcelable data class NicknameArg(val mac: MacAddressCompat, val nickname: CharSequence) : Parcelable
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() { class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setView(R.layout.dialog_nickname) setView(R.layout.dialog_nickname)
setTitle(getString(R.string.clients_nickname_title, arg.mac.macToString())) setTitle(getString(R.string.clients_nickname_title, arg.mac.toString()))
setPositiveButton(android.R.string.ok, listener) setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null) setNegativeButton(android.R.string.cancel, null)
setNeutralButton(emojize(getText(R.string.clients_nickname_set_to_vendor)), listener) setNeutralButton(emojize(getText(R.string.clients_nickname_set_to_vendor)), listener)
@@ -153,7 +153,7 @@ class ClientsFragment : Fragment() {
withContext(Dispatchers.Unconfined) { withContext(Dispatchers.Unconfined) {
StatsDialogFragment().withArg(StatsArg( StatsDialogFragment().withArg(StatsArg(
client.title.value ?: return@withContext, client.title.value ?: return@withContext,
AppDatabase.instance.trafficRecordDao.queryStats(client.mac) AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)
)).show(this@ClientsFragment) )).show(this@ClientsFragment)
} }
} }
@@ -181,7 +181,7 @@ class ClientsFragment : Fragment() {
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) { override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
val client = getItem(position) val client = getItem(position)
holder.binding.client = client holder.binding.client = client
holder.binding.rate = rates.computeIfAbsent(Pair(client.iface, client.mac)) { TrafficRate() } holder.binding.rate = rates.computeIfAbsent(client.iface to client.mac) { TrafficRate() }
holder.binding.executePendingBindings() holder.binding.executePendingBindings()
} }
@@ -196,7 +196,9 @@ class ClientsFragment : Fragment() {
check(newRecord.receivedPackets == oldRecord.receivedPackets) check(newRecord.receivedPackets == oldRecord.receivedPackets)
check(newRecord.receivedBytes == oldRecord.receivedBytes) check(newRecord.receivedBytes == oldRecord.receivedBytes)
} else { } else {
val rate = rates.computeIfAbsent(Pair(newRecord.downstream, newRecord.mac)) { TrafficRate() } val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) {
TrafficRate()
}
if (rate.send < 0 || rate.receive < 0) { if (rate.send < 0 || rate.receive < 0) {
rate.send = 0 rate.send = 0
rate.receive = 0 rate.receive = 0
@@ -211,7 +213,7 @@ class ClientsFragment : Fragment() {
private lateinit var binding: FragmentClientsBinding private lateinit var binding: FragmentClientsBinding
private val adapter = ClientAdapter() private val adapter = ClientAdapter()
private var rates = mutableMapOf<Pair<String, Long>, TrafficRate>() private var rates = mutableMapOf<Pair<String, MacAddressCompat>, TrafficRate>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentClientsBinding.inflate(inflater, container, false) binding = FragmentClientsBinding.inflate(inflater, container, false)

View File

@@ -5,8 +5,8 @@ import android.os.Build
import androidx.annotation.MainThread import androidx.annotation.MainThread
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -22,26 +22,26 @@ import java.net.URL
* This class generates a default nickname for new clients. * This class generates a default nickname for new clients.
*/ */
object MacLookup { object MacLookup {
class UnexpectedError(val mac: Long, val error: String) : JSONException("") { class UnexpectedError(val mac: MacAddressCompat, val error: String) : JSONException("") {
private fun formatMessage(context: Context) = private fun formatMessage(context: Context) =
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.macToString(), error) context.getString(R.string.clients_mac_lookup_unexpected_error, mac.toString(), error)
override val message get() = formatMessage(app.english) override val message get() = formatMessage(app.english)
override fun getLocalizedMessage() = formatMessage(app) override fun getLocalizedMessage() = formatMessage(app)
} }
private val macLookupBusy = mutableMapOf<Long, Pair<HttpURLConnection, Job>>() private val macLookupBusy = mutableMapOf<MacAddressCompat, Pair<HttpURLConnection, Job>>()
private val countryCodeRegex = "([A-Z]{2})\\s*\$".toRegex() // http://en.wikipedia.org/wiki/ISO_3166-1 private val countryCodeRegex = "([A-Z]{2})\\s*\$".toRegex() // http://en.wikipedia.org/wiki/ISO_3166-1
@MainThread @MainThread
fun abort(mac: Long) = macLookupBusy.remove(mac)?.let { (conn, job) -> fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
job.cancel() job.cancel()
if (Build.VERSION.SDK_INT >= 26) conn.disconnect() else GlobalScope.launch(Dispatchers.IO) { conn.disconnect() } if (Build.VERSION.SDK_INT >= 26) conn.disconnect() else GlobalScope.launch(Dispatchers.IO) { conn.disconnect() }
} }
@MainThread @MainThread
fun perform(mac: Long, explicit: Boolean = false) { fun perform(mac: MacAddressCompat, explicit: Boolean = false) {
abort(mac) abort(mac)
val conn = URL("https://macvendors.co/api/" + mac.macToString()).openConnection() as HttpURLConnection val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) { macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
try { try {
val response = conn.inputStream.bufferedReader().readText() val response = conn.inputStream.bufferedReader().readText()
@@ -69,7 +69,7 @@ object MacLookup {
} }
} }
private fun extractCountry(mac: Long, response: String, obj: JSONObject): MatchResult? { private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? {
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it } countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
val address = obj.optString("address") val address = obj.optString("address")
if (address.isBlank()) return null if (address.isBlank()) return null

View File

@@ -1,9 +1,10 @@
package be.mygod.vpnhotspot.client package be.mygod.vpnhotspot.client
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.macToLong
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client(p2p.deviceAddress!!.macToLong(), p2pInterface) { class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) :
Client(MacAddressCompat.fromString(p2p.deviceAddress!!), p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon override val icon: Int get() = TetherType.WIFI_P2P.icon
} }

View File

@@ -3,7 +3,6 @@ package be.mygod.vpnhotspot.net
import android.os.Build import android.os.Build
import android.system.ErrnoException import android.system.ErrnoException
import android.system.OsConstants import android.system.OsConstants
import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.parseNumericAddress import be.mygod.vpnhotspot.util.parseNumericAddress
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@@ -13,7 +12,7 @@ import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, val state: State) { data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) {
enum class State { enum class State {
INCOMPLETE, VALID, FAILED, DELETING INCOMPLETE, VALID, FAILED, DELETING
} }
@@ -56,12 +55,12 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, v
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}") else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}")
} }
val mac = try { val mac = try {
lladdr.macToLong() MacAddressCompat.fromString(lladdr)
} catch (e: NumberFormatException) { } catch (e: IllegalArgumentException) {
if (match.groups[4] == null) return emptyList() if (match.groups[4] == null) return emptyList()
// for DELETING, we only care about IP address and do not care if MAC is not present // for DELETING, we only care about IP address and do not care if MAC is not present
if (state != State.DELETING) Timber.w(IOException("Failed to find MAC address for $line")) if (state != State.DELETING) Timber.w(IOException("Failed to find MAC address for $line"))
0L MacAddressCompat.ALL_ZEROS_ADDRESS
} }
val result = IpNeighbour(ip, dev, mac, state) val result = IpNeighbour(ip, dev, mac, state)
val devParser = devFallback.matchEntire(dev) val devParser = devFallback.matchEntire(dev)

View File

@@ -0,0 +1,45 @@
package be.mygod.vpnhotspot.net
import java.nio.ByteBuffer
import java.nio.ByteOrder
inline class MacAddressCompat(val addr: Long) {
companion object {
private const val ETHER_ADDR_LEN = 6
/**
* The MacAddress zero MAC address.
*
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
* @hide
*/
val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
fun bytesToString(addr: ByteArray): String {
require(addr.size == ETHER_ADDR_LEN) { addr.contentToString() + " was not a valid MAC address" }
return addr.joinToString(":") { "%02x".format(it) }
}
fun bytesToString(addr: Collection<Byte>): String {
require(addr.size == ETHER_ADDR_LEN) { addr.joinToString() + " was not a valid MAC address" }
return addr.joinToString(":") { "%02x".format(it) }
}
@Throws(IllegalArgumentException::class)
fun fromString(addr: String) = MacAddressCompat(ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
try {
put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
} catch (e: NumberFormatException) {
throw IllegalArgumentException(e)
}
reset()
long
})
}
override fun toString() = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(addr)
bytesToString(array().take(6))
}
}

View File

@@ -208,7 +208,7 @@ class Routing(private val caller: Any, private val downstream: String,
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM) private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
private var disableSystem: RootSession.Transaction? = null private var disableSystem: RootSession.Transaction? = null
private inner class Client(private val ip: Inet4Address, mac: Long) : AutoCloseable { private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable {
private val transaction = RootSession.beginTransaction().safeguard { private val transaction = RootSession.beginTransaction().safeguard {
val address = ip.hostAddress val address = ip.hostAddress
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT") iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.net.monitor package be.mygod.vpnhotspot.net.monitor
import android.util.LongSparseArray import android.util.LongSparseArray
import be.mygod.vpnhotspot.net.MacAddressCompat
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
@@ -20,8 +21,8 @@ object TrafficRecorder {
private val records = mutableMapOf<Pair<InetAddress, String>, TrafficRecord>() private val records = mutableMapOf<Pair<InetAddress, String>, TrafficRecord>()
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>() val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
fun register(ip: InetAddress, downstream: String, mac: Long) { fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream) val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
AppDatabase.instance.trafficRecordDao.insert(record) AppDatabase.instance.trafficRecordDao.insert(record)
synchronized(this) { synchronized(this) {
Timber.d("Registering $ip%$downstream") Timber.d("Registering $ip%$downstream")
@@ -149,5 +150,5 @@ object TrafficRecorder {
/** /**
* Possibly inefficient. Don't call this too often. * Possibly inefficient. Don't call this too often.
*/ */
fun isWorking(mac: Long) = records.values.any { it.mac == mac } fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr }
} }

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.room.* import androidx.room.*
import be.mygod.vpnhotspot.net.MacAddressCompat
@Entity @Entity
data class ClientRecord(@PrimaryKey data class ClientRecord(@PrimaryKey
@@ -14,7 +15,7 @@ data class ClientRecord(@PrimaryKey
abstract class Dao { abstract class Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupBlocking(mac: Long): ClientRecord? protected abstract fun lookupBlocking(mac: Long): ClientRecord?
fun lookupOrDefaultBlocking(mac: Long) = lookupBlocking(mac) ?: ClientRecord(mac) fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract suspend fun lookup(mac: Long): ClientRecord? protected abstract suspend fun lookup(mac: Long): ClientRecord?
@@ -22,14 +23,15 @@ data class ClientRecord(@PrimaryKey
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?> protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
fun lookupOrDefaultSync(mac: Long) = lookupSync(mac).map { it ?: ClientRecord(mac) } fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) }
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun updateInternal(value: ClientRecord): Long protected abstract suspend fun updateInternal(value: ClientRecord): Long
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac) suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac)
@Transaction @Transaction
open suspend fun upsert(mac: Long, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(mac).apply { open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
mac.addr).apply {
operation() operation()
update(this) update(this)
} }

View File

@@ -5,8 +5,6 @@ import androidx.room.TypeConverter
import be.mygod.vpnhotspot.util.useParcel import be.mygod.vpnhotspot.util.useParcel
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
object Converters { object Converters {
@JvmStatic @JvmStatic
@@ -37,18 +35,3 @@ object Converters {
@TypeConverter @TypeConverter
fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data) fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data)
} }
fun String.macToLong(): Long = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
put(split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
reset()
long
}
fun Iterable<Byte>.macToString() = joinToString(":") { "%02x".format(it) }
fun Long.macToString(): String = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(this@macToString)
array().take(6).macToString()
}

View File

@@ -19,7 +19,7 @@ import androidx.databinding.BindingAdapter
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.room.macToString import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
@@ -94,7 +94,7 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply { fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
try { try {
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) } hardwareAddress?.let { appendln(makeMacSpan(MacAddressCompat.bytesToString(it))) }
} catch (_: SocketException) { } } catch (_: SocketException) { }
if (!macOnly) for (address in interfaceAddresses) { if (!macOnly) for (address in interfaceAddresses) {
append(makeIpSpan(address.address)) append(makeIpSpan(address.address))

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.room package be.mygod.vpnhotspot.net
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
@@ -7,7 +7,7 @@ class ConvertersTest {
@Test @Test
fun macSerialization() { fun macSerialization() {
for (test in listOf("01:23:45:67:89:ab", "DE:AD:88:88:BE:EF")) { for (test in listOf("01:23:45:67:89:ab", "DE:AD:88:88:BE:EF")) {
assertTrue(test.equals(test.macToLong().macToString(), true)) assertTrue(test.equals(MacAddressCompat.fromString(test).toString(), true))
} }
} }
} }