diff --git a/README.md b/README.md index ac7f4fcb..221c9d18 100644 --- a/README.md +++ b/README.md @@ -295,9 +295,17 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded * (on API 30) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Landroid/net/wifi/SoftApInfo;)V,sdk,system-api,test-api` * (since API 31) `Landroid/net/wifi/WifiManager$SoftApCallback;->onInfoChanged(Ljava/util/List;)V,sdk,system-api,test-api` * (since API 28) `Landroid/net/wifi/WifiManager$SoftApCallback;->onStateChanged(II)V,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_FAILURE_REASON:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_INTERFACE_NAME:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->EXTRA_WIFI_AP_STATE:Ljava/lang/String;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->SAP_CLIENT_BLOCK_REASON_CODE_*:I,sdk,system-api,test-api` * (since API 28) `Landroid/net/wifi/WifiManager;->SAP_START_FAILURE_*:I,sdk,system-api,test-api` -* (since API 28) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_CHANGED_ACTION:Ljava/lang/String;,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLED:I,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_DISABLING:I,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLED:I,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_ENABLING:I,sdk,system-api,test-api` +* (since API 26) `Landroid/net/wifi/WifiManager;->WIFI_AP_STATE_FAILED:I,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->getSoftApConfiguration()Landroid/net/wifi/SoftApConfiguration;,sdk,system-api,test-api` * (prior to API 30) `Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,sdk,system-api,test-api` * (since API 30) `Landroid/net/wifi/WifiManager;->isApMacRandomizationSupported()Z,sdk,system-api,test-api` diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index 7c80c71b..1055f2f8 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -6,16 +6,12 @@ import android.net.wifi.WifiManager import android.os.Build import androidx.annotation.RequiresApi import be.mygod.vpnhotspot.net.IpNeighbour -import be.mygod.vpnhotspot.net.TetherType -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.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.StickyEvent1 -import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.* import timber.log.Timber @@ -43,7 +39,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { null -> return // stopped "" -> WifiApManager.cancelLocalOnlyHotspotRequest() } - reservation?.close() ?: stopService() + reservation?.close() + stopService() } } @@ -56,24 +53,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { override val coroutineContext = dispatcher + Job() private var routingManager: RoutingManager? = null private var timeoutMonitor: TetherTimeoutMonitor? = null - private var receiverRegistered = false - private val receiver = broadcastReceiver { _, intent -> - val ifaces = (intent.localOnlyTetheredIfaces ?: return@broadcastReceiver).filter { - TetherType.ofInterface(it) != TetherType.WIFI_P2P - } - Timber.d("onTetherStateChangedLocked: $ifaces") - check(ifaces.size <= 1) - val iface = ifaces.singleOrNull() - binder.iface = iface - if (iface.isNullOrEmpty()) stopService() else launch { - val routingManager = routingManager - if (routingManager == null) { - this@LocalOnlyHotspotService.routingManager = RoutingManager.LocalOnly(this@LocalOnlyHotspotService, - iface).apply { start() } - IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService) - } else check(iface == routingManager.downstream) - } - } override val activeIfaces get() = binder.iface.let { if (it.isNullOrEmpty()) emptyList() else listOf(it) } override fun onBind(intent: Intent?) = binder @@ -87,20 +66,37 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { if (reservation == null) onFailed(-2) else { this@LocalOnlyHotspotService.reservation = reservation - if (!receiverRegistered) { - val configuration = binder.configuration!! - if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { - timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, - coroutineContext) { reservation.close() } + val configuration = binder.configuration!! + if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) { + timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, + coroutineContext) { reservation.close() } + } + // based on: https://android.googlesource.com/platform/packages/services/Car/+/df5cd06/service/src/com/android/car/CarProjectionService.java#160 + val sticky = registerReceiver(null, IntentFilter(WifiApManager.WIFI_AP_STATE_CHANGED_ACTION))!! + val apState = sticky.getIntExtra(WifiApManager.EXTRA_WIFI_AP_STATE, + WifiApManager.WIFI_AP_STATE_DISABLED) + val iface = sticky.getStringExtra(WifiApManager.EXTRA_WIFI_AP_INTERFACE_NAME) + if (apState != WifiApManager.WIFI_AP_STATE_ENABLED || iface.isNullOrEmpty()) { + if (apState == WifiApManager.WIFI_AP_STATE_FAILED) { + SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, + WifiApManager.failureReasonLookup(sticky.getIntExtra( + WifiApManager.EXTRA_WIFI_AP_FAILURE_REASON, 0)))).show() } - registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) - receiverRegistered = true + return stopService() + } + binder.iface = iface + launch { + check(routingManager == null) + routingManager = RoutingManager.LocalOnly( + this@LocalOnlyHotspotService, iface).apply { start() } + IpNeighbourMonitor.registerCallback(this@LocalOnlyHotspotService) } } } override fun onStopped() { Timber.d("LOHCallback.onStopped") + reservation?.close() reservation = null } @@ -152,14 +148,10 @@ 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 + IpNeighbourMonitor.unregisterCallback(this) + if (Build.VERSION.SDK_INT >= 28) { + timeoutMonitor?.close() + timeoutMonitor = null } launch { routingManager?.stop() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt index 86290ca2..2e039e13 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -160,11 +160,11 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), } override fun onStateChanged(state: Int, failureReason: Int) { - if (state < 10 || state > 14) { + if (state < WifiApManager.WIFI_AP_STATE_DISABLING || state > WifiApManager.WIFI_AP_STATE_FAILED) { Timber.w(Exception("Unknown state $state, $failureReason")) return } - this.failureReason = if (state == 14) failureReason else null // WIFI_AP_STATE_FAILED + this.failureReason = if (state == WifiApManager.WIFI_AP_STATE_FAILED) failureReason else null data.notifyChange() } override fun onNumClientsChanged(numClients: Int) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt index c5f0a250..56ac2303 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt @@ -134,7 +134,7 @@ object TetheringManager { * Requires MANAGE_USB permission, unfortunately. * * Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389 - * @see [startTethering]. + * @see startTethering */ @RequiresApi(24) const val TETHERING_USB = 1 @@ -142,14 +142,14 @@ object TetheringManager { * Bluetooth tethering type. * * Requires BLUETOOTH permission. - * @see [startTethering]. + * @see startTethering */ @RequiresApi(24) const val TETHERING_BLUETOOTH = 2 /** * Ncm local tethering type. * - * @see [startTethering] + * @see startTethering */ @RequiresApi(30) const val TETHERING_NCM = 4 @@ -157,7 +157,7 @@ object TetheringManager { * Ethernet tethering type. * * Requires MANAGE_USB permission, also. - * @see [startTethering] + * @see startTethering */ @RequiresApi(30) const val TETHERING_ETHERNET = 5 diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index b3ef3aa2..811f9d2c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -59,6 +59,87 @@ object WifiApManager { @get:RequiresApi(30) val isApMacRandomizationSupported get() = apMacRandomizationSupported(Services.wifi) as Boolean + /** + * Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled, + * enabling, disabling, or failed. + */ + const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED" + /** + * The lookup key for an int that indicates whether Wi-Fi AP is enabled, + * disabled, enabling, disabling, or failed. Retrieve it with [Intent.getIntExtra]. + * + * @see WIFI_AP_STATE_DISABLED + * @see WIFI_AP_STATE_DISABLING + * @see WIFI_AP_STATE_ENABLED + * @see WIFI_AP_STATE_ENABLING + * @see WIFI_AP_STATE_FAILED + */ + const val EXTRA_WIFI_AP_STATE = "wifi_state" + /** + * An extra containing the int error code for Soft AP start failure. + * Can be obtained from the [WIFI_AP_STATE_CHANGED_ACTION] using [Intent.getIntExtra]. + * This extra will only be attached if [EXTRA_WIFI_AP_STATE] is + * attached and is equal to [WIFI_AP_STATE_FAILED]. + * + * The error code will be one of: + * {@link #SAP_START_FAILURE_GENERAL}, + * {@link #SAP_START_FAILURE_NO_CHANNEL}, + * {@link #SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION} + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#210 + */ + @get:RequiresApi(23) + val EXTRA_WIFI_AP_FAILURE_REASON get() = + if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_FAILURE_REASON" else "wifi_ap_error_code" + /** + * The lookup key for a String extra that stores the interface name used for the Soft AP. + * This extra is included in the broadcast [WIFI_AP_STATE_CHANGED_ACTION]. + * Retrieve its value with [Intent.getStringExtra]. + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r1/wifi/java/android/net/wifi/WifiManager.java#413 + */ + @get:RequiresApi(26) + val EXTRA_WIFI_AP_INTERFACE_NAME get() = + if (Build.VERSION.SDK_INT >= 30) "android.net.wifi.extra.WIFI_AP_INTERFACE_NAME" else "wifi_ap_interface_name" + /** + * Wi-Fi AP is currently being disabled. The state will change to + * [WIFI_AP_STATE_DISABLED] if it finishes successfully. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_DISABLING = 10 + /** + * Wi-Fi AP is disabled. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiState() + */ + const val WIFI_AP_STATE_DISABLED = 11 + /** + * Wi-Fi AP is currently being enabled. The state will change to + * {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_ENABLING = 12 + /** + * Wi-Fi AP is enabled. + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_ENABLED = 13 + /** + * Wi-Fi AP is in a failed state. This state will occur when an error occurs during + * enabling or disabling + * + * @see WIFI_AP_STATE_CHANGED_ACTION + * @see #getWifiApState() + */ + const val WIFI_AP_STATE_FAILED = 14 + private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") } @Suppress("DEPRECATION") private val setWifiApConfiguration by lazy { @@ -88,7 +169,7 @@ object WifiApManager { /** * Called when soft AP state changes. * - * @param state the new AP state. One of {@link #WIFI_AP_STATE_DISABLED}, + * @param state the new AP state. One of [WIFI_AP_STATE_DISABLED], * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED}, * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED} * @param failureReason reason when in failed state. One of @@ -146,7 +227,7 @@ object WifiApManager { @RequiresApi(30) fun onBlockedClientConnecting(client: Parcelable, blockedReason: Int) { } } - @RequiresApi(28) + @RequiresApi(23) val failureReasonLookup = ConstantLookup("SAP_START_FAILURE_", "GENERAL", "NO_CHANNEL") @get:RequiresApi(30) val clientBlockLookup by lazy { ConstantLookup("SAP_CLIENT_") }