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 =