diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index 158c6a7c..c647c2fe 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -54,7 +54,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { private val dispatcher = newSingleThreadContext("LocalOnlyHotspotService") override val coroutineContext = dispatcher + Job() private var routingManager: RoutingManager? = null - @RequiresApi(28) private var timeoutMonitor: TetherTimeoutMonitor? = null private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> @@ -86,8 +85,11 @@ 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, reservation::close) + val configuration = binder.configuration!! + if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { + timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, + coroutineContext) { reservation.close() } + } registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) receiverRegistered = true } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index fa67c8e3..a97d844b 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -47,6 +47,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene private const val KEY_PASSPHRASE = "service.repeater.passphrase" private const val KEY_OPERATING_BAND = "service.repeater.band.v2" private const val KEY_OPERATING_CHANNEL = "service.repeater.oc" + private const val KEY_AUTO_SHUTDOWN = "service.repeater.autoShutdown" + private const val KEY_SHUTDOWN_TIMEOUT = "service.repeater.shutdownTimeout" private const val KEY_DEVICE_ADDRESS = "service.repeater.mac" /** * Placeholder for bypassing networkName check. @@ -83,6 +85,12 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene return if (result > 0) result else 0 } set(value) = app.pref.edit { putString(KEY_OPERATING_CHANNEL, value.toString()) } + var isAutoShutdownEnabled: Boolean + get() = app.pref.getBoolean(KEY_AUTO_SHUTDOWN, false) + set(value) = app.pref.edit { putBoolean(KEY_AUTO_SHUTDOWN, value) } + var shutdownTimeoutMillis: Long + get() = app.pref.getLong(KEY_SHUTDOWN_TIMEOUT, 0) + set(value) = app.pref.edit { putLong(KEY_SHUTDOWN_TIMEOUT, value) } var deviceAddress: MacAddressCompat? get() = try { MacAddressCompat(app.pref.getLong(KEY_DEVICE_ADDRESS, MacAddressCompat.ANY_ADDRESS.addr)).run { @@ -190,7 +198,6 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene private val p2pManager get() = Services.p2p!! private var channel: WifiP2pManager.Channel? = null private val binder = Binder() - @RequiresApi(28) private var timeoutMonitor: TetherTimeoutMonitor? = null private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> @@ -422,7 +429,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene * startService Step 3 */ private fun doStartLocked(group: WifiP2pGroup) { - if (Build.VERSION.SDK_INT >= 28) timeoutMonitor = TetherTimeoutMonitor(this, binder::shutdown) + if (isAutoShutdownEnabled) timeoutMonitor = TetherTimeoutMonitor(shutdownTimeoutMillis, coroutineContext) { + binder.shutdown() + } binder.group = group if (persistNextGroup) { networkName = group.networkName diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt index 63e54b21..34851b04 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt @@ -188,7 +188,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic val networkName = RepeaterService.networkName val passphrase = RepeaterService.passphrase if (networkName != null && passphrase != null) { - return SoftApConfigurationCompat.empty().apply { + return SoftApConfigurationCompat().apply { ssid = networkName securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used this.passphrase = passphrase @@ -199,7 +199,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic } } else binder?.let { binder -> val group = binder.group ?: binder.fetchPersistentGroup().let { binder.group } - if (group != null) return SoftApConfigurationCompat.empty().run { + if (group != null) return SoftApConfigurationCompat().run { ssid = group.networkName securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used band = SoftApConfigurationCompat.BAND_ANY diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt index 46eba248..34c32843 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/TetherTimeoutMonitor.kt @@ -1,27 +1,18 @@ 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.Build -import android.os.Handler -import android.os.Looper import android.provider.Settings import androidx.annotation.RequiresApi -import androidx.core.os.postDelayed import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.wifi.WifiApManager -import be.mygod.vpnhotspot.util.broadcastReceiver -import be.mygod.vpnhotspot.util.ensureReceiverUnregistered -import be.mygod.vpnhotspot.util.intentFilter +import kotlinx.coroutines.* import timber.log.Timber +import kotlin.coroutines.CoroutineContext -@RequiresApi(28) -class TetherTimeoutMonitor(private val context: Context, private val onTimeout: () -> Unit, - private val handler: Handler = Handler(Looper.getMainLooper())) : - ContentObserver(handler), AutoCloseable { +class TetherTimeoutMonitor(private val timeout: Long = 0, + private val context: CoroutineContext = Dispatchers.Main.immediate, + private val onTimeout: () -> Unit) : AutoCloseable { /** * config_wifi_framework_soft_ap_timeout_delay was introduced in Android 9. * @@ -37,18 +28,19 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout: /** * Minimum limit to use for timeout delay if the value from overlay setting is too small. */ - @RequiresApi(21) - const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes + private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes @Deprecated("Use SoftApConfigurationCompat instead") + @get:RequiresApi(28) + @set:RequiresApi(28) var enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1 set(value) { // TODO: WRITE_SECURE_SETTINGS permission check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0)) } - val timeout by lazy { - val delay = try { + val defaultTimeout: Int get() { + val delay = if (Build.VERSION.SDK_INT >= 28) try { if (Build.VERSION.SDK_INT < 30) Resources.getSystem().run { getInteger(getIdentifier("config_wifi_framework_soft_ap_timeout_delay", "integer", "android")) } else app.packageManager.getResourcesForApplication(WifiApManager.resolvedActivity.activityInfo @@ -59,57 +51,27 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout: } catch (e: Resources.NotFoundException) { Timber.w(e) MIN_SOFT_AP_TIMEOUT_DELAY_MS - } - if (Build.VERSION.SDK_INT < 30 && delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) { + } else MIN_SOFT_AP_TIMEOUT_DELAY_MS + return if (Build.VERSION.SDK_INT < 30 && 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) - } + private var timeoutJob: Job? = null override fun close() { - context.ensureReceiverUnregistered(receiver) - context.contentResolver.unregisterContentObserver(this) + timeoutJob?.cancel() + timeoutJob = null } 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 - } + if (!noClient) close() else if (timeoutJob == null) timeoutJob = GlobalScope.launch(context) { + delay(if (timeout == 0L) defaultTimeout.toLong() else timeout) + onTimeout() } } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt index b91b20bf..62f3b61c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApConfigurationCompat.kt @@ -222,9 +222,6 @@ data class SoftApConfigurationCompat( } }, if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, - if (Build.VERSION.SDK_INT >= 28) { - TetherTimeoutMonitor.timeout.toLong() - } else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong(), underlying = this) @RequiresApi(30) @@ -244,12 +241,6 @@ data class SoftApConfigurationCompat( getBlockedClientList(this) as List, getAllowedClientList(this) as List, this) - - fun empty() = SoftApConfigurationCompat( - isAutoShutdownEnabled = if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, - shutdownTimeoutMillis = if (Build.VERSION.SDK_INT >= 28) { - TetherTimeoutMonitor.timeout.toLong() - } else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong()) } @Suppress("DEPRECATION") diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt index 6d4e817b..02bbd476 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialogFragment.kt @@ -25,6 +25,7 @@ import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.databinding.DialogWifiApBinding import be.mygod.vpnhotspot.net.MacAddressCompat +import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.util.QRCodeDialog import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.showAllowingStateLoss @@ -109,6 +110,10 @@ class WifiApDialogFragment : AlertDialogFragment + if (text.isNullOrEmpty()) 0 else text.toString().toLong() + } val bandOption = dialogView.band.selectedItem as BandOption band = bandOption.band channel = bandOption.channel @@ -141,6 +146,8 @@ class WifiApDialogFragment : AlertDialogFragment= 23 || arg.p2pMode) dialogView.band.apply { bandOptions = mutableListOf().apply { if (arg.p2pMode) { @@ -172,6 +179,8 @@ class WifiApDialogFragment : AlertDialogFragment= 23 || arg.p2pMode) { dialogView.band.setSelection(if (base.channel in 1..165) { bandOptions.indexOfFirst { it.channel == base.channel } @@ -208,6 +217,15 @@ class WifiApDialogFragment : AlertDialogFragment + if (text.isNullOrEmpty()) null else try { + text.toString().toLong() + null + } catch (e: NumberFormatException) { + e.readableMessage + } + } + dialogView.timeoutWrapper.error = timeoutError dialogView.bssidWrapper.error = null val bssidValid = dialogView.bssid.length() == 0 || try { MacAddressCompat.fromString(dialogView.bssid.text.toString()) @@ -217,8 +235,8 @@ class WifiApDialogFragment : AlertDialogFragment + + + +