fix(android): the stream stops fighting large displays and owns the cutout explicitly

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 23:44:09 +02:00
co-authored by Claude Fable 5
parent 87b6fa8813
commit 581320df0c
@@ -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) {