From 08ab2b6beeb7faf039cf7d46811e06cac5ed8fcc Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 7 Jul 2026 08:07:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(android):=20declare=20WAKE=5FLOCK=20?= =?UTF-8?q?=E2=80=94=20the=20stream's=20Wi-Fi=20locks=20never=20actually?= =?UTF-8?q?=20engaged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WifiLock.acquire() enforces the WAKE_LOCK permission, which the manifest never declared — every acquisition since the first Wi-Fi lock shipped threw SecurityException, silently swallowed by a bare runCatching. The phone's own accounting proved it (dumpsys wifi: high_perf/low_latency active_time_ms = 0 across weeks of streams): every on-device session ran with Wi-Fi power save fully active, whatever the code intended. Verified live after the fix: both locks registered in WifiLockManager, mPowerSaveDisableRequests=2, ping RTT to the streaming phone 3.8 ms avg. A failed acquire now logs loudly — this class of failure must never be invisible again. Co-Authored-By: Claude Fable 5 --- clients/android/app/src/main/AndroidManifest.xml | 6 ++++++ .../main/kotlin/io/unom/punktfunk/StreamScreen.kt | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml index 932c3d6c..1bc78c58 100644 --- a/clients/android/app/src/main/AndroidManifest.xml +++ b/clients/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,12 @@ reception needs it (also an OEM Wi-Fi power-save hedge). --> + + diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt index 8dc852f3..fd91c1c8 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/StreamScreen.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.net.wifi.WifiManager import android.os.Build +import android.util.Log import android.view.SurfaceHolder import android.view.SurfaceView import android.view.WindowManager @@ -126,8 +127,10 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) { // - FULL_HIGH_PERF covers older releases — it is deprecated AND a documented no-op on recent // Android, which is exactly why it can't be the only lock (a lesson learned: holding just // HIGH_PERF left power save fully active on Android 13+). - // Needs no permission beyond ACCESS_WIFI_STATE (declared). Non-reference-counted: one explicit - // acquire/release each. + // acquire() ENFORCES the WAKE_LOCK permission (manifest) — and a failed acquire MUST be loud: + // a silent runCatching hid the missing permission for weeks (dumpsys wifi showed + // low_latency_active_time_ms=0 across every "locked" stream). Non-reference-counted: one + // explicit acquire/release each. val wifiLocks = remember(handle) { val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager ?: return@remember emptyList() @@ -144,7 +147,11 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) { DisposableEffect(handle) { window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - wifiLocks.forEach { runCatching { it.acquire() } } + wifiLocks.forEach { lock -> + runCatching { lock.acquire() }.onFailure { e -> + Log.w("punktfunk", "WifiLock acquire failed — power save stays ON: $lock", e) + } + } // HDMI Auto Low-Latency Mode: ask the display to drop its post-processing (game mode) — // the biggest panel-side latency win on the TV boxes. No-op where ALLM isn't supported. API // 30+. Part of the experimental low-latency stack.