From 1d83e55e61b8fb2ff9df2a437f05820e1c8b91e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A6=81=E7=B3=96=E9=86=8B=E6=94=BE=E6=A4=92?= =?UTF-8?q?=E7=9B=90?= <62319450+Moriafly@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:57:24 +0800 Subject: [PATCH] Fix Sheet system bar visibility reads on legacy Android Convert native root insets with the owner View so AndroidX accounts for legacy system UI hide flags instead of treating reserved layout space as visible bars on API 23-29. Keep the existing Dialog host and public API. Add a real System UI regression for bottom and floating Sheets. Restrict the existing full owner-isolation test to API 30+, and document legacy focus propagation and the host-side focus workaround in code comments. Validation: debug and Android test APK assembly passed. Four targeted tests passed on Android 10 (API 29), and four passed on Android 12 (API 31). git diff --check passed. --- .../salt/sample/ui/SheetInteractionTest.kt | 31 +++++++++++++++++++ .../sheet/internal/SheetSystemBars.android.kt | 16 +++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/androidApp/src/androidTest/kotlin/com/moriafly/salt/sample/ui/SheetInteractionTest.kt b/androidApp/src/androidTest/kotlin/com/moriafly/salt/sample/ui/SheetInteractionTest.kt index 01ded4dd..3036e393 100644 --- a/androidApp/src/androidTest/kotlin/com/moriafly/salt/sample/ui/SheetInteractionTest.kt +++ b/androidApp/src/androidTest/kotlin/com/moriafly/salt/sample/ui/SheetInteractionTest.kt @@ -98,8 +98,39 @@ import java.util.concurrent.atomic.AtomicReference class SheetInteractionTest { private val device get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + @Test + fun sheetInheritsHiddenStatusBarOnLegacyAndroid() { + assumeTrue(Build.VERSION.SDK_INT < 30) + // On API 29 and below, inspect System UI itself so the assertion cannot repeat the + // compat insets misreading that this regression test is intended to catch. + val statusBar = By.res("com.android.systemui", "status_bar") + for (displayMode in listOf(SheetDisplayMode.Bottom, SheetDisplayMode.Floating)) { + val fixture = AndroidSheetFixture(displayMode = displayMode) + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + WindowCompat.getInsetsController(activity.window, activity.window.decorView) + .hide(WindowInsetsCompat.Type.statusBars()) + activity.setContent { fixture.Content() } + } + shown(By.text("Open sheet")) + assertTrue(device.wait(Until.gone(statusBar), 5_000)) + click(By.text("Open sheet")) + shown(By.text("Fixture sheet")) + assertTrue(device.wait(Until.gone(statusBar), 5_000)) + click(By.text("Inside")) + shown(By.text("Inside: 1")) + assertTrue(device.wait(Until.gone(statusBar), 5_000)) + click(By.text("Program close")) + assertTrue(device.wait(Until.gone(By.text("Fixture sheet")), 5_000)) + } + } + } + @Test fun sheetInheritsSystemBarsAndAllowsContentToManageThem() { + // On Android 10 (API 29) and earlier, dialog focus can clear the owner's hide flags. + // Opening inheritance is tested separately there; this full isolation check needs API 30+. + assumeTrue(Build.VERSION.SDK_INT >= 30) val statusBar = WindowInsetsCompat.Type.statusBars() val navigationBar = WindowInsetsCompat.Type.navigationBars() for (displayMode in listOf(SheetDisplayMode.Bottom, SheetDisplayMode.Floating)) { diff --git a/ui2/src/androidMain/kotlin/com/moriafly/salt/ui/sheet/internal/SheetSystemBars.android.kt b/ui2/src/androidMain/kotlin/com/moriafly/salt/ui/sheet/internal/SheetSystemBars.android.kt index a507e5f8..ae4ddff1 100644 --- a/ui2/src/androidMain/kotlin/com/moriafly/salt/ui/sheet/internal/SheetSystemBars.android.kt +++ b/ui2/src/androidMain/kotlin/com/moriafly/salt/ui/sheet/internal/SheetSystemBars.android.kt @@ -21,7 +21,6 @@ package com.moriafly.salt.ui.sheet.internal import android.view.View import android.view.Window -import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -29,8 +28,12 @@ import androidx.core.view.WindowInsetsCompat * Initializes one Sheet dialog window with the owner's opening system bar visibility. * * Call [inheritOwnerVisibility] from the host's SideEffect after a successful composition. - * Only the dialog's controller is changed; the owner keeps its own requests, so dismissal requires - * no restoration. Further visibility changes belong to the developer controlling the dialog. + * Only the dialog's controller is changed. On Android 10 (API 29) and earlier, the platform can + * propagate cleared system UI flags to other windows during focus changes, removing the owner's + * hide requests. Restoring the opening snapshot could overwrite newer host requests, so no + * restoration is done on dismissal. + * On these versions, immersive hosts should reapply their current visibility when focus returns. + * Further visibility changes belong to the developer controlling the dialog. */ internal class SheetSystemBars( window: Window, @@ -72,7 +75,12 @@ internal class SheetSystemBars( * uses the platform's default visible bars. */ fun readOwnerVisibility(ownerView: View): Visibility { - val insets = ViewCompat.getRootWindowInsets(ownerView) + // On Android 6–10 (API 23–29), root insets can retain layout space for hidden bars. + // ViewCompat.getRootWindowInsets() omits the window's system UI flags during conversion, + // so isVisible() can mistake that space for visible bars. Passing the View includes + // the window's hide flags in the calculation. API 30+ uses native visibility instead. + val insets = ownerView.rootWindowInsets + ?.let { WindowInsetsCompat.toWindowInsetsCompat(it, ownerView) } return Visibility( statusBarVisible = insets?.isVisible(WindowInsetsCompat.Type.statusBars()) != false, navigationBarVisible =