From 1bcb786382129ae9263684195b1875392b4e191a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 16:14:57 +0200 Subject: [PATCH] fix(android): request NEARBY_WIFI_DEVICES at runtime so mDNS discovery works on real devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NsdManager service discovery needs NEARBY_WIFI_DEVICES on Android 13+. The app DECLARED it but never REQUESTED it, so on a real device the permission stayed denied and discoverServices silently found nothing — no prompt, no hosts. (It only worked on the emulator because the permission was granted via `adb pm grant`.) Request it (mirroring the mic RECORD_AUDIO flow) when the connect screen appears, and start/restart discovery once granted; on API < 33 discovery starts immediately (the permission doesn't apply there). The advertised hosts the Apple clients already see will then appear here too. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../kotlin/io/unom/punktfunk/MainActivity.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt index 160e5b3..4dbf8c4 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/MainActivity.kt @@ -1,6 +1,7 @@ package io.unom.punktfunk import android.Manifest +import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -13,8 +14,10 @@ import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown @@ -244,11 +247,22 @@ private fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { // mDNS discovery scoped to this screen; NsdManager callbacks arrive on the main thread, so the // onChange callback can set Compose state directly. (Emulator SLIRP drops multicast → empty.) + // NsdManager discovery needs NEARBY_WIFI_DEVICES on Android 13+ (a runtime permission) — without + // it discoverServices silently finds nothing. Request it once, then (re)start discovery on grant. val discovery = remember { HostDiscovery(context) } var discovered by remember { mutableStateOf>(emptyList()) } - DisposableEffect(Unit) { + var nearbyGranted by remember { mutableStateOf(hasNearbyPermission(context)) } + val nearbyLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { granted -> nearbyGranted = granted } + LaunchedEffect(Unit) { + if (!nearbyGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + nearbyLauncher.launch(Manifest.permission.NEARBY_WIFI_DEVICES) + } + } + DisposableEffect(nearbyGranted) { discovery.onChange = { discovered = it } - discovery.start() + if (nearbyGranted) discovery.start() onDispose { discovery.onChange = null discovery.stop() @@ -576,6 +590,12 @@ private fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) { } } +/** NsdManager discovery needs NEARBY_WIFI_DEVICES on API 33+; below that it doesn't apply. */ +private fun hasNearbyPermission(context: Context): Boolean = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES) == + PackageManager.PERMISSION_GRANTED + /** Left-aligned section header above each block of the connect screen. */ @Composable private fun SectionLabel(text: String) {