Support showing connected devices from system tethering
Also fixes IP address not showing up.
This commit is contained in:
@@ -8,6 +8,7 @@ import android.databinding.BaseObservable
|
||||
import android.databinding.Bindable
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.net.wifi.p2p.WifiP2pDevice
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.support.v4.app.Fragment
|
||||
@@ -23,9 +24,12 @@ import android.view.*
|
||||
import android.widget.EditText
|
||||
import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
|
||||
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
|
||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.NetUtils
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
|
||||
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
|
||||
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener, IpNeighbourMonitor.Callback {
|
||||
inner class Data : BaseObservable() {
|
||||
val switchEnabled: Boolean
|
||||
@Bindable get() = when (binder?.service?.status) {
|
||||
@@ -55,30 +59,53 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
fun onStatusChanged() {
|
||||
notifyPropertyChanged(BR.switchEnabled)
|
||||
notifyPropertyChanged(BR.serviceStarted)
|
||||
onGroupChanged()
|
||||
val binder = binder
|
||||
onGroupChanged(if (binder?.active == true) binder.service.group else null)
|
||||
}
|
||||
fun onGroupChanged() {
|
||||
fun onGroupChanged(group: WifiP2pGroup?) {
|
||||
notifyPropertyChanged(BR.ssid)
|
||||
notifyPropertyChanged(BR.password)
|
||||
adapter.fetchClients()
|
||||
p2pInterface = group?.`interface`
|
||||
adapter.p2p = group?.clientList ?: emptyList()
|
||||
adapter.recreate()
|
||||
}
|
||||
|
||||
val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
|
||||
}
|
||||
|
||||
class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
|
||||
private var owner: WifiP2pDevice? = null
|
||||
private lateinit var clients: Collection<WifiP2pDevice>
|
||||
private lateinit var arpCache: Map<String, String>
|
||||
inner class Client(p2p: WifiP2pDevice? = null,
|
||||
private val pair: Map.Entry<IpNeighbour, IpNeighbour.State>? = null) {
|
||||
val iface = pair?.key?.dev ?: p2pInterface!!
|
||||
val mac = pair?.key?.lladdr ?: p2p!!.deviceAddress
|
||||
val ip = pair?.key?.ip
|
||||
|
||||
fun fetchClients() {
|
||||
val binder = binder
|
||||
if (binder?.active == true) {
|
||||
owner = binder.service.group?.owner
|
||||
clients = binder.service.group?.clientList ?: emptyList()
|
||||
arpCache = NetUtils.arp(binder.service.routing?.downstream)
|
||||
} else owner = null
|
||||
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon
|
||||
val title: CharSequence get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString(", ")
|
||||
val description: CharSequence get() = when (pair?.value) {
|
||||
IpNeighbour.State.INCOMPLETE, null -> "Connecting to $iface"
|
||||
IpNeighbour.State.VALID -> "Connected to $iface"
|
||||
IpNeighbour.State.VALID_DELAY -> "Connected to $iface (losing)"
|
||||
IpNeighbour.State.FAILED -> "Failed to connect to $iface"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
private inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
|
||||
private val clients = ArrayList<Client>()
|
||||
var p2p: Collection<WifiP2pDevice> = emptyList()
|
||||
var neighbours = emptyMap<IpNeighbour, IpNeighbour.State>()
|
||||
|
||||
fun recreate() {
|
||||
clients.clear()
|
||||
val map = HashMap(p2p.associateBy { it.deviceAddress })
|
||||
val tethered = (tetheredInterfaces + p2pInterface).filterNotNull()
|
||||
for (pair in neighbours) {
|
||||
val client = map.remove(pair.key.lladdr)
|
||||
if (client != null) clients.add(Client(client, pair))
|
||||
else if (tethered.contains(pair.key.dev)) clients.add(Client(pair = pair))
|
||||
}
|
||||
clients.addAll(map.map { Client(it.value) })
|
||||
clients.sortWith(compareBy<Client> { it.ip }.thenBy { it.mac })
|
||||
notifyDataSetChanged() // recreate everything
|
||||
binding.swipeRefresher.isRefreshing = false
|
||||
}
|
||||
@@ -87,25 +114,23 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
ClientViewHolder(ListitemClientBinding.inflate(LayoutInflater.from(parent.context)))
|
||||
|
||||
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
|
||||
val device = when (position) {
|
||||
0 -> owner
|
||||
else -> clients.elementAt(position - 1)
|
||||
}
|
||||
holder.binding.device = device
|
||||
holder.binding.ipAddress = when (position) {
|
||||
0 -> binder?.service?.routing?.hostAddress?.hostAddress
|
||||
else -> arpCache[device?.deviceAddress]
|
||||
}
|
||||
holder.binding.client = clients[position]
|
||||
holder.binding.executePendingBindings()
|
||||
}
|
||||
|
||||
override fun getItemCount() = if (owner == null) 0 else 1 + clients.size
|
||||
override fun getItemCount() = clients.size
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentRepeaterBinding
|
||||
private val data = Data()
|
||||
private val adapter = ClientAdapter()
|
||||
private var binder: RepeaterService.HotspotBinder? = null
|
||||
private var p2pInterface: String? = null
|
||||
private var tetheredInterfaces = emptySet<String>()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
tetheredInterfaces = NetUtils.getTetheredIfaces(intent.extras).toSet()
|
||||
adapter.recreate()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false)
|
||||
@@ -115,7 +140,10 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding
|
||||
binding.clients.itemAnimator = animator
|
||||
binding.clients.adapter = adapter
|
||||
binding.swipeRefresher.setOnRefreshListener { adapter.fetchClients() }
|
||||
binding.swipeRefresher.setOnRefreshListener {
|
||||
IpNeighbourMonitor.instance?.flush()
|
||||
adapter.recreate()
|
||||
}
|
||||
binding.toolbar.inflateMenu(R.menu.repeater)
|
||||
binding.toolbar.setOnMenuItemClickListener(this)
|
||||
return binding.root
|
||||
@@ -125,11 +153,16 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
super.onStart()
|
||||
val context = context!!
|
||||
context.bindService(Intent(context, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
IpNeighbourMonitor.registerCallback(this)
|
||||
context.registerReceiver(receiver, intentFilter(NetUtils.ACTION_TETHER_STATE_CHANGED))
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
val context = context!!
|
||||
context.unregisterReceiver(receiver)
|
||||
IpNeighbourMonitor.unregisterCallback(this)
|
||||
onServiceDisconnected(null)
|
||||
context!!.unbindService(this)
|
||||
context.unbindService(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -175,4 +208,9 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onIpNeighbourAvailable(neighbours: Map<IpNeighbour, IpNeighbour.State>) {
|
||||
adapter.neighbours = neighbours.toMap()
|
||||
}
|
||||
override fun postIpNeighbourAvailable() = adapter.recreate()
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
||||
return
|
||||
}
|
||||
this.group = group
|
||||
binder.data?.onGroupChanged()
|
||||
binder.data?.onGroupChanged(group)
|
||||
showNotification(group)
|
||||
debugLog(TAG, "P2P connection changed: $info\n$net\n$group")
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
.setType("text/plain")
|
||||
.putExtra(Intent.EXTRA_TEXT, Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
|
||||
.inputStream.bufferedReader().use { it.readText() })
|
||||
.inputStream.bufferedReader().readText())
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.abc_shareactionprovider_share_with)))
|
||||
} catch (e: IOException) {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -3,7 +3,6 @@ package be.mygod.vpnhotspot
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.databinding.BaseObservable
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
@@ -18,30 +17,12 @@ import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
||||
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||
import be.mygod.vpnhotspot.net.NetUtils
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
|
||||
class TetheringFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
/**
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/61fa313/core/res/res/values/config.xml#328
|
||||
*/
|
||||
private val usbRegexes = app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_usb_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wifiRegexes = app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wifi_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wimaxRegexes = app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wimax_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val bluetoothRegexes = app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_bluetooth_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
override fun onInserted(position: Int, count: Int) { }
|
||||
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
|
||||
@@ -59,13 +40,7 @@ class TetheringFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
private object StringSorter : DefaultSorter<String>()
|
||||
|
||||
class Data(val iface: String) : BaseObservable() {
|
||||
val icon: Int get() = when {
|
||||
usbRegexes.any { it.matcher(iface).matches() } -> R.drawable.ic_device_usb
|
||||
wifiRegexes.any { it.matcher(iface).matches() } -> R.drawable.ic_device_network_wifi
|
||||
wimaxRegexes.any { it.matcher(iface).matches() } -> R.drawable.ic_device_network_wifi
|
||||
bluetoothRegexes.any { it.matcher(iface).matches() } -> R.drawable.ic_device_bluetooth
|
||||
else -> R.drawable.ic_device_wifi_tethering
|
||||
}
|
||||
val icon: Int get() = TetherType.ofInterface(iface).icon
|
||||
var active = TetheringService.active.contains(iface)
|
||||
}
|
||||
|
||||
@@ -112,7 +87,6 @@ class TetheringFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
NetUtils.ACTION_TETHER_STATE_CHANGED -> adapter.update(NetUtils.getTetheredIfaces(intent.extras).toSet())
|
||||
}
|
||||
}
|
||||
private var receiverRegistered = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false)
|
||||
@@ -128,22 +102,16 @@ class TetheringFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (!receiverRegistered) {
|
||||
val context = context!!
|
||||
context.registerReceiver(receiver, intentFilter(NetUtils.ACTION_TETHER_STATE_CHANGED))
|
||||
LocalBroadcastManager.getInstance(context)
|
||||
.registerReceiver(receiver, intentFilter(TetheringService.ACTION_ACTIVE_INTERFACES_CHANGED))
|
||||
receiverRegistered = true
|
||||
}
|
||||
val context = context!!
|
||||
context.registerReceiver(receiver, intentFilter(NetUtils.ACTION_TETHER_STATE_CHANGED))
|
||||
LocalBroadcastManager.getInstance(context)
|
||||
.registerReceiver(receiver, intentFilter(TetheringService.ACTION_ACTIVE_INTERFACES_CHANGED))
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (receiverRegistered) {
|
||||
val context = context!!
|
||||
context.unregisterReceiver(receiver)
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
val context = context!!
|
||||
context.unregisterReceiver(receiver)
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.support.annotation.DrawableRes
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
fun debugLog(tag: String?, message: String?) {
|
||||
if (BuildConfig.DEBUG) Log.d(tag, message)
|
||||
@@ -30,25 +29,23 @@ fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageVi
|
||||
|
||||
private const val NOISYSU_TAG = "NoisySU"
|
||||
private const val NOISYSU_SUFFIX = "SUCCESS\n"
|
||||
fun loggerSuStream(command: String): InputStream {
|
||||
fun loggerSu(command: String): String? {
|
||||
val process = ProcessBuilder("su", "-c", command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
val err = try {
|
||||
process.errorStream.bufferedReader().use { it.readText() }
|
||||
try {
|
||||
val err = process.errorStream.bufferedReader().readText()
|
||||
if (err.isNotBlank()) Log.e(NOISYSU_TAG, err)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return try {
|
||||
process.inputStream.bufferedReader().readText()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
if (!err.isNullOrBlank()) Log.e(NOISYSU_TAG, err)
|
||||
return process.inputStream
|
||||
}
|
||||
fun loggerSu(command: String): String? = try {
|
||||
loggerSuStream(command).bufferedReader().use { it.readText() }
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
fun noisySu(commands: Iterable<String>): Boolean {
|
||||
var out = loggerSu("""function noisy() { "$@" || echo "$@" exited with $?; }
|
||||
|
||||
43
mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
Normal file
43
mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.debugLog
|
||||
|
||||
data class IpNeighbour(val ip: String, val dev: String, val lladdr: String) {
|
||||
enum class State {
|
||||
INCOMPLETE, VALID, VALID_DELAY, FAILED, DELETING
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IpNeighbour"
|
||||
|
||||
/**
|
||||
* Parser based on:
|
||||
* https://android.googlesource.com/platform/external/iproute2/+/ad0a6a2/ip/ipneigh.c#194
|
||||
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
||||
* Assumptions: IPv4 only, RTM_GETNEIGH is never used and show_stats = 0
|
||||
*/
|
||||
private val parser =
|
||||
"^(Deleted )?((.+?) )?(dev (.+?) )?(lladdr (.+?))?( proxy)?( ([INCOMPLET,RAHBSDYF]+))?\$".toRegex()
|
||||
fun parse(line: String): Pair<IpNeighbour, State>? {
|
||||
val match = parser.matchEntire(line)
|
||||
if (match == null) {
|
||||
if (!line.isBlank()) Log.w(TAG, line)
|
||||
return null
|
||||
}
|
||||
val neighbour = IpNeighbour(match.groupValues[3], match.groupValues[5], match.groupValues[7])
|
||||
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[10]) {
|
||||
"", "INCOMPLETE" -> State.INCOMPLETE
|
||||
"REACHABLE", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
||||
"DELAY" -> State.VALID_DELAY
|
||||
"FAILED" -> State.FAILED
|
||||
"NOARP" -> return null // skip
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown state encountered: ${match.groupValues[10]}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Pair(neighbour, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.debugLog
|
||||
import java.io.InterruptedIOException
|
||||
|
||||
class IpNeighbourMonitor private constructor() {
|
||||
companion object {
|
||||
private const val TAG = "IpNeighbourMonitor"
|
||||
private val callbacks = HashSet<Callback>()
|
||||
var instance: IpNeighbourMonitor? = null
|
||||
|
||||
fun registerCallback(callback: Callback) {
|
||||
if (!callbacks.add(callback)) return
|
||||
var monitor = instance
|
||||
if (monitor == null) {
|
||||
monitor = IpNeighbourMonitor()
|
||||
instance = monitor
|
||||
monitor.flush()
|
||||
} else synchronized(monitor.neighbours) { callback.onIpNeighbourAvailable(monitor.neighbours) }
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) {
|
||||
if (!callbacks.remove(callback) || callbacks.isNotEmpty()) return
|
||||
val monitor = instance ?: return
|
||||
instance = null
|
||||
monitor.monitor?.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for kotlin.concurrent.thread that silences uncaught exceptions.
|
||||
*/
|
||||
private fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
|
||||
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
|
||||
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
|
||||
thread.setUncaughtExceptionHandler { _, _ -> }
|
||||
if (start) thread.start()
|
||||
return thread
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onIpNeighbourAvailable(neighbours: Map<IpNeighbour, IpNeighbour.State>)
|
||||
fun postIpNeighbourAvailable() { }
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
private var updatePosted = false
|
||||
val neighbours = HashMap<IpNeighbour, IpNeighbour.State>()
|
||||
/**
|
||||
* Using monitor requires using /proc/self/ns/net which would be problematic on Android 6.0+.
|
||||
*
|
||||
* Source: https://source.android.com/security/enhancements/enhancements60
|
||||
*/
|
||||
private var monitor: Process? = null
|
||||
|
||||
init {
|
||||
thread(name = TAG + "-input") {
|
||||
val monitor = (if (Build.VERSION.SDK_INT >= 23)
|
||||
ProcessBuilder("su", "-c", "ip", "-4", "monitor", "neigh") else
|
||||
ProcessBuilder("ip", "-4", "monitor", "neigh"))
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
this.monitor = monitor
|
||||
thread(name = TAG + "-error") {
|
||||
try {
|
||||
monitor.errorStream.bufferedReader().forEachLine { Log.e(TAG, it) }
|
||||
} catch (ignore: InterruptedIOException) { }
|
||||
}
|
||||
try {
|
||||
monitor.inputStream.bufferedReader().forEachLine {
|
||||
debugLog(TAG, it)
|
||||
synchronized(neighbours) {
|
||||
val (neighbour, state) = IpNeighbour.parse(it) ?: return@forEachLine
|
||||
val changed = if (state == IpNeighbour.State.DELETING) neighbours.remove(neighbour) != null else
|
||||
neighbours.put(neighbour, state) != state
|
||||
if (changed) postUpdateLocked()
|
||||
}
|
||||
}
|
||||
Log.w(TAG, if (Build.VERSION.SDK_INT >= 26 && monitor.isAlive) "monitor closed stdout" else
|
||||
"monitor died unexpectedly")
|
||||
} catch (ignore: InterruptedIOException) { }
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() = thread(name = TAG + "-flush") {
|
||||
val process = ProcessBuilder("ip", "-4", "neigh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
val err = process.errorStream.bufferedReader().readText()
|
||||
if (err.isNotBlank()) Log.e(TAG, err)
|
||||
process.inputStream.bufferedReader().useLines {
|
||||
synchronized(neighbours) {
|
||||
neighbours.clear()
|
||||
neighbours.putAll(it.map(IpNeighbour.Companion::parse).filterNotNull().toMap())
|
||||
postUpdateLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postUpdateLocked() {
|
||||
if (updatePosted || instance != this) return
|
||||
handler.post {
|
||||
synchronized(neighbours) {
|
||||
for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
|
||||
updatePosted = false
|
||||
}
|
||||
for (callback in callbacks) callback.postIpNeighbourAvailable()
|
||||
}
|
||||
updatePosted = true
|
||||
}
|
||||
}
|
||||
43
mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
Normal file
43
mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.content.res.Resources
|
||||
import be.mygod.vpnhotspot.App
|
||||
import be.mygod.vpnhotspot.R
|
||||
|
||||
enum class TetherType {
|
||||
NONE, WIFI_P2P, USB, WIFI, WIMAX, BLUETOOTH;
|
||||
|
||||
val icon get() = when (this) {
|
||||
USB -> R.drawable.ic_device_usb
|
||||
WIFI_P2P, WIFI, WIMAX -> R.drawable.ic_device_network_wifi
|
||||
BLUETOOTH -> R.drawable.ic_device_bluetooth
|
||||
else -> R.drawable.ic_device_wifi_tethering
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/61fa313/core/res/res/values/config.xml#328
|
||||
*/
|
||||
private val usbRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_usb_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wifiRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wifi_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wimaxRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wimax_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val bluetoothRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_bluetooth_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
|
||||
fun ofInterface(iface: String, p2pDev: String? = null) = when {
|
||||
iface == p2pDev -> WIFI_P2P
|
||||
usbRegexes.any { it.matcher(iface).matches() } -> USB
|
||||
wifiRegexes.any { it.matcher(iface).matches() } -> WIFI
|
||||
wimaxRegexes.any { it.matcher(iface).matches() } -> WIMAX
|
||||
bluetoothRegexes.any { it.matcher(iface).matches() } -> BLUETOOTH
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,10 +102,6 @@
|
||||
android:id="@+id/clients"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/listitem_client"/>
|
||||
|
||||
@@ -3,33 +3,51 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<data>
|
||||
<import type="android.view.View"/>
|
||||
<variable
|
||||
name="device"
|
||||
type="android.net.wifi.p2p.WifiP2pDevice"/>
|
||||
<variable
|
||||
name="ipAddress"
|
||||
type="String"/>
|
||||
name="client"
|
||||
type="be.mygod.vpnhotspot.RepeaterFragment.Client"/>
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="4dp">
|
||||
|
||||
<TextView
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@{client.icon}"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
tools:src="@drawable/ic_device_network_wifi"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{device.deviceAddress}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="xx:xx:xx:xx:xx:xx"/>
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{ipAddress == null ? View.GONE : View.VISIBLE}"
|
||||
android:text="@{ipAddress}"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="192.168.49.123"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{client.title}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="192.168.49.123, 01:23:45:ab:cd:ef"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{client.description}"
|
||||
tools:text="Connected to p2p0"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
tools:src="@drawable/ic_device_network_wifi"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="8dp"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<Switch
|
||||
|
||||
Reference in New Issue
Block a user