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..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,6 +222,20 @@ class AndroidNetworkAdapter @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override suspend fun isHotspotEnabled(): KMResult { + // 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) { + 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 return withContext(Dispatchers.IO) { systemBridgeConnManager.run { systemBridge -> systemBridge.isTetheringEnabled } @@ -230,14 +244,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. */