Merge "[bc25] Handle transitions involving overlays." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 671b012..a6d5c1c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -48,17 +48,13 @@
import com.android.systemui.scene.shared.model.Scenes
object QuickSettings {
- private val SCENES =
- setOf(
- Scenes.QuickSettings,
- Scenes.Shade,
- )
+ private val SCENES = setOf(Scenes.QuickSettings, Scenes.Shade)
object Elements {
val Content =
MovableElementKey(
"QuickSettingsContent",
- contentPicker = MovableElementContentPicker(SCENES)
+ contentPicker = MovableElementContentPicker(SCENES),
)
val QuickQuickSettings = ElementKey("QuickQuickSettings")
val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
@@ -87,7 +83,7 @@
private fun SceneScope.stateForQuickSettingsContent(
isSplitShade: Boolean,
- squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default }
+ squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
): QSSceneAdapter.State {
return when (val transitionState = layoutState.transitionState) {
is TransitionState.Idle -> {
@@ -122,7 +118,7 @@
}
}
is TransitionState.Transition.OverlayTransition ->
- TODO("b/359173565: Handle overlay transitions")
+ error("Bad transition for QuickSettings scene: overlays not supported")
}
}
@@ -172,7 +168,7 @@
val height = heightProvider().coerceAtLeast(0)
layout(placeable.width, height) { placeable.placeRelative(0, 0) }
- }
+ },
) {
content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
}
@@ -225,7 +221,7 @@
it.addView(view)
}
},
- onRelease = { it.removeAllViews() }
+ onRelease = { it.removeAllViews() },
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
index edaa3d3..bf97afe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
@@ -18,9 +18,12 @@
package com.android.systemui.scene.domain.interactor
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -29,8 +32,10 @@
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.sceneDataSource
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
@@ -63,10 +68,11 @@
private val sceneDataSource =
kosmos.sceneDataSource.apply { changeScene(toScene = Scenes.Lockscreen) }
- private val underTest = kosmos.sceneContainerOcclusionInteractor
+ private val underTest by lazy { kosmos.sceneContainerOcclusionInteractor }
@Test
- fun invisibleDueToOcclusion() =
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun invisibleDueToOcclusion_dualShadeDisabled() =
testScope.runTest {
val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
@@ -126,6 +132,68 @@
.isFalse()
}
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun invisibleDueToOcclusion_dualShadeEnabled() =
+ testScope.runTest {
+ val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
+ val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
+
+ // Assert that we have the desired preconditions:
+ assertThat(keyguardState).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+ assertThat(sceneInteractor.transitionState.value)
+ .isEqualTo(ObservableTransitionState.Idle(Scenes.Lockscreen))
+ assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse()
+
+ // Actual testing starts here:
+ showOccludingActivity()
+ assertWithMessage("Should become occluded when occluding activity is shown")
+ .that(invisibleDueToOcclusion)
+ .isTrue()
+
+ transitionIntoAod {
+ assertWithMessage("Should become unoccluded when transitioning into AOD")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+ }
+ assertWithMessage("Should stay unoccluded when in AOD")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+
+ transitionOutOfAod {
+ assertWithMessage("Should remain unoccluded while transitioning away from AOD")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+ }
+ assertWithMessage("Should become occluded now that no longer in AOD")
+ .that(invisibleDueToOcclusion)
+ .isTrue()
+
+ expandDualShade {
+ assertWithMessage("Should become unoccluded once shade begins to expand")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+ }
+ assertWithMessage("Should be unoccluded when shade is fully expanded")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+
+ collapseDualShade {
+ assertWithMessage("Should remain unoccluded while shade is collapsing")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+ }
+ assertWithMessage("Should become occluded now that shade is fully collapsed")
+ .that(invisibleDueToOcclusion)
+ .isTrue()
+
+ hideOccludingActivity()
+ assertWithMessage("Should become unoccluded once the occluding activity is hidden")
+ .that(invisibleDueToOcclusion)
+ .isFalse()
+ }
+
/** Simulates the appearance of a show-when-locked `Activity` in the foreground. */
private fun TestScope.showOccludingActivity() {
keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
@@ -138,15 +206,13 @@
/** Simulates the disappearance of a show-when-locked `Activity` from the foreground. */
private fun TestScope.hideOccludingActivity() {
keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
- showWhenLockedActivityOnTop = false,
+ showWhenLockedActivityOnTop = false
)
runCurrent()
}
/** Simulates a user-driven gradual expansion of the shade. */
- private fun TestScope.expandShade(
- assertMidTransition: () -> Unit = {},
- ) {
+ private fun TestScope.expandShade(assertMidTransition: () -> Unit = {}) {
val progress = MutableStateFlow(0f)
mutableTransitionState.value =
ObservableTransitionState.Transition(
@@ -170,10 +236,41 @@
runCurrent()
}
+ /** Simulates a user-driven gradual expansion of the dual shade (notifications). */
+ private fun TestScope.expandDualShade(assertMidTransition: () -> Unit = {}) {
+ val progress = MutableStateFlow(0f)
+ mutableTransitionState.value =
+ ShowOrHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ fromContent = sceneDataSource.currentScene.value,
+ toContent = Overlays.NotificationsShade,
+ currentScene = sceneDataSource.currentScene.value,
+ currentOverlays = sceneDataSource.currentOverlays,
+ progress = progress,
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ runCurrent()
+
+ progress.value = 0.5f
+ runCurrent()
+ assertMidTransition()
+
+ progress.value = 1f
+ runCurrent()
+
+ mutableTransitionState.value =
+ ObservableTransitionState.Idle(
+ sceneDataSource.currentScene.value,
+ setOf(Overlays.NotificationsShade),
+ )
+ runCurrent()
+ }
+
/** Simulates a user-driven gradual collapse of the shade. */
- private fun TestScope.collapseShade(
- assertMidTransition: () -> Unit = {},
- ) {
+ private fun TestScope.collapseShade(assertMidTransition: () -> Unit = {}) {
val progress = MutableStateFlow(0f)
mutableTransitionState.value =
ObservableTransitionState.Transition(
@@ -197,10 +294,37 @@
runCurrent()
}
+ /** Simulates a user-driven gradual collapse of the dual shade (notifications). */
+ private fun TestScope.collapseDualShade(assertMidTransition: () -> Unit = {}) {
+ val progress = MutableStateFlow(0f)
+ mutableTransitionState.value =
+ ShowOrHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ fromContent = Overlays.NotificationsShade,
+ toContent = Scenes.Lockscreen,
+ currentScene = Scenes.Lockscreen,
+ currentOverlays = flowOf(setOf(Overlays.NotificationsShade)),
+ progress = progress,
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ runCurrent()
+
+ progress.value = 0.5f
+ runCurrent()
+ assertMidTransition()
+
+ progress.value = 1f
+ runCurrent()
+
+ mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+ runCurrent()
+ }
+
/** Simulates a transition into AOD. */
- private suspend fun TestScope.transitionIntoAod(
- assertMidTransition: () -> Unit = {},
- ) {
+ private suspend fun TestScope.transitionIntoAod(assertMidTransition: () -> Unit = {}) {
val currentKeyguardState = keyguardTransitionInteractor.getCurrentState()
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -235,9 +359,7 @@
}
/** Simulates a transition away from AOD. */
- private suspend fun TestScope.transitionOutOfAod(
- assertMidTransition: () -> Unit = {},
- ) {
+ private suspend fun TestScope.transitionOutOfAod(assertMidTransition: () -> Unit = {}) {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
from = KeyguardState.AOD,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 4a7d8b0..7fe3d8d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -21,6 +21,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -38,6 +39,7 @@
import com.android.systemui.scene.overlayKeys
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.sceneKeys
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -256,7 +258,7 @@
}
@Test
- fun transitioningTo() =
+ fun transitioningTo_sceneChange() =
testScope.runTest {
val transitionState =
MutableStateFlow<ObservableTransitionState>(
@@ -293,6 +295,51 @@
}
@Test
+ fun transitioningTo_overlayChange() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(underTest.currentScene.value)
+ )
+ underTest.setTransitionState(transitionState)
+
+ val transitionTo by collectLastValue(underTest.transitioningTo)
+ assertThat(transitionTo).isNull()
+
+ underTest.showOverlay(Overlays.NotificationsShade, "reason")
+ assertThat(transitionTo).isNull()
+
+ val progress = MutableStateFlow(0f)
+ transitionState.value =
+ ShowOrHideOverlay(
+ overlay = Overlays.NotificationsShade,
+ fromContent = underTest.currentScene.value,
+ toContent = Overlays.NotificationsShade,
+ currentScene = underTest.currentScene.value,
+ currentOverlays = underTest.currentOverlays,
+ progress = progress,
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+ progress.value = 0.5f
+ assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+ progress.value = 1f
+ assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+ transitionState.value =
+ ObservableTransitionState.Idle(
+ currentScene = underTest.currentScene.value,
+ currentOverlays = setOf(Overlays.NotificationsShade),
+ )
+ assertThat(transitionTo).isNull()
+ }
+
+ @Test
fun isTransitionUserInputOngoing_idle_false() =
testScope.runTest {
val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
index 851b7b9..ba559b5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
@@ -21,6 +21,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay
+import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -32,6 +34,7 @@
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.testKosmos
@@ -125,6 +128,63 @@
@Test
@EnableSceneContainer
+ fun legacyPanelExpansion_dualShade_whenIdle_whenLocked() =
+ testScope.runTest {
+ underTest = kosmos.panelExpansionInteractorImpl
+ val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)
+
+ changeScene(Scenes.Lockscreen) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ changeScene(Scenes.Bouncer) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ showOverlay(Overlays.NotificationsShade) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ showOverlay(Overlays.QuickSettingsShade) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun legacyPanelExpansion_dualShade_whenIdle_whenUnlocked() =
+ testScope.runTest {
+ underTest = kosmos.panelExpansionInteractorImpl
+ val unlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+
+ assertThat(unlockStatus)
+ .isEqualTo(DeviceUnlockStatus(true, DeviceUnlockSource.Fingerprint))
+
+ val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)
+
+ changeScene(Scenes.Gone) { assertThat(panelExpansion).isEqualTo(0f) }
+ assertThat(panelExpansion).isEqualTo(0f)
+
+ showOverlay(Overlays.NotificationsShade) { progress ->
+ assertThat(panelExpansion).isEqualTo(progress)
+ }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ showOverlay(Overlays.QuickSettingsShade) {
+ // Notification shade is already expanded, so moving to QS shade should also be 1f.
+ assertThat(panelExpansion).isEqualTo(1f)
+ }
+ assertThat(panelExpansion).isEqualTo(1f)
+
+ changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
+ assertThat(panelExpansion).isEqualTo(1f)
+ }
+
+ @Test
+ @EnableSceneContainer
fun shouldHideStatusBarIconsWhenExpanded_goneScene() =
testScope.runTest {
underTest = kosmos.panelExpansionInteractorImpl
@@ -193,4 +253,72 @@
assertThat(currentScene).isEqualTo(toScene)
}
+
+ private fun TestScope.showOverlay(
+ toOverlay: OverlayKey,
+ assertDuringProgress: ((progress: Float) -> Unit) = {},
+ ) {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ val progressFlow = MutableStateFlow(0f)
+ transitionState.value =
+ if (checkNotNull(currentOverlays).isEmpty()) {
+ ShowOrHideOverlay(
+ overlay = toOverlay,
+ fromContent = checkNotNull(currentScene),
+ toContent = toOverlay,
+ currentScene = checkNotNull(currentScene),
+ currentOverlays = flowOf(emptySet()),
+ progress = progressFlow,
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ } else {
+ ObservableTransitionState.Transition.ReplaceOverlay(
+ fromOverlay = checkNotNull(currentOverlays).first(),
+ toOverlay = toOverlay,
+ currentScene = checkNotNull(currentScene),
+ currentOverlays = flowOf(emptySet()),
+ progress = progressFlow,
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ previewProgress = flowOf(0f),
+ isInPreviewStage = flowOf(false),
+ )
+ }
+ runCurrent()
+ assertDuringProgress(progressFlow.value)
+
+ progressFlow.value = 0.2f
+ runCurrent()
+ assertDuringProgress(progressFlow.value)
+
+ progressFlow.value = 0.6f
+ runCurrent()
+ assertDuringProgress(progressFlow.value)
+
+ progressFlow.value = 1f
+ runCurrent()
+ assertDuringProgress(progressFlow.value)
+
+ transitionState.value =
+ ObservableTransitionState.Idle(
+ currentScene = checkNotNull(currentScene),
+ currentOverlays = setOf(toOverlay),
+ )
+ if (checkNotNull(currentOverlays).isEmpty()) {
+ fakeSceneDataSource.showOverlay(toOverlay)
+ } else {
+ fakeSceneDataSource.replaceOverlay(
+ from = checkNotNull(currentOverlays).first(),
+ to = toOverlay,
+ )
+ }
+ runCurrent()
+ assertDuringProgress(progressFlow.value)
+
+ assertThat(currentOverlays).containsExactly(toOverlay)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index 429b47b..667827a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -16,13 +16,14 @@
package com.android.systemui.scene.domain.interactor
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -117,28 +118,30 @@
private val ObservableTransitionState.canBeOccluded: Boolean
get() =
when (this) {
- is ObservableTransitionState.Idle -> currentScene.canBeOccluded
- is ObservableTransitionState.Transition.ChangeScene ->
- fromScene.canBeOccluded && toScene.canBeOccluded
- is ObservableTransitionState.Transition.ReplaceOverlay,
- is ObservableTransitionState.Transition.ShowOrHideOverlay ->
- TODO("b/359173565: Handle overlay transitions")
+ is ObservableTransitionState.Idle ->
+ currentOverlays.all { it.canBeOccluded } && currentScene.canBeOccluded
+ is ObservableTransitionState.Transition ->
+ // TODO(b/356596436): Should also verify currentOverlays.isEmpty(), but
+ // currentOverlays is a Flow and we need a state.
+ fromContent.canBeOccluded && toContent.canBeOccluded
}
/**
- * Whether the scene can be occluded by a "show when locked" activity. Some scenes should, on
+ * Whether the content can be occluded by a "show when locked" activity. Some content should, on
* principle not be occlude-able because they render as if they are expanding on top of the
* occluding activity.
*/
- private val SceneKey.canBeOccluded: Boolean
+ private val ContentKey.canBeOccluded: Boolean
get() =
when (this) {
+ Overlays.NotificationsShade -> false
+ Overlays.QuickSettingsShade -> false
Scenes.Bouncer -> false
Scenes.Communal -> true
Scenes.Gone -> true
Scenes.Lockscreen -> true
Scenes.QuickSettings -> false
Scenes.Shade -> false
- else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
+ else -> error("ContentKey \"$this\" doesn't have a mapping for canBeOccluded!")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 0d24adc..f20e5a5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -120,21 +120,18 @@
)
/**
- * The key of the scene that the UI is currently transitioning to or `null` if there is no
+ * The key of the content that the UI is currently transitioning to or `null` if there is no
* active transition at the moment.
*
* This is a convenience wrapper around [transitionState], meant for flow-challenged consumers
* like Java code.
*/
- val transitioningTo: StateFlow<SceneKey?> =
+ val transitioningTo: StateFlow<ContentKey?> =
transitionState
.map { state ->
when (state) {
is ObservableTransitionState.Idle -> null
- is ObservableTransitionState.Transition.ChangeScene -> state.toScene
- is ObservableTransitionState.Transition.ShowOrHideOverlay,
- is ObservableTransitionState.Transition.ReplaceOverlay ->
- TODO("b/359173565: Handle overlay transitions")
+ is ObservableTransitionState.Transition -> state.toContent
}
}
.stateIn(
@@ -160,15 +157,14 @@
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = false
+ initialValue = false,
)
/** Whether the scene container is visible. */
val isVisible: StateFlow<Boolean> =
- combine(
- repository.isVisible,
- repository.isRemoteUserInputOngoing,
- ) { isVisible, isRemoteUserInteractionOngoing ->
+ combine(repository.isVisible, repository.isRemoteUserInputOngoing) {
+ isVisible,
+ isRemoteUserInteractionOngoing ->
isVisibleInternal(
raw = isVisible,
isRemoteUserInputOngoing = isRemoteUserInteractionOngoing,
@@ -177,7 +173,7 @@
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
- initialValue = isVisibleInternal()
+ initialValue = isVisibleInternal(),
)
/** Whether there's an ongoing remotely-initiated user interaction. */
@@ -259,10 +255,7 @@
* The change is instantaneous and not animated; it will be observable in the next frame and
* there will be no transition animation.
*/
- fun snapToScene(
- toScene: SceneKey,
- loggingReason: String,
- ) {
+ fun snapToScene(toScene: SceneKey, loggingReason: String) {
val currentSceneKey = currentScene.value
val resolvedScene =
sceneFamilyResolvers.get()[toScene]?.let { familyResolver ->
@@ -313,15 +306,9 @@
return
}
- logger.logOverlayChangeRequested(
- to = overlay,
- reason = loggingReason,
- )
+ logger.logOverlayChangeRequested(to = overlay, reason = loggingReason)
- repository.showOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ repository.showOverlay(overlay = overlay, transitionKey = transitionKey)
}
/**
@@ -345,15 +332,9 @@
return
}
- logger.logOverlayChangeRequested(
- from = overlay,
- reason = loggingReason,
- )
+ logger.logOverlayChangeRequested(from = overlay, reason = loggingReason)
- repository.hideOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ repository.hideOverlay(overlay = overlay, transitionKey = transitionKey)
}
/**
@@ -378,17 +359,9 @@
return
}
- logger.logOverlayChangeRequested(
- from = from,
- to = to,
- reason = loggingReason,
- )
+ logger.logOverlayChangeRequested(from = from, to = to, reason = loggingReason)
- repository.replaceOverlay(
- from = from,
- to = to,
- transitionKey = transitionKey,
- )
+ repository.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
}
/**
@@ -405,11 +378,7 @@
return
}
- logger.logVisibilityChange(
- from = wasVisible,
- to = isVisible,
- reason = loggingReason,
- )
+ logger.logVisibilityChange(from = wasVisible, to = isVisible, reason = loggingReason)
return repository.setVisible(isVisible)
}
@@ -491,11 +460,7 @@
* @param loggingReason The reason why the transition is requested, for logging purposes
* @return `true` if the scene change is valid; `false` if it shouldn't happen
*/
- private fun validateSceneChange(
- from: SceneKey,
- to: SceneKey,
- loggingReason: String,
- ): Boolean {
+ private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
if (to !in repository.allContentKeys) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index c451704..af1f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -52,7 +52,7 @@
private val sceneInteractor: SceneInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
- private val shadeInteractor: ShadeInteractor,
+ shadeInteractor: ShadeInteractor,
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
@@ -212,9 +212,10 @@
)
}
}
+ // Overlay transitions don't use scene families, nothing to resolve.
is UserActionResult.ShowOverlay,
is UserActionResult.HideOverlay,
- is UserActionResult.ReplaceByOverlay -> TODO("b/353679003: Support overlays")
+ is UserActionResult.ReplaceByOverlay -> null
} ?: actionResult
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
index e276f88..cea521f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
@@ -18,10 +18,11 @@
package com.android.systemui.shade.domain.interactor
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.SysuiStatusBarStateController
import javax.inject.Inject
@@ -55,7 +56,9 @@
when (state) {
is ObservableTransitionState.Idle ->
flowOf(
- if (state.currentScene != Scenes.Gone) {
+ if (
+ state.currentScene != Scenes.Gone || state.currentOverlays.isNotEmpty()
+ ) {
// When resting on a non-Gone scene, the panel is fully expanded.
1f
} else {
@@ -64,10 +67,10 @@
0f
}
)
- is ObservableTransitionState.Transition.ChangeScene ->
+ is ObservableTransitionState.Transition ->
when {
- state.fromScene == Scenes.Gone ->
- if (state.toScene.isExpandable()) {
+ state.fromContent == Scenes.Gone ->
+ if (state.toContent.isExpandable()) {
// Moving from Gone to a scene that can animate-expand has a
// panel expansion that tracks with the transition.
state.progress
@@ -76,8 +79,8 @@
// immediately makes the panel fully expanded.
flowOf(1f)
}
- state.toScene == Scenes.Gone ->
- if (state.fromScene.isExpandable()) {
+ state.toContent == Scenes.Gone ->
+ if (state.fromContent.isExpandable()) {
// Moving to Gone from a scene that can animate-expand has a
// panel expansion that tracks with the transition.
state.progress.map { 1 - it }
@@ -88,9 +91,6 @@
}
else -> flowOf(1f)
}
- is ObservableTransitionState.Transition.ShowOrHideOverlay,
- is ObservableTransitionState.Transition.ReplaceOverlay ->
- TODO("b/359173565: Handle overlay transitions")
}
}
@@ -132,7 +132,13 @@
return sceneInteractor.currentScene.value == Scenes.Lockscreen
}
- private fun SceneKey.isExpandable(): Boolean {
- return this == Scenes.Shade || this == Scenes.QuickSettings
+ private fun ContentKey.isExpandable(): Boolean {
+ return when (this) {
+ Scenes.Shade,
+ Scenes.QuickSettings,
+ Overlays.NotificationsShade,
+ Overlays.QuickSettingsShade -> true
+ else -> false
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 3e42413..8d7007b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState.Idle
import com.android.compose.animation.scene.ObservableTransitionState.Transition
import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene
@@ -26,7 +27,7 @@
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
@@ -46,7 +47,6 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@@ -91,7 +91,7 @@
): Float {
return if (fullyExpandedDuringSceneChange(change)) {
1f
- } else if (change.isBetween({ it == Scenes.Gone }, { it in SceneFamilies.NotifShade })) {
+ } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) {
shadeExpansion
} else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
// during QS expansion, increase fraction at same rate as scrim alpha,
@@ -99,6 +99,22 @@
(qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN)
.coerceIn(0f, 1f)
} else {
+ // TODO(b/356596436): If notification shade overlay is open, we'll reach this point and
+ // the expansion fraction in that case should be `shadeExpansion`.
+ 0f
+ }
+ }
+
+ private fun expandFractionDuringOverlayTransition(
+ transition: Transition,
+ currentScene: SceneKey,
+ shadeExpansion: Float,
+ ): Float {
+ return if (currentScene == Scenes.Lockscreen) {
+ 1f
+ } else if (transition.isTransitioningFromOrTo(Overlays.NotificationsShade)) {
+ shadeExpansion
+ } else {
0f
}
}
@@ -114,18 +130,35 @@
shadeInteractor.shadeMode,
shadeInteractor.qsExpansion,
sceneInteractor.transitionState,
- sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
- ) { shadeExpansion, _, qsExpansion, transitionState, _ ->
+ ) { shadeExpansion, _, qsExpansion, transitionState ->
when (transitionState) {
- is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f
+ is Idle ->
+ if (
+ expandedInScene(transitionState.currentScene) ||
+ Overlays.NotificationsShade in transitionState.currentOverlays
+ ) {
+ 1f
+ } else {
+ 0f
+ }
is ChangeScene ->
expandFractionDuringSceneChange(
- transitionState,
- shadeExpansion,
- qsExpansion,
+ change = transitionState,
+ shadeExpansion = shadeExpansion,
+ qsExpansion = qsExpansion,
)
- is Transition.ShowOrHideOverlay,
- is Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions")
+ is Transition.ShowOrHideOverlay ->
+ expandFractionDuringOverlayTransition(
+ transition = transitionState,
+ currentScene = transitionState.currentScene,
+ shadeExpansion = shadeExpansion,
+ )
+ is Transition.ReplaceOverlay ->
+ expandFractionDuringOverlayTransition(
+ transition = transitionState,
+ currentScene = transitionState.currentScene,
+ shadeExpansion = shadeExpansion,
+ )
}
}
.distinctUntilChanged()
@@ -166,14 +199,14 @@
fun shadeScrimShape(
cornerRadius: Flow<Int>,
- viewLeftOffset: Flow<Int>
+ viewLeftOffset: Flow<Int>,
): Flow<ShadeScrimShape?> =
combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset ->
if (clipping == null) return@combine null
ShadeScrimShape(
bounds = clipping.bounds.minus(leftOffset = leftOffset),
topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0,
- bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0
+ bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0,
)
}
.dumpWhileCollecting("shadeScrimShape")
@@ -209,10 +242,10 @@
/** Whether the notification stack is scrollable or not. */
val isScrollable: Flow<Boolean> =
- sceneInteractor.currentScene
- .map {
- sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) ||
- it == Scenes.Lockscreen
+ combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) {
+ currentScene,
+ currentOverlays ->
+ currentScene.showsNotifications() || currentOverlays.any { it.showsNotifications() }
}
.dumpWhileCollecting("isScrollable")
@@ -242,13 +275,20 @@
}
}
+ private fun ContentKey.showsNotifications(): Boolean {
+ return when (this) {
+ Overlays.NotificationsShade,
+ Scenes.Lockscreen,
+ Scenes.Shade -> true
+ else -> false
+ }
+ }
+
@AssistedFactory
interface Factory {
fun create(): NotificationScrollViewModel
}
}
-private fun ChangeScene.isBetween(
- a: (SceneKey) -> Boolean,
- b: (SceneKey) -> Boolean,
-): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
+private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean =
+ (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 45aee5b..a658115 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -30,6 +30,7 @@
import android.view.WindowInsets;
import androidx.annotation.VisibleForTesting;
+
import com.android.compose.animation.scene.ObservableTransitionState;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.Dumpable;
@@ -69,7 +70,9 @@
private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
private boolean mIsStatusBarExpanded = false;
- private boolean mIsIdleOnGone = true;
+ // Whether the scene container has no UI to render, i.e. is in idle state on the Gone scene and
+ // without any overlays to display.
+ private boolean mIsSceneContainerUiEmpty = true;
private boolean mIsRemoteUserInteractionOngoing = false;
private boolean mShouldAdjustInsets = false;
private View mNotificationShadeWindowView;
@@ -134,7 +137,7 @@
if (SceneContainerFlag.isEnabled()) {
javaAdapter.alwaysCollectFlow(
sceneInteractor.get().getTransitionState(),
- this::onSceneChanged);
+ this::onSceneContainerTransition);
javaAdapter.alwaysCollectFlow(
sceneInteractor.get().isRemoteUserInteractionOngoing(),
this::onRemoteUserInteractionOngoingChanged);
@@ -172,11 +175,13 @@
}
}
- private void onSceneChanged(ObservableTransitionState transitionState) {
- boolean isIdleOnGone = transitionState.isIdle(Scenes.Gone);
- if (isIdleOnGone != mIsIdleOnGone) {
- mIsIdleOnGone = isIdleOnGone;
- if (!isIdleOnGone) {
+ private void onSceneContainerTransition(ObservableTransitionState transitionState) {
+ boolean isSceneContainerUiEmpty = transitionState.isIdle(Scenes.Gone)
+ && ((ObservableTransitionState.Idle) transitionState).getCurrentOverlays()
+ .isEmpty();
+ if (isSceneContainerUiEmpty != mIsSceneContainerUiEmpty) {
+ mIsSceneContainerUiEmpty = isSceneContainerUiEmpty;
+ if (!isSceneContainerUiEmpty) {
// make sure our state is sensible
mForceCollapsedUntilLayout = false;
}
@@ -296,7 +301,7 @@
// underneath.
return mIsStatusBarExpanded
|| (SceneContainerFlag.isEnabled()
- && (!mIsIdleOnGone || mIsRemoteUserInteractionOngoing))
+ && (!mIsSceneContainerUiEmpty || mIsRemoteUserInteractionOngoing))
|| mPrimaryBouncerInteractor.isShowing().getValue()
|| mAlternateBouncerInteractor.isVisibleState()
|| mUnlockedScreenOffAnimationController.isAnimationPlaying();