Add tether timeout monitor for Android 9+

This commit is contained in:
Mygod
2019-07-18 19:08:34 +08:00
parent 81979aad38
commit b4121b7d66
4 changed files with 135 additions and 14 deletions

View File

@@ -4,11 +4,15 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour
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.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -44,6 +48,9 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
*/ */
override val coroutineContext = newSingleThreadContext("LocalOnlyHotspotService") + Job() override val coroutineContext = newSingleThreadContext("LocalOnlyHotspotService") + Job()
private var routingManager: RoutingManager? = null private var routingManager: RoutingManager? = null
private val handler = Handler()
@RequiresApi(28)
private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
@@ -81,6 +88,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (reservation == null) onFailed(-2) else { if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation this@LocalOnlyHotspotService.reservation = reservation
if (!receiverRegistered) { if (!receiverRegistered) {
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor = TetherTimeoutMonitor(
this@LocalOnlyHotspotService, handler, reservation::close)
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true receiverRegistered = true
} }
@@ -123,6 +132,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
stopSelf() stopSelf()
} }
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
super.onIpNeighbourAvailable(neighbours)
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.isEmpty())
}
override fun onDestroy() { override fun onDestroy() {
binder.stop() binder.stop()
unregisterReceiver(true) unregisterReceiver(true)
@@ -130,15 +144,19 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
} }
private fun unregisterReceiver(exit: Boolean = false) { private fun unregisterReceiver(exit: Boolean = false) {
if (receiverRegistered) {
unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this)
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close()
timeoutMonitor = null
}
receiverRegistered = false
}
launch { launch {
routingManager?.destroy() routingManager?.destroy()
routingManager = null routingManager = null
if (exit) cancel() if (exit) cancel()
} }
if (receiverRegistered) {
unregisterReceiver(receiver)
IpNeighbourMonitor.unregisterCallback(this)
receiverRegistered = false
}
} }
} }

View File

@@ -10,10 +10,12 @@ import android.net.wifi.p2p.*
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes 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.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
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
@@ -87,6 +89,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
set(value) { set(value) {
field = value field = value
groupChanged(value) groupChanged(value)
if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let {
timeoutMonitor?.onClientsChanged(it.isEmpty())
}
} }
val groupChanged = StickyEvent1 { group } val groupChanged = StickyEvent1 { group }
@Deprecated("Not initialized and no use at all since API 29") @Deprecated("Not initialized and no use at all since API 29")
@@ -118,6 +123,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
private var channel: WifiP2pManager.Channel? = null private var channel: WifiP2pManager.Channel? = null
private val binder = Binder() private val binder = Binder()
private val handler = Handler() private val handler = Handler()
@RequiresApi(28)
private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
when (intent.action) { when (intent.action) {
@@ -348,6 +355,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
* startService Step 3 * startService Step 3
*/ */
private fun doStartLocked(group: WifiP2pGroup) { private fun doStartLocked(group: WifiP2pGroup) {
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor = TetherTimeoutMonitor(this, handler, binder::shutdown)
binder.group = group binder.group = group
if (persistNextGroup) { if (persistNextGroup) {
networkName = group.networkName networkName = group.networkName
@@ -389,6 +397,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
private fun cleanLocked() { private fun cleanLocked() {
unregisterReceiver() unregisterReceiver()
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close()
timeoutMonitor = null
}
routingManager?.destroy() routingManager?.destroy()
routingManager = null routingManager = null
status = Status.IDLE status = Status.IDLE

View File

@@ -0,0 +1,95 @@
package be.mygod.vpnhotspot.net.monitor
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.database.ContentObserver
import android.os.BatteryManager
import android.os.Handler
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.os.postDelayed
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.intentFilter
import timber.log.Timber
import java.lang.IllegalArgumentException
@RequiresApi(28)
class TetherTimeoutMonitor(private val context: Context, private val handler: Handler, private val onTimeout: () -> Unit):
ContentObserver(handler), AutoCloseable {
/**
* config_wifi_framework_soft_ap_timeout_delay was introduced in Android 9.
*
* Source: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/87ed136/service/java/com/android/server/wifi/SoftApManager.java
*/
companion object {
/**
* Whether soft AP will shut down after a timeout period when no devices are connected.
*
* Type: int (0 for false, 1 for true)
*/
private const val SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled"
/**
* Minimum limit to use for timeout delay if the value from overlay setting is too small.
*/
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
private val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
private val timeout by lazy {
app.resources.getInteger(Resources.getSystem().getIdentifier(
"config_wifi_framework_soft_ap_timeout_delay", "integer", "android")).let { delay ->
if (delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
Timber.w("Overriding timeout delay with minimum limit value: $delay < $MIN_SOFT_AP_TIMEOUT_DELAY_MS")
MIN_SOFT_AP_TIMEOUT_DELAY_MS
} else delay
}
}
}
private var charging = when (context.registerReceiver(null, intentFilter(Intent.ACTION_BATTERY_CHANGED))
?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) {
BatteryManager.BATTERY_STATUS_CHARGING, BatteryManager.BATTERY_STATUS_FULL -> true
null, -1 -> false.also { Timber.w(Exception("Battery status not found")) }
else -> false
}
private var noClient = true
private var timeoutPending = false
private val receiver = broadcastReceiver { _, intent ->
charging = when (intent.action) {
Intent.ACTION_POWER_CONNECTED -> true
Intent.ACTION_POWER_DISCONNECTED -> false
else -> throw IllegalArgumentException("Invalid intent.action")
}
onChange(true)
}.also {
context.registerReceiver(it, intentFilter(Intent.ACTION_POWER_CONNECTED, Intent.ACTION_POWER_DISCONNECTED))
context.contentResolver.registerContentObserver(Settings.Global.getUriFor(SOFT_AP_TIMEOUT_ENABLED), true, this)
}
override fun close() {
context.unregisterReceiver(receiver)
context.contentResolver.unregisterContentObserver(this)
}
fun onClientsChanged(noClient: Boolean) {
this.noClient = noClient
onChange(true)
}
override fun onChange(selfChange: Boolean) {
// super.onChange(selfChange) should not do anything
if (enabled && noClient && !charging) {
if (!timeoutPending) {
handler.postDelayed(timeout.toLong(), this, onTimeout)
timeoutPending = true
}
} else {
if (timeoutPending) {
handler.removeCallbacksAndMessages(this)
timeoutPending = false
}
}
}
}

View File

@@ -58,11 +58,7 @@ fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastR
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent) override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
} }
fun intentFilter(vararg actions: String): IntentFilter { fun intentFilter(vararg actions: String) = IntentFilter().also { actions.forEach(it::addAction) }
val result = IntentFilter()
actions.forEach { result.addAction(it) }
return result
}
@BindingAdapter("android:src") @BindingAdapter("android:src")
fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageView.setImageResource(resource) fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageView.setImageResource(resource)
@@ -74,11 +70,11 @@ fun setVisibility(view: View, value: Boolean) {
fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let { fun makeIpSpan(ip: InetAddress) = ip.hostAddress.let {
// exclude all bogon IP addresses supported by Android APIs // exclude all bogon IP addresses supported by Android APIs
if (app.hasTouch && !(ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress || if (!app.hasTouch || ip.isMulticastAddress || ip.isAnyLocalAddress || ip.isLoopbackAddress ||
ip.isLinkLocalAddress || ip.isSiteLocalAddress || ip.isMCGlobal || ip.isMCNodeLocal || ip.isLinkLocalAddress || ip.isSiteLocalAddress || ip.isMCGlobal || ip.isMCNodeLocal ||
ip.isMCLinkLocal || ip.isMCSiteLocal || ip.isMCOrgLocal)) SpannableString(it).apply { ip.isMCLinkLocal || ip.isMCSiteLocal || ip.isMCOrgLocal) it else SpannableString(it).apply {
setSpan(CustomTabsUrlSpan("https://ipinfo.io/$it"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(CustomTabsUrlSpan("https://ipinfo.io/$it"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} else it }
} }
fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply { 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)