Support monitoring tethered interface
This would be useful to be used in together with Instant Tethering + Turn off hotspot automatically. Refine #26, #53.
This commit is contained in:
@@ -8,6 +8,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
|
|||||||
private var neighbours = emptyList<IpNeighbour>()
|
private var neighbours = emptyList<IpNeighbour>()
|
||||||
|
|
||||||
protected abstract val activeIfaces: List<String>
|
protected abstract val activeIfaces: List<String>
|
||||||
|
protected open val inactiveIfaces get() = emptyList<String>()
|
||||||
|
|
||||||
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
|
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
|
||||||
this.neighbours = neighbours
|
this.neighbours = neighbours
|
||||||
@@ -20,6 +21,8 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
|
|||||||
.distinctBy { it.lladdr }
|
.distinctBy { it.lladdr }
|
||||||
.size
|
.size
|
||||||
}
|
}
|
||||||
ServiceNotification.startForeground(this, activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) })
|
ServiceNotification.startForeground(this,
|
||||||
|
activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) },
|
||||||
|
inactiveIfaces)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
} else {
|
} else {
|
||||||
val routingManager = routingManager
|
val routingManager = routingManager
|
||||||
if (routingManager == null) {
|
if (routingManager == null) {
|
||||||
this.routingManager = RoutingManager.LocalOnly(this, iface).apply { initRouting() }
|
this.routingManager = RoutingManager.LocalOnly(this, iface).apply { start() }
|
||||||
IpNeighbourMonitor.registerCallback(this)
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
} else check(iface == routingManager.downstream)
|
} else check(iface == routingManager.downstream)
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun unregisterReceiver() {
|
private fun unregisterReceiver() {
|
||||||
routingManager?.stop()
|
routingManager?.destroy()
|
||||||
routingManager = null
|
routingManager = null
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
private fun doStart(group: WifiP2pGroup) {
|
private fun doStart(group: WifiP2pGroup) {
|
||||||
binder.group = group
|
binder.group = group
|
||||||
check(routingManager == null)
|
check(routingManager == null)
|
||||||
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { initRouting() }
|
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
|
||||||
status = Status.ACTIVE
|
status = Status.ACTIVE
|
||||||
showNotification(group)
|
showNotification(group)
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
}
|
}
|
||||||
private fun clean() {
|
private fun clean() {
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
routingManager?.stop()
|
routingManager?.destroy()
|
||||||
routingManager = null
|
routingManager = null
|
||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
ServiceNotification.stopForeground(this)
|
ServiceNotification.stopForeground(this)
|
||||||
|
|||||||
@@ -30,15 +30,21 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var routing: Routing? = null
|
var started = false
|
||||||
|
private var routing: Routing? = null
|
||||||
init {
|
init {
|
||||||
app.onPreCleanRoutings[this] = { routing?.stop() }
|
|
||||||
app.onRoutingsCleaned[this] = { initRouting() }
|
|
||||||
if (isWifi) WifiDoubleLock.acquire(this)
|
if (isWifi) WifiDoubleLock.acquire(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initRouting() = try {
|
fun start(): Boolean {
|
||||||
|
check(!started)
|
||||||
|
started = true
|
||||||
|
app.onPreCleanRoutings[this] = { routing?.stop() }
|
||||||
|
app.onRoutingsCleaned[this] = { initRouting() }
|
||||||
|
return initRouting()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRouting() = try {
|
||||||
routing = Routing(caller, downstream).apply {
|
routing = Routing(caller, downstream).apply {
|
||||||
try {
|
try {
|
||||||
configure()
|
configure()
|
||||||
@@ -58,9 +64,15 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
protected abstract fun Routing.configure()
|
protected abstract fun Routing.configure()
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
if (!started) return
|
||||||
routing?.revert()
|
routing?.revert()
|
||||||
if (isWifi) WifiDoubleLock.release(this)
|
|
||||||
app.onPreCleanRoutings -= this
|
app.onPreCleanRoutings -= this
|
||||||
app.onRoutingsCleaned -= this
|
app.onRoutingsCleaned -= this
|
||||||
|
started = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
if (isWifi) WifiDoubleLock.release(this)
|
||||||
|
stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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 java.util.*
|
||||||
|
|
||||||
object ServiceNotification {
|
object ServiceNotification {
|
||||||
private const val CHANNEL = "tethering"
|
private const val CHANNEL = "tethering"
|
||||||
private const val CHANNEL_ID = 1
|
private const val CHANNEL_ID = 1
|
||||||
|
|
||||||
private val deviceCountsMap = HashMap<Service, Map<String, Int>>()
|
private val deviceCountsMap = WeakHashMap<Service, Map<String, Int>>()
|
||||||
|
private val inactiveMap = WeakHashMap<Service, List<String>>()
|
||||||
private val manager = app.getSystemService<NotificationManager>()!!
|
private val manager = app.getSystemService<NotificationManager>()!!
|
||||||
|
|
||||||
private fun buildNotification(context: Context): Notification {
|
private fun buildNotification(context: Context): Notification {
|
||||||
@@ -27,32 +29,29 @@ object ServiceNotification {
|
|||||||
Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
|
Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
|
val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key }
|
||||||
return when (deviceCounts.size) {
|
val inactive = inactiveMap.values.flatten()
|
||||||
0 -> builder.build()
|
var lines = deviceCounts.map { (dev, size) ->
|
||||||
1 -> {
|
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
|
||||||
val (dev, size) = deviceCounts.single()
|
|
||||||
builder.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
|
||||||
size, size, dev))
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
else -> {
|
if (inactive.isNotEmpty()) {
|
||||||
|
lines += context.getString(R.string.notification_interfaces_inactive) + inactive.joinToString()
|
||||||
|
}
|
||||||
|
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
|
||||||
val deviceCount = deviceCounts.sumBy { it.value }
|
val deviceCount = deviceCounts.sumBy { it.value }
|
||||||
|
val interfaceCount = deviceCounts.size + inactive.size
|
||||||
NotificationCompat.BigTextStyle(builder
|
NotificationCompat.BigTextStyle(builder
|
||||||
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
||||||
deviceCount, deviceCount,
|
deviceCount, deviceCount,
|
||||||
context.resources.getQuantityString(R.plurals.notification_interfaces,
|
context.resources.getQuantityString(R.plurals.notification_interfaces,
|
||||||
deviceCounts.size, deviceCounts.size))))
|
interfaceCount, interfaceCount))))
|
||||||
.bigText(deviceCounts.joinToString("\n") { (dev, size) ->
|
.bigText(lines.joinToString("\n"))
|
||||||
context.resources.getQuantityString(R.plurals.notification_connected_devices,
|
|
||||||
size, size, dev)
|
|
||||||
})
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun startForeground(service: Service, deviceCounts: Map<String, Int>) {
|
fun startForeground(service: Service, deviceCounts: Map<String, Int>, inactive: List<String> = emptyList()) {
|
||||||
deviceCountsMap[service] = deviceCounts
|
deviceCountsMap[service] = deviceCounts
|
||||||
|
if (inactive.isEmpty()) inactiveMap.remove(service) else inactiveMap[service] = inactive
|
||||||
service.startForeground(CHANNEL_ID, buildNotification(service))
|
service.startForeground(CHANNEL_ID, buildNotification(service))
|
||||||
}
|
}
|
||||||
fun stopForeground(service: Service) {
|
fun stopForeground(service: Service) {
|
||||||
|
|||||||
@@ -16,16 +16,22 @@ import kotlinx.coroutines.launch
|
|||||||
class TetheringService : IpNeighbourMonitoringService() {
|
class TetheringService : IpNeighbourMonitoringService() {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ADD_INTERFACES = "interface.add"
|
const val EXTRA_ADD_INTERFACES = "interface.add"
|
||||||
|
const val EXTRA_ADD_INTERFACE_MONITOR = "interface.add.monitor"
|
||||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
inner class Binder : android.os.Binder() {
|
||||||
val routingsChanged = Event0()
|
val routingsChanged = Event0()
|
||||||
|
val monitoredIfaces get() = synchronized(downstreams) {
|
||||||
fun isActive(iface: String): Boolean = synchronized(downstreams) { downstreams.containsKey(iface) }
|
downstreams.values.filter { it.monitor }.map { it.downstream }
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class Downstream(caller: Any, downstream: String) :
|
fun isActive(iface: String) = synchronized(downstreams) { downstreams.containsKey(iface) }
|
||||||
|
fun isInactive(iface: String) = synchronized(downstreams) { downstreams[iface] }?.run { !started && monitor }
|
||||||
|
fun monitored(iface: String) = synchronized(downstreams) { downstreams[iface] }?.monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Downstream(caller: Any, downstream: String, var monitor: Boolean = false) :
|
||||||
RoutingManager(caller, downstream, TetherType.ofInterface(downstream).isWifi) {
|
RoutingManager(caller, downstream, TetherType.ofInterface(downstream).isWifi) {
|
||||||
override fun Routing.configure() {
|
override fun Routing.configure() {
|
||||||
forward()
|
forward()
|
||||||
@@ -41,12 +47,23 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
val extras = intent.extras ?: return@broadcastReceiver
|
val extras = intent.extras ?: return@broadcastReceiver
|
||||||
synchronized(downstreams) {
|
synchronized(downstreams) {
|
||||||
for (iface in downstreams.keys - TetheringManager.getTetheredIfaces(extras))
|
val toRemove = downstreams.toMutableMap() // make a copy
|
||||||
downstreams.remove(iface)?.stop()
|
for (iface in TetheringManager.getTetheredIfaces(extras)) {
|
||||||
|
val downstream = toRemove.remove(iface) ?: continue
|
||||||
|
if (downstream.monitor && !downstream.started) downstream.start()
|
||||||
|
}
|
||||||
|
for ((iface, downstream) in toRemove) {
|
||||||
|
if (downstream.monitor) downstream.stop() else downstreams.remove(iface)?.destroy()
|
||||||
|
}
|
||||||
onDownstreamsChangedLocked()
|
onDownstreamsChangedLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override val activeIfaces get() = synchronized(downstreams) { downstreams.keys.toList() }
|
override val activeIfaces get() = synchronized(downstreams) {
|
||||||
|
downstreams.values.filter { it.started }.map { it.downstream }
|
||||||
|
}
|
||||||
|
override val inactiveIfaces get() = synchronized(downstreams) {
|
||||||
|
downstreams.values.filter { !it.started }.map { it.downstream }
|
||||||
|
}
|
||||||
|
|
||||||
private fun onDownstreamsChangedLocked() {
|
private fun onDownstreamsChangedLocked() {
|
||||||
if (downstreams.isEmpty()) {
|
if (downstreams.isEmpty()) {
|
||||||
@@ -67,22 +84,30 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
override fun onBind(intent: Intent?) = binder
|
override fun onBind(intent: Intent?) = binder
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent != null) {
|
if (intent != null) synchronized(downstreams) {
|
||||||
val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()
|
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
||||||
synchronized(downstreams) {
|
if (downstreams[iface] == null) Downstream(this, iface).apply {
|
||||||
for (iface in ifaces) Downstream(this, iface).let { downstream ->
|
if (start()) check(downstreams.put(iface, this) == null) else destroy()
|
||||||
if (downstream.initRouting()) downstreams[iface] = downstream else downstream.stop()
|
|
||||||
}
|
}
|
||||||
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.stop()
|
}
|
||||||
|
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { iface ->
|
||||||
|
val downstream = downstreams[iface]
|
||||||
|
if (downstream == null) Downstream(this, iface, true).apply {
|
||||||
|
start()
|
||||||
|
check(downstreams.put(iface, this) == null)
|
||||||
|
downstreams[iface] = this
|
||||||
|
} else downstream.monitor = true
|
||||||
|
}
|
||||||
|
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.destroy()
|
||||||
|
updateNotification() // call this first just in case we are shutting down immediately
|
||||||
onDownstreamsChangedLocked()
|
onDownstreamsChangedLocked()
|
||||||
}
|
|
||||||
} else if (downstreams.isEmpty()) stopSelf(startId)
|
} else if (downstreams.isEmpty()) stopSelf(startId)
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
synchronized(downstreams) {
|
synchronized(downstreams) {
|
||||||
downstreams.values.forEach { it.stop() } // force clean to prevent leakage
|
downstreams.values.forEach { it.destroy() } // force clean to prevent leakage
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Intent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.TetheringService
|
import be.mygod.vpnhotspot.TetheringService
|
||||||
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
@@ -30,12 +31,14 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
|
|||||||
}
|
}
|
||||||
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
||||||
override val icon get() = TetherType.ofInterface(iface).icon
|
override val icon get() = TetherType.ofInterface(iface).icon
|
||||||
override val title get() = iface
|
override val title get() = if (parent.binder?.monitored(iface) == true) {
|
||||||
|
parent.getString(R.string.tethering_state_monitored, iface)
|
||||||
|
} else iface
|
||||||
override val text get() = addresses
|
override val text get() = addresses
|
||||||
override val active get() = parent.binder?.isActive(iface) == true
|
override val active get() = parent.binder?.isActive(iface) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val addresses = parent.ifaceLookup[iface]?.formatAddresses() ?: ""
|
private val addresses = parent.ifaceLookup[iface] ?.formatAddresses(parent.binder?.isInactive(iface) == true) ?: ""
|
||||||
override val type get() = VIEW_TYPE_INTERFACE
|
override val type get() = VIEW_TYPE_INTERFACE
|
||||||
private val data = Data()
|
private val data = Data()
|
||||||
|
|
||||||
|
|||||||
@@ -9,29 +9,26 @@ import android.content.pm.PackageManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
import androidx.core.content.ContextCompat
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
import be.mygod.vpnhotspot.*
|
||||||
import be.mygod.vpnhotspot.R
|
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
|
||||||
import be.mygod.vpnhotspot.TetheringService
|
|
||||||
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
|
||||||
class TetheringFragment : Fragment(), ServiceConnection {
|
class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClickListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val START_LOCAL_ONLY_HOTSPOT = 1
|
const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||||
const val REPEATER_EDIT_CONFIGURATION = 2
|
const val REPEATER_EDIT_CONFIGURATION = 2
|
||||||
@@ -63,7 +60,10 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
val list = ArrayList<Manager>()
|
val list = ArrayList<Manager>()
|
||||||
if (RepeaterService.supported) list.add(repeaterManager)
|
if (RepeaterService.supported) list.add(repeaterManager)
|
||||||
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
||||||
list.addAll(activeIfaces.map { InterfaceManager(this@TetheringFragment, it) }.sortedBy { it.iface })
|
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
||||||
|
updateMonitorList(activeIfaces - monitoredIfaces)
|
||||||
|
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
|
||||||
|
.map { InterfaceManager(this@TetheringFragment, it) })
|
||||||
list.add(ManageBar)
|
list.add(ManageBar)
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
list.addAll(tetherManagers)
|
list.addAll(tetherManagers)
|
||||||
@@ -94,6 +94,27 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
extras.getStringArrayList(TetheringManager.EXTRA_ERRORED_TETHER)!!)
|
extras.getStringArrayList(TetheringManager.EXTRA_ERRORED_TETHER)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
||||||
|
val toolbar = requireActivity().toolbar
|
||||||
|
val menu = toolbar.menu
|
||||||
|
if (canMonitor.isEmpty()) menu.removeItem(R.id.monitor) else {
|
||||||
|
var item = menu.findItem(R.id.monitor)
|
||||||
|
if (item == null) {
|
||||||
|
toolbar.inflateMenu(R.menu.toolbar_monitor)
|
||||||
|
item = menu.findItem(R.id.monitor)!!
|
||||||
|
}
|
||||||
|
item.subMenu.apply {
|
||||||
|
clear()
|
||||||
|
canMonitor.sorted().forEach { add(it).setOnMenuItemClickListener(this@TetheringFragment) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
|
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
|
||||||
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, item?.title ?: return false))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
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.setLifecycleOwner(this)
|
||||||
@@ -127,7 +148,13 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
binder = service as TetheringService.Binder
|
binder = service as TetheringService.Binder
|
||||||
service.routingsChanged[this] = { adapter.notifyDataSetChanged() }
|
service.routingsChanged[this] = {
|
||||||
|
requireContext().apply {
|
||||||
|
// flush tethered interfaces
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
|
}
|
||||||
|
}
|
||||||
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,4 +163,9 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
binder = null
|
binder = null
|
||||||
requireContext().unregisterReceiver(receiver)
|
requireContext().unregisterReceiver(receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
updateMonitorList()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
*/
|
*/
|
||||||
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For debugging: check that we do not start a Routing for the same interface twice.
|
||||||
|
*/
|
||||||
|
private var downstreams = mutableSetOf<String>()
|
||||||
|
|
||||||
fun clean() {
|
fun clean() {
|
||||||
TrafficRecorder.clean()
|
TrafficRecorder.clean()
|
||||||
RootSession.use {
|
RootSession.use {
|
||||||
@@ -54,6 +59,7 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
||||||
}
|
}
|
||||||
|
downstreams.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RootSession.Transaction.iptables(command: String, revert: String) {
|
private fun RootSession.Transaction.iptables(command: String, revert: String) {
|
||||||
@@ -82,6 +88,10 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
override val message: String get() = app.getString(R.string.exception_interface_not_found)
|
override val message: String get() = app.getString(R.string.exception_interface_not_found)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(downstreams.add(downstream)) { "Double routing detected" }
|
||||||
|
}
|
||||||
|
|
||||||
private val hostAddress = try {
|
private val hostAddress = try {
|
||||||
NetworkInterface.getByName(downstream)!!.interfaceAddresses!!.asSequence().single { it.address is Inet4Address }
|
NetworkInterface.getByName(downstream)!!.interfaceAddresses!!.asSequence().single { it.address is Inet4Address }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -302,5 +312,6 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
|||||||
fallbackUpstream.subrouting?.transaction?.revert()
|
fallbackUpstream.subrouting?.transaction?.revert()
|
||||||
upstream.subrouting?.transaction?.revert()
|
upstream.subrouting?.transaction?.revert()
|
||||||
transaction.revert()
|
transaction.revert()
|
||||||
|
check(downstreams.remove(downstream)) { "Double reverting detected" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
|
|||||||
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
} else mac
|
} else mac
|
||||||
|
|
||||||
fun NetworkInterface.formatAddresses() = SpannableStringBuilder().apply {
|
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
|
||||||
try {
|
try {
|
||||||
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) }
|
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) }
|
||||||
} catch (_: SocketException) { }
|
} catch (_: SocketException) { }
|
||||||
for (address in interfaceAddresses) {
|
if (!macOnly) for (address in interfaceAddresses) {
|
||||||
append(makeIpSpan(address.address))
|
append(makeIpSpan(address.address))
|
||||||
appendln("/${address.networkPrefixLength}")
|
appendln("/${address.networkPrefixLength}")
|
||||||
}
|
}
|
||||||
|
|||||||
10
mobile/src/main/res/drawable/ic_image_remove_red_eye.xml
Normal file
10
mobile/src/main/res/drawable/ic_image_remove_red_eye.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||||
|
</vector>
|
||||||
10
mobile/src/main/res/menu/toolbar_monitor.xml
Normal file
10
mobile/src/main/res/menu/toolbar_monitor.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item android:id="@+id/monitor"
|
||||||
|
android:icon="@drawable/ic_image_remove_red_eye"
|
||||||
|
android:title="@string/tethering_monitor"
|
||||||
|
app:showAsAction="always">
|
||||||
|
<menu/>
|
||||||
|
</item>
|
||||||
|
</menu>
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
<string name="tethering_temp_hotspot_failure_incompatible_mode">模式不兼容</string>
|
<string name="tethering_temp_hotspot_failure_incompatible_mode">模式不兼容</string>
|
||||||
<string name="tethering_temp_hotspot_failure_tethering_disallowed">共享被禁用</string>
|
<string name="tethering_temp_hotspot_failure_tethering_disallowed">共享被禁用</string>
|
||||||
|
|
||||||
|
<string name="tethering_monitor">监视…</string>
|
||||||
|
<string name="tethering_state_monitored">%s(监视)</string>
|
||||||
|
|
||||||
<string name="tethering_manage">管理系统共享…</string>
|
<string name="tethering_manage">管理系统共享…</string>
|
||||||
<string name="tethering_manage_offload_enabled">若 VPN 共享无法使用,请尝试禁用“开发者选项”中的“网络共享硬件加速”。</string>
|
<string name="tethering_manage_offload_enabled">若 VPN 共享无法使用,请尝试禁用“开发者选项”中的“网络共享硬件加速”。</string>
|
||||||
<!--
|
<!--
|
||||||
@@ -122,6 +125,7 @@
|
|||||||
<plurals name="notification_interfaces">
|
<plurals name="notification_interfaces">
|
||||||
<item quantity="other">%d 个接口</item>
|
<item quantity="other">%d 个接口</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="notification_interfaces_inactive">不活跃:</string>
|
||||||
|
|
||||||
<string name="failure_reason_unknown">未知 #%d</string>
|
<string name="failure_reason_unknown">未知 #%d</string>
|
||||||
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
||||||
|
|||||||
@@ -49,6 +49,9 @@
|
|||||||
<string name="tethering_temp_hotspot_failure_incompatible_mode">incompatible mode</string>
|
<string name="tethering_temp_hotspot_failure_incompatible_mode">incompatible mode</string>
|
||||||
<string name="tethering_temp_hotspot_failure_tethering_disallowed">tethering disallowed</string>
|
<string name="tethering_temp_hotspot_failure_tethering_disallowed">tethering disallowed</string>
|
||||||
|
|
||||||
|
<string name="tethering_monitor">Monitor…</string>
|
||||||
|
<string name="tethering_state_monitored">%s (monitored)</string>
|
||||||
|
|
||||||
<string name="tethering_manage">Manage system tethering…</string>
|
<string name="tethering_manage">Manage system tethering…</string>
|
||||||
<string name="tethering_manage_offload_enabled">Please disable Tethering hardware acceleration in Developer options
|
<string name="tethering_manage_offload_enabled">Please disable Tethering hardware acceleration in Developer options
|
||||||
if VPN tethering does not work.</string>
|
if VPN tethering does not work.</string>
|
||||||
@@ -131,6 +134,7 @@
|
|||||||
<item quantity="one">%d interface</item>
|
<item quantity="one">%d interface</item>
|
||||||
<item quantity="other">%d interfaces</item>
|
<item quantity="other">%d interfaces</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="notification_interfaces_inactive">"Inactive: "</string>
|
||||||
|
|
||||||
<string name="failure_reason_unknown">unknown #%d</string>
|
<string name="failure_reason_unknown">unknown #%d</string>
|
||||||
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user