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.res.Configuration
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import androidx.annotation.RequiresApi
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.localOnlyTetheredIfaces
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.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -44,6 +48,9 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
*/
override val coroutineContext = newSingleThreadContext("LocalOnlyHotspotService") + Job()
private var routingManager: RoutingManager? = null
private val handler = Handler()
@RequiresApi(28)
private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
@@ -81,6 +88,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation
if (!receiverRegistered) {
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor = TetherTimeoutMonitor(
this@LocalOnlyHotspotService, handler, reservation::close)
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
}
@@ -123,6 +132,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
stopSelf()
}
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
super.onIpNeighbourAvailable(neighbours)
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.isEmpty())
}
override fun onDestroy() {
binder.stop()
unregisterReceiver(true)
@@ -130,15 +144,19 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
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 {
routingManager?.destroy()
routingManager = null
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.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.core.content.getSystemService
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.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
@@ -87,6 +89,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
set(value) {
field = value
groupChanged(value)
if (Build.VERSION.SDK_INT >= 28) value?.clientList?.let {
timeoutMonitor?.onClientsChanged(it.isEmpty())
}
}
val groupChanged = StickyEvent1 { group }
@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 val binder = Binder()
private val handler = Handler()
@RequiresApi(28)
private var timeoutMonitor: TetherTimeoutMonitor? = null
private var receiverRegistered = false
private val receiver = broadcastReceiver { _, intent ->
when (intent.action) {
@@ -348,6 +355,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
* startService Step 3
*/
private fun doStartLocked(group: WifiP2pGroup) {
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor = TetherTimeoutMonitor(this, handler, binder::shutdown)
binder.group = group
if (persistNextGroup) {
networkName = group.networkName
@@ -389,6 +397,10 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
private fun cleanLocked() {
unregisterReceiver()
if (Build.VERSION.SDK_INT >= 28) {
timeoutMonitor?.close()
timeoutMonitor = null
}
routingManager?.destroy()
routingManager = null
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)
}
fun intentFilter(vararg actions: String): IntentFilter {
val result = IntentFilter()
actions.forEach { result.addAction(it) }
return result
}
fun intentFilter(vararg actions: String) = IntentFilter().also { actions.forEach(it::addAction) }
@BindingAdapter("android:src")
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 {
// 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.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)
} else it
}
}
fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
setSpan(CustomTabsUrlSpan("https://macvendors.co/results/$mac"), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)