From 581320df0ce37e4bbb1728667835338a124dd23f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 19:48:12 +0200 Subject: [PATCH] fix(android): the stream stops fighting large displays and owns the cutout explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three Play Console pre-release findings for 0.22.3, resolved: - Orientation restriction: the in-stream SENSOR_LANDSCAPE lock is now applied on compact devices (sw < 600 dp) only. On tablets/foldables/desktop windows it is a large-display anti-pattern (Android 16+ ignores it there outright) and unnecessary — the aspect-ratio letterbox renders correctly in any orientation; the lock was always a phone-ergonomics choice. No manifest restriction existed, and resizeability stays unrestricted. - Edge-to-edge determinism: the stream window now sets LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS explicitly (and restores it on the way out) — SDK-35 enforcement makes that the immersive default, pre-15 devices letterboxed the notch as a dead bar; being explicit gives both the same, correct behaviour. The stream's own letterbox is black, so the cutout region can never show anything wrong. - Deprecated edge-to-edge APIs: audited — no in-app use of setStatusBarColor/setNavigationBarColor/systemUiVisibility/translucent flags or theme attrs; enableEdgeToEdge (androidx.activity 1.13.0) is already in place with explicit SystemBarStyle. What the scanner sees are androidx's own API-level-guarded compat branches, which Google documents as ignorable. Co-Authored-By: Claude Fable 5 --- .../kotlin/io/unom/punktfunk/StreamScreen.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) 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 537d9b25..d1c6a1e6 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 @@ -235,13 +235,39 @@ fun StreamScreen(session: ActiveSession, onDisconnect: () -> Unit) { val priorSoftInput = window?.attributes?.softInputMode ?: WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + // Draw under the display cutout, explicitly. Android 15's SDK-35 edge-to-edge enforcement + // makes ALWAYS the immersive default, but pre-15 devices letterbox the notch as a dead + // black bar unless asked — and the stream's own letterbox is black anyway, so the cutout + // region can never show anything wrong. Captured + restored like the rest of the window + // state so the menus keep their platform-default behaviour. + val priorCutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window?.attributes?.layoutInDisplayCutoutMode + } else { + null + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window?.let { w -> + w.attributes = w.attributes.apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + } + } + } // Lock to landscape while streaming — the host streams a landscape desktop, so pin the device // there (either landscape direction is fine) and stop it rotating to portrait mid-session. The // activity declares configChanges=orientation, so this re-lays out the surface in place without // recreating the activity (no stream restart). On TV (fixed landscape) it's a harmless no-op. // The prior request is captured and restored on the way out. + // + // COMPACT devices only (sw < 600 dp): on tablets/foldables/desktop windows the lock is a + // large-display anti-pattern (Play flags it; Android 16+ ignores it there outright), and the + // stream doesn't need it — the aspect-ratio letterbox renders correctly in any orientation, + // the lock is purely a phone-ergonomics choice. + val compactDevice = context.resources.configuration.smallestScreenWidthDp < 600 val priorOrientation = activity?.requestedOrientation - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + if (compactDevice) { + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } activity?.streamHandle = handle // route hardware keys to this session // Multi-controller router: a stable wire pad index per connected controller, per-device axis // state, Arrival/Remove on hot-plug, and feedback routed back by pad index. Forwards every @@ -463,6 +489,11 @@ fun StreamScreen(session: ActiveSession, onDisconnect: () -> Unit) { } controller?.hide(WindowInsetsCompat.Type.ime()) // drop any keyboard left showing window?.setSoftInputMode(priorSoftInput) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && priorCutout != null) { + window?.let { w -> + w.attributes = w.attributes.apply { layoutInDisplayCutoutMode = priorCutout } + } + } controller?.show(WindowInsetsCompat.Type.systemBars()) window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (lowLatencyMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {