diff options
10 files changed, 444 insertions, 131 deletions
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 671b0128b621..a6d5c1cef81f 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.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS 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 @@ object QuickSettings { 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 @@ private fun SceneScope.stateForQuickSettingsContent( } } is TransitionState.Transition.OverlayTransition -> - TODO("b/359173565: Handle overlay transitions") + error("Bad transition for QuickSettings scene: overlays not supported") } } @@ -172,7 +168,7 @@ fun SceneScope.QuickSettings( val height = heightProvider().coerceAtLeast(0) layout(placeable.width, height) { placeable.placeRelative(0, 0) } - } + }, ) { content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) } } @@ -225,7 +221,7 @@ private fun QuickSettingsContent( 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 edaa3d373807..bf97afed92f4 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.KeyguardState 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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { 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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { .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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { /** 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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { 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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { 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 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { } /** 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 4a7d8b0f8287..7fe3d8d08afa 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 @@ package com.android.systemui.scene.domain.interactor 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.domain.resolver.homeSceneFamilyResolver 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 @@ class SceneInteractorTest : SysuiTestCase() { } @Test - fun transitioningTo() = + fun transitioningTo_sceneChange() = testScope.runTest { val transitionState = MutableStateFlow<ObservableTransitionState>( @@ -293,6 +295,51 @@ class SceneInteractorTest : SysuiTestCase() { } @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 851b7b986527..ba559b59c92e 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 @@ package com.android.systemui.shade.domain.interactor 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.data.repository.fakeDeviceEntryFingerprintA 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 @@ class PanelExpansionInteractorImplTest : SysuiTestCase() { @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 @@ class PanelExpansionInteractorImplTest : SysuiTestCase() { 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 429b47bcfba1..667827ac4724 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 @@ constructor( 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 0d24adc30799..f20e5a54f6ed 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 @@ constructor( ) /** - * 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 @@ constructor( .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 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = isVisibleInternal() + initialValue = isVisibleInternal(), ) /** Whether there's an ongoing remotely-initiated user interaction. */ @@ -259,10 +255,7 @@ constructor( * 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( * @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 c451704aa0f8..af1f5a716961 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 @@ constructor( 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 @@ constructor( ) } } + // 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 e276f8807df7..cea521f094be 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( // 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 @@ constructor( } else -> flowOf(1f) } - is ObservableTransitionState.Transition.ShowOrHideOverlay, - is ObservableTransitionState.Transition.ReplaceOverlay -> - TODO("b/359173565: Handle overlay transitions") } } @@ -132,7 +132,13 @@ constructor( 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 3e42413932f4..8d7007b2fba4 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.keyguard.domain.interactor.KeyguardInteractor 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.StateFlow 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 @@ constructor( ): 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 @@ constructor( (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 @@ constructor( 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 -> + expandFractionDuringOverlayTransition( + transition = transitionState, + currentScene = transitionState.currentScene, + shadeExpansion = shadeExpansion, + ) + is Transition.ReplaceOverlay -> + expandFractionDuringOverlayTransition( + transition = transitionState, + currentScene = transitionState.currentScene, + shadeExpansion = shadeExpansion, ) - is Transition.ShowOrHideOverlay, - is Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions") } } .distinctUntilChanged() @@ -166,14 +199,14 @@ constructor( 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 @@ constructor( /** 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 @@ constructor( } } + 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 45aee5b8c22f..a6581159d33e 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.ViewTreeObserver.OnComputeInternalInsetsListener; 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 @@ public final class StatusBarTouchableRegionManager implements Dumpable { 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 @@ public final class StatusBarTouchableRegionManager implements Dumpable { if (SceneContainerFlag.isEnabled()) { javaAdapter.alwaysCollectFlow( sceneInteractor.get().getTransitionState(), - this::onSceneChanged); + this::onSceneContainerTransition); javaAdapter.alwaysCollectFlow( sceneInteractor.get().isRemoteUserInteractionOngoing(), this::onRemoteUserInteractionOngoingChanged); @@ -172,11 +175,13 @@ public final class StatusBarTouchableRegionManager implements Dumpable { } } - 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 @@ public final class StatusBarTouchableRegionManager implements Dumpable { // underneath. return mIsStatusBarExpanded || (SceneContainerFlag.isEnabled() - && (!mIsIdleOnGone || mIsRemoteUserInteractionOngoing)) + && (!mIsSceneContainerUiEmpty || mIsRemoteUserInteractionOngoing)) || mPrimaryBouncerInteractor.isShowing().getValue() || mAlternateBouncerInteractor.isVisibleState() || mUnlockedScreenOffAnimationController.isAnimationPlaying(); |