From f5b26f41d47336a23e3476b67b8e13259f7935bd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 22:25:59 +0000 Subject: [PATCH 1/2] #2060 fix: fix toggle hotspot action crashing on Android 11 Three-part fix: 1. Catch RuntimeException in SystemBridgeConnectionManager.run() to prevent the accessibility service from crashing if the SystemBridge throws an unexpected exception (e.g. UnsupportedOperationException when the TetheringConnector is unavailable on some Android 11 devices). 2. On Android 11 (API 30) use the public (deprecated) WifiManager isWifiApEnabled() to check hotspot state without requiring root. 3. On Android 11 with WRITE_SETTINGS permission, use the hidden ConnectivityManager.startTethering() / stopTethering() APIs via reflection to enable/disable the hotspot without Shizuku or root. WRITE_SETTINGS is now declared as a required permission for hotspot actions on Android 11 only. https://claude.ai/code/session_01AdijnJnpbsCqPDTYobwnLa --- .../keymapper/base/actions/ActionUtils.kt | 10 +++ .../manager/SystemBridgeConnectionManager.kt | 3 + .../system/network/AndroidNetworkAdapter.kt | 61 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index a5bfaa6454..9471fd1846 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -931,6 +931,16 @@ object ActionUtils { return listOf(Permission.FIND_NEARBY_DEVICES) } + ActionId.TOGGLE_HOTSPOT, + ActionId.ENABLE_HOTSPOT, + ActionId.DISABLE_HOTSPOT, + -> + // On Android 11, WRITE_SETTINGS is used as an alternative to the system bridge + // which requires Shizuku/root. On Android 12+, the system bridge is preferred. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + return listOf(Permission.WRITE_SETTINGS) + } + // Permissions handled based on setting type at runtime ActionId.MODIFY_SETTING -> return emptyList() diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 3aaa0cbb74..b78927a275 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -198,6 +198,9 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( } catch (e: RemoteException) { Timber.e(e, "RemoteException when running block with System Bridge") return KMError.Exception(e) + } catch (e: RuntimeException) { + Timber.e(e, "RuntimeException when running block with System Bridge") + return KMError.Exception(e) } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index f500d1d980..1c96c717ee 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -222,6 +222,13 @@ class AndroidNetworkAdapter @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override suspend fun isHotspotEnabled(): KMResult { + // On Android 11 try the public (deprecated) WifiManager API first since + // the SystemBridge TetheringConnector may not be available without root/Shizuku. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + @Suppress("DEPRECATION") + return Success(wifiManager.isWifiApEnabled) + } + // isTetheringEnabled is a blocking call that registers a callback return withContext(Dispatchers.IO) { systemBridgeConnManager.run { systemBridge -> systemBridge.isTetheringEnabled } @@ -230,14 +237,68 @@ class AndroidNetworkAdapter @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override fun enableHotspot(): KMResult<*> { + // On Android 11, use the hidden ConnectivityManager API via reflection when + // WRITE_SETTINGS is granted. This avoids needing Shizuku/root on Android 11. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && + Settings.System.canWrite(ctx) + ) { + return enableHotspotViaReflection() + } + return systemBridgeConnManager.run { bridge -> bridge.setTetheringEnabled(true) } } @RequiresApi(Build.VERSION_CODES.R) override fun disableHotspot(): KMResult<*> { + // On Android 11, use the hidden ConnectivityManager API via reflection when + // WRITE_SETTINGS is granted. This avoids needing Shizuku/root on Android 11. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && + Settings.System.canWrite(ctx) + ) { + return disableHotspotViaReflection() + } + return systemBridgeConnManager.run { bridge -> bridge.setTetheringEnabled(false) } } + /** + * Enables the WiFi hotspot using the hidden ConnectivityManager.startTethering() API + * via reflection. This works on Android 11 with only WRITE_SETTINGS permission. + */ + private fun enableHotspotViaReflection(): KMResult<*> { + return try { + // TETHERING_WIFI = 0 + val startTetheringMethod = connectivityManager.javaClass.methods + .firstOrNull { it.name == "startTethering" } + ?: return KMError.Exception(Exception("startTethering not found on this device")) + + startTetheringMethod.invoke(connectivityManager, 0, false, null, null) + Success(Unit) + } catch (e: Exception) { + Timber.e(e, "Failed to enable hotspot via reflection") + KMError.Exception(e) + } + } + + /** + * Disables the WiFi hotspot using the hidden ConnectivityManager.stopTethering() API + * via reflection. This works on Android 11 with only WRITE_SETTINGS permission. + */ + private fun disableHotspotViaReflection(): KMResult<*> { + return try { + // TETHERING_WIFI = 0 + val stopTetheringMethod = connectivityManager.javaClass.methods + .firstOrNull { it.name == "stopTethering" } + ?: return KMError.Exception(Exception("stopTethering not found on this device")) + + stopTetheringMethod.invoke(connectivityManager, 0) + Success(Unit) + } catch (e: Exception) { + Timber.e(e, "Failed to disable hotspot via reflection") + KMError.Exception(e) + } + } + /** * @return Null on Android 10+ because there is no API to do this anymore. */ From ae2b93cb687e7c8cb990b8a9ca40fd5d3329256b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 08:56:31 +0000 Subject: [PATCH 2/2] #2060 fix: use reflection for hotspot state check on Android 11 Replace direct call to deprecated WifiManager.isWifiApEnabled() with a reflection-based lookup in isHotspotEnabled(). The deprecated method may not be present in newer Android SDK compilation stubs (compileSdk 36), causing a build failure. Reflection bypasses the compile-time check while still working correctly at runtime on Android 11 devices. Fall back to the SystemBridge path if reflection fails. https://claude.ai/code/session_01AdijnJnpbsCqPDTYobwnLa --- .../system/network/AndroidNetworkAdapter.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 1c96c717ee..65c504c364 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -222,11 +222,18 @@ class AndroidNetworkAdapter @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override suspend fun isHotspotEnabled(): KMResult { - // On Android 11 try the public (deprecated) WifiManager API first since - // the SystemBridge TetheringConnector may not be available without root/Shizuku. + // On Android 11, use reflection to check soft AP state. The deprecated + // WifiManager.isWifiApEnabled() may not be available in newer SDK compilation stubs. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - @Suppress("DEPRECATION") - return Success(wifiManager.isWifiApEnabled) + return try { + val method = wifiManager.javaClass.getMethod("isWifiApEnabled") + Success(method.invoke(wifiManager) == true) + } catch (e: Exception) { + Timber.w(e, "Could not check hotspot state via reflection on Android 11") + withContext(Dispatchers.IO) { + systemBridgeConnManager.run { systemBridge -> systemBridge.isTetheringEnabled } + } + } } // isTetheringEnabled is a blocking call that registers a callback