From 42a8edf8edd51e6d1ce66f86909b5f3208628b3c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:06:12 +0200 Subject: [PATCH] fix: request local network access for hotspot sharing --- .../android/hotspot/HotspotActivity.kt | 38 +++++++----------- .../bitchat/android/hotspot/HotspotManager.kt | 31 +++++--------- .../android/hotspot/HotspotPermissions.kt | 35 ++++++++++++++++ .../android/hotspot/HotspotPermissionsTest.kt | 40 +++++++++++++++++++ 4 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/hotspot/HotspotPermissions.kt create mode 100644 app/src/test/kotlin/com/bitchat/android/hotspot/HotspotPermissionsTest.kt diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt index dbd3888b..bf9745d9 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt @@ -1,6 +1,5 @@ package com.bitchat.android.hotspot -import android.Manifest import android.content.Intent import android.graphics.Bitmap import android.os.Build @@ -39,9 +38,7 @@ import com.bitchat.android.ui.theme.BitchatFontFamily import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.util.UniversalApkManager import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale +import com.google.accompanist.permissions.rememberMultiplePermissionsState import java.io.File /** @@ -158,18 +155,10 @@ fun HotspotScreen( @OptIn(ExperimentalPermissionsApi::class) @Composable fun IntroScreen(onStartHotspot: () -> Unit) { - // Determine which permission to request based on Android version - val requiredPermission = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> Manifest.permission.NEARBY_WIFI_DEVICES - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Manifest.permission.ACCESS_FINE_LOCATION - else -> null // No runtime permission needed on Android < 10 - } - - val permissionState = requiredPermission?.let { - rememberPermissionState(it) { granted -> - if (granted) { - onStartHotspot() - } + val requiredPermissions = remember { HotspotPermissions.requiredForSdk() } + val permissionState = rememberMultiplePermissionsState(requiredPermissions) { results -> + if (requiredPermissions.all { results[it] == true }) { + onStartHotspot() } } @@ -219,7 +208,7 @@ fun IntroScreen(onStartHotspot: () -> Unit) { } // Permission rationale (if needed) - if (permissionState != null && !permissionState.status.isGranted && permissionState.status.shouldShowRationale) { + if (!permissionState.allPermissionsGranted && permissionState.shouldShowRationale) { Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors( @@ -237,10 +226,13 @@ fun IntroScreen(onStartHotspot: () -> Unit) { color = MaterialTheme.colorScheme.onPrimaryContainer ) Text( - text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - "BitChat needs nearby devices permission to create a Wi-Fi hotspot for sharing the app offline." - } else { - "BitChat needs location permission to create a Wi-Fi hotspot. This is required by Android for Wi-Fi scanning, but no location data is collected." + text = when { + Build.VERSION.SDK_INT >= HotspotPermissions.ANDROID_17_API_LEVEL -> + "BitChat needs nearby devices and local network access to create a Wi-Fi hotspot and serve the app to connected devices." + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> + "BitChat needs nearby devices permission to create a Wi-Fi hotspot for sharing the app offline." + else -> + "BitChat needs location permission to create a Wi-Fi hotspot. This is required by Android for Wi-Fi scanning, but no location data is collected." }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) @@ -278,12 +270,12 @@ fun IntroScreen(onStartHotspot: () -> Unit) { Button( onClick = { // Check permission before starting hotspot - if (permissionState == null || permissionState.status.isGranted) { + if (permissionState.allPermissionsGranted) { // No permission needed or already granted onStartHotspot() } else { // Request permission (auto-start handled by onPermissionResult callback) - permissionState.launchPermissionRequest() + permissionState.launchMultiplePermissionRequest() } }, modifier = Modifier diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotManager.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotManager.kt index bf2f3a93..3a22b6b1 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotManager.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotManager.kt @@ -6,7 +6,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.pm.PackageManager import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pManager @@ -16,7 +15,6 @@ import android.os.Handler import android.os.Looper import android.os.PowerManager import android.util.Log -import androidx.core.content.ContextCompat import java.net.NetworkInterface import java.security.SecureRandom import kotlin.random.Random @@ -100,12 +98,15 @@ class HotspotManager(private val context: Context) { return } - val missingPermission = requiredRuntimePermission()?.takeUnless { - ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } - if (missingPermission != null) { - Log.w(TAG, "Cannot start hotspot without $missingPermission") - callback.onError("Nearby Wi-Fi permission is required to start the hotspot") + val missingPermissions = HotspotPermissions.missingFrom(context) + if (missingPermissions.isNotEmpty()) { + Log.w(TAG, "Cannot start hotspot; missing required permissions: $missingPermissions") + val message = if (Manifest.permission.ACCESS_LOCAL_NETWORK in missingPermissions) { + "Local network permission is required to share the app over the hotspot" + } else { + "Nearby Wi-Fi permission is required to start the hotspot" + } + callback.onError(message) return } @@ -248,7 +249,7 @@ class HotspotManager(private val context: Context) { } } catch (e: SecurityException) { Log.e(TAG, "Wi-Fi permission was revoked while creating the group", e) - failStartup("Nearby Wi-Fi permission was revoked. Grant it and try again.") + failStartup("A required Wi-Fi or local network permission was revoked. Grant it and try again.") } } @@ -369,17 +370,7 @@ class HotspotManager(private val context: Context) { } } catch (e: SecurityException) { Log.e(TAG, "Wi-Fi permission was revoked while reading group info", e) - failStartup("Nearby Wi-Fi permission was revoked. Grant it and try again.") - } - } - - private fun requiredRuntimePermission(): String? { - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> - Manifest.permission.NEARBY_WIFI_DEVICES - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> - Manifest.permission.ACCESS_FINE_LOCATION - else -> null + failStartup("A required Wi-Fi or local network permission was revoked. Grant it and try again.") } } diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotPermissions.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotPermissions.kt new file mode 100644 index 00000000..122ea8c2 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotPermissions.kt @@ -0,0 +1,35 @@ +package com.bitchat.android.hotspot + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat + +internal object HotspotPermissions { + const val ANDROID_17_API_LEVEL = 37 + + @SuppressLint("InlinedApi") + fun requiredForSdk(sdkInt: Int = Build.VERSION.SDK_INT): List { + return when { + sdkInt >= ANDROID_17_API_LEVEL -> listOf( + Manifest.permission.NEARBY_WIFI_DEVICES, + Manifest.permission.ACCESS_LOCAL_NETWORK + ) + sdkInt >= Build.VERSION_CODES.TIRAMISU -> listOf( + Manifest.permission.NEARBY_WIFI_DEVICES + ) + sdkInt >= Build.VERSION_CODES.Q -> listOf( + Manifest.permission.ACCESS_FINE_LOCATION + ) + else -> emptyList() + } + } + + fun missingFrom(context: Context): List { + return requiredForSdk().filter { permission -> + ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED + } + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/hotspot/HotspotPermissionsTest.kt b/app/src/test/kotlin/com/bitchat/android/hotspot/HotspotPermissionsTest.kt new file mode 100644 index 00000000..a7914fe0 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/hotspot/HotspotPermissionsTest.kt @@ -0,0 +1,40 @@ +package com.bitchat.android.hotspot + +import android.Manifest +import org.junit.Assert.assertEquals +import org.junit.Test + +class HotspotPermissionsTest { + + @Test + fun `Android 17 requires nearby Wi-Fi and local network permissions`() { + assertEquals( + listOf( + Manifest.permission.NEARBY_WIFI_DEVICES, + Manifest.permission.ACCESS_LOCAL_NETWORK + ), + HotspotPermissions.requiredForSdk(37) + ) + } + + @Test + fun `Android 13 through 16 require nearby Wi-Fi permission`() { + val expected = listOf(Manifest.permission.NEARBY_WIFI_DEVICES) + + assertEquals(expected, HotspotPermissions.requiredForSdk(33)) + assertEquals(expected, HotspotPermissions.requiredForSdk(36)) + } + + @Test + fun `Android 10 through 12 require fine location permission`() { + val expected = listOf(Manifest.permission.ACCESS_FINE_LOCATION) + + assertEquals(expected, HotspotPermissions.requiredForSdk(29)) + assertEquals(expected, HotspotPermissions.requiredForSdk(32)) + } + + @Test + fun `Android 9 and earlier require no hotspot runtime permission`() { + assertEquals(emptyList(), HotspotPermissions.requiredForSdk(28)) + } +}