Merge pull request #799 from permissionlesstech/codex/fix-hotspot-local-network-permission

Fix Android 17 local network permission for hotspot sharing
This commit is contained in:
callebtc 2026-07-28 17:32:46 +02:00 committed by GitHub
commit bd29257b73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 43 deletions

View File

@ -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

View File

@ -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.")
}
}

View File

@ -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<String> {
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<String> {
return requiredForSdk().filter { permission ->
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
}
}
}

View File

@ -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<String>(), HotspotPermissions.requiredForSdk(28))
}
}