diff options
author | 2024-03-18 17:26:00 -0700 | |
---|---|---|
committer | 2024-03-27 16:53:47 -0700 | |
commit | 75584ff6da827aa0ed13d1da2b3b3d44d12da977 (patch) | |
tree | 42399259654f468f5f8d9a461fe771df73c48000 | |
parent | c3d7b6367070147a53837e90cf412b1f7c8d4bf7 (diff) |
[flexiglass] Occlusion support.
Adds support for occlusion in Flexiglass.
System UI's window is rendered on top of all other windows. This is by
design because the keyguard (lockscreen, bouncer) must be rendered on
top of any other activity window to "lock" the device and also because
the shade (notification or QS shade) must be expandable above any other
activity window.
Sometimes, however, we can show an activity "on top" of the locked
keyguard; this is a developer-facing API that allows them to mark their
activities as "shown on lockscreen" in their app's AndroidManifest.xml
file (if they have the right permission).
When an activity like that is shown, we simulate "occlusion" of the
System UI window by hiding our window-view, which shows that activity.
That said, when the user expands the shade or QS, our window needs to be
shown again.
Similarly, in AOD, our window should again be shown.
This CL adds that logic to Flexiglass.
The majority of the logic is in the new
SceneContainerOcclusionInteractor which reuses the
KeyguardOcclusionInteractor (with one minor change). The
visibility-setting logic in our SceneContainerStartable is also altered
to take that new state into account.
Still missing:
1. Transition to bouncer when the "camera roll" button is clicked inside
the camera app (this requires compliance with a signal from
KeyguardService, most likely)
2. Inability to navigate home or back while the occluding activity is
showing
Bug: 308001302
Test: manually verified that showing an occluding activity (used the
camera lockscreen shortcut) hides system UI
Test: manually verified that expanding the shade or QS scenes when
occluded, shows those scenes and that collapsing them re-occludes system
UI
Test: manually verified that AOD also shows system UI, even when the
camera activity was on top
Test: Launched Maps Navigation while unlocked, turned the screen off, and then back on (ended on occluding activity, tests waking to occluded)
Test: Set a timer, turned the screen off, waited for the timer activity to open over lockscreen, dismissed it (tests manually interacting with the device to end an occluding activity)
Test: Received a phone call and didn't answer it (this tests the occluding activity ending itself)
Test: added unit and integration tests
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Change-Id: If004ecae201d8193e598030a89a0dc589bd6b4d4
11 files changed, 525 insertions, 47 deletions
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt index 8a77ed2130a9..056a401a2644 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt @@ -30,17 +30,24 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.keyguard.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -49,22 +56,33 @@ import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue -import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardOcclusionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = kosmos.keyguardOcclusionInteractor - private val powerInteractor = kosmos.powerInteractor - private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + private lateinit var underTest: KeyguardOcclusionInteractor + private lateinit var powerInteractor: PowerInteractor + private lateinit var transitionRepository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + powerInteractor = kosmos.powerInteractor + transitionRepository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.keyguardOcclusionInteractor + } @Test - fun testTransitionFromPowerGesture_whileGoingToSleep_isTrue() = + fun transitionFromPowerGesture_whileGoingToSleep_isTrue() = testScope.runTest { powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( @@ -81,7 +99,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { } @Test - fun testTransitionFromPowerGesture_whileAsleep_isTrue() = + fun transitionFromPowerGesture_whileAsleep_isTrue() = testScope.runTest { powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( @@ -97,7 +115,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { } @Test - fun testTransitionFromPowerGesture_whileWaking_isFalse() = + fun transitionFromPowerGesture_whileWaking_isFalse() = testScope.runTest { powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( @@ -119,7 +137,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { } @Test - fun testTransitionFromPowerGesture_whileAwake_isFalse() = + fun transitionFromPowerGesture_whileAwake_isFalse() = testScope.runTest { powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( @@ -140,7 +158,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { } @Test - fun testShowWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() = + fun showWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() = testScope.runTest { val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture) powerInteractor.setAsleepForTest() @@ -187,7 +205,7 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { } @Test - fun testShowWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() = + fun showWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() = testScope.runTest { val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture) powerInteractor.setAwakeForTest() @@ -221,4 +239,23 @@ class KeyguardOcclusionInteractorTest : SysuiTestCase() { false, ) } + + @Test + @EnableSceneContainer + fun occludingActivityWillDismissKeyguard() = + testScope.runTest { + val occludingActivityWillDismissKeyguard by + collectLastValue(underTest.occludingActivityWillDismissKeyguard) + assertThat(occludingActivityWillDismissKeyguard).isFalse() + + // Unlock device: + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + runCurrent() + assertThat(occludingActivityWillDismissKeyguard).isTrue() + + // Re-lock device: + kosmos.fakeDeviceEntryRepository.setUnlocked(false) + runCurrent() + assertThat(occludingActivityWillDismissKeyguard).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 307778bf7e0f..05c81496c3dc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -64,6 +64,7 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.qs.footerActionsController import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags @@ -285,6 +286,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, centralSurfaces = mock(), headsUpInteractor = kosmos.headsUpNotificationInteractor, + occlusionInteractor = kosmos.sceneContainerOcclusionInteractor, ) startable.start() 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 new file mode 100644 index 000000000000..c3366ad21e2a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +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.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +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.Scenes +import com.android.systemui.scene.shared.model.sceneDataSource +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneContainerOcclusionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val keyguardOcclusionInteractor = kosmos.keyguardOcclusionInteractor + private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor + private val mutableTransitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) + private val sceneInteractor = + kosmos.sceneInteractor.apply { setTransitionState(mutableTransitionState) } + private val sceneDataSource = + kosmos.sceneDataSource.apply { changeScene(toScene = Scenes.Lockscreen) } + + private val underTest = kosmos.sceneContainerOcclusionInteractor + + @Test + fun invisibleDueToOcclusion() = + 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() + + expandShade { + assertWithMessage("Should become unoccluded once shade begins to expand") + .that(invisibleDueToOcclusion) + .isFalse() + } + assertWithMessage("Should be unoccluded when shade is fully expanded") + .that(invisibleDueToOcclusion) + .isFalse() + + collapseShade { + 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( + showWhenLockedActivityOnTop = true, + taskInfo = mock(), + ) + runCurrent() + } + + /** Simulates the disappearance of a show-when-locked `Activity` from the foreground. */ + private fun TestScope.hideOccludingActivity() { + keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( + showWhenLockedActivityOnTop = false, + ) + runCurrent() + } + + /** Simulates a user-driven gradual expansion of the shade. */ + private fun TestScope.expandShade( + assertMidTransition: () -> Unit = {}, + ) { + val progress = MutableStateFlow(0f) + mutableTransitionState.value = + ObservableTransitionState.Transition( + fromScene = sceneDataSource.currentScene.value, + toScene = Scenes.Shade, + progress = progress, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + runCurrent() + + progress.value = 0.5f + runCurrent() + assertMidTransition() + + progress.value = 1f + runCurrent() + + mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Shade) + runCurrent() + } + + /** Simulates a user-driven gradual collapse of the shade. */ + private fun TestScope.collapseShade( + assertMidTransition: () -> Unit = {}, + ) { + val progress = MutableStateFlow(0f) + mutableTransitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Shade, + toScene = Scenes.Lockscreen, + progress = progress, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + 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 = {}, + ) { + val currentKeyguardState = keyguardTransitionInteractor.getCurrentState() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = currentKeyguardState, + to = KeyguardState.AOD, + value = 0f, + transitionState = TransitionState.STARTED, + ) + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = currentKeyguardState, + to = KeyguardState.AOD, + value = 0.5f, + transitionState = TransitionState.RUNNING, + ) + ) + runCurrent() + assertMidTransition() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = currentKeyguardState, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED, + ) + ) + runCurrent() + } + + /** Simulates a transition away from AOD. */ + private suspend fun TestScope.transitionOutOfAod( + assertMidTransition: () -> Unit = {}, + ) { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 0f, + transitionState = TransitionState.STARTED, + ) + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 0.5f, + transitionState = TransitionState.RUNNING, + ) + ) + runCurrent() + assertMidTransition() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 1f, + transitionState = TransitionState.FINISHED, + ) + ) + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 594543b88b17..605e5c067eab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -46,11 +46,13 @@ import com.android.systemui.model.SysUiState import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory +import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository @@ -128,6 +130,7 @@ class SceneContainerStartableTest : SysuiTestCase() { deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor, centralSurfaces = centralSurfaces, headsUpInteractor = kosmos.headsUpNotificationInteractor, + occlusionInteractor = kosmos.sceneContainerOcclusionInteractor, ) } @@ -204,6 +207,28 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun hydrateVisibility_basedOnOcclusion() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + prepareState( + isDeviceUnlocked = true, + initialSceneKey = Scenes.Lockscreen, + ) + + underTest.start() + assertThat(isVisible).isTrue() + + kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( + true, + mock() + ) + assertThat(isVisible).isFalse() + + kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(false) + assertThat(isVisible).isTrue() + } + + @Test fun startsInLockscreenScene() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt index 9aa2202b4100..03ed5675a77a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt @@ -19,13 +19,17 @@ package com.android.systemui.keyguard.domain.interactor import android.app.ActivityManager.RunningTaskInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.keyguard.data.repository.KeyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.sample +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -45,11 +49,12 @@ import kotlinx.coroutines.flow.stateIn class KeyguardOcclusionInteractor @Inject constructor( - @Application scope: CoroutineScope, - val repository: KeyguardOcclusionRepository, - val powerInteractor: PowerInteractor, - val transitionInteractor: KeyguardTransitionInteractor, - val keyguardInteractor: KeyguardInteractor, + @Application applicationScope: CoroutineScope, + private val repository: KeyguardOcclusionRepository, + private val powerInteractor: PowerInteractor, + private val transitionInteractor: KeyguardTransitionInteractor, + keyguardInteractor: KeyguardInteractor, + deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, ) { val showWhenLockedActivityInfo = repository.showWhenLockedActivityInfo.asStateFlow() @@ -94,14 +99,19 @@ constructor( // Emit false once that activity goes away. isShowWhenLockedActivityOnTop.filter { !it }.map { false } ) - .stateIn(scope, SharingStarted.Eagerly, false) + .stateIn(applicationScope, SharingStarted.Eagerly, false) /** * Whether launching an occluding activity will automatically dismiss keyguard. This happens if * the keyguard is dismissable. */ - val occludingActivityWillDismissKeyguard = - keyguardInteractor.isKeyguardDismissible.stateIn(scope, SharingStarted.Eagerly, false) + val occludingActivityWillDismissKeyguard: StateFlow<Boolean> = + if (SceneContainerFlag.isEnabled) { + deviceUnlockedInteractor.get().isDeviceUnlocked + } else { + keyguardInteractor.isKeyguardDismissible + } + .stateIn(scope = applicationScope, SharingStarted.Eagerly, false) /** * Called to let System UI know that WM says a SHOW_WHEN_LOCKED activity is on top (or no longer 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 new file mode 100644 index 000000000000..a82391643e11 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.domain.interactor + +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.dagger.SysUISingleton +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.Scenes +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** Encapsulates logic regarding the occlusion state of the scene container. */ +@SysUISingleton +class SceneContainerOcclusionInteractor +@Inject +constructor( + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + sceneInteractor: SceneInteractor, + keyguardTransitionInteractor: KeyguardTransitionInteractor, +) { + /** + * Whether the scene container should become invisible due to "occlusion" by an in-foreground + * "show when locked" activity. + */ + val invisibleDueToOcclusion: Flow<Boolean> = + combine( + keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, + sceneInteractor.transitionState, + keyguardTransitionInteractor + .transitionValue(KeyguardState.AOD) + .onStart { emit(0f) } + .map { it > 0 } + .distinctUntilChanged(), + ) { isOccludingActivityShown, sceneTransitionState, isAodFullyOrPartiallyShown -> + isOccludingActivityShown && + !isAodFullyOrPartiallyShown && + sceneTransitionState.canBeOccluded + } + .distinctUntilChanged() + + private val ObservableTransitionState.canBeOccluded: Boolean + get() = + when (this) { + is ObservableTransitionState.Idle -> scene.canBeOccluded + is ObservableTransitionState.Transition -> + fromScene.canBeOccluded && toScene.canBeOccluded + } + + /** + * Whether the scene can be occluded by a "show when locked" activity. Some scenes 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 + get() = + when (this) { + Scenes.Bouncer -> true + 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!") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 42b41f88e155..0e4049bbd21e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -40,6 +40,7 @@ import com.android.systemui.model.updateFlags import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.logger.SceneLogger @@ -94,6 +95,7 @@ constructor( private val deviceProvisioningInteractor: DeviceProvisioningInteractor, private val centralSurfaces: CentralSurfaces, private val headsUpInteractor: HeadsUpNotificationInteractor, + private val occlusionInteractor: SceneContainerOcclusionInteractor, ) : CoreStartable { override fun start() { @@ -130,32 +132,36 @@ constructor( .distinctUntilChanged() .flatMapLatest { isAllowedToBeVisible -> if (isAllowedToBeVisible) { - sceneInteractor.transitionState - .mapNotNull { state -> - when (state) { - is ObservableTransitionState.Idle -> { - if (state.scene != Scenes.Gone) { - true to "scene is not Gone" - } else { - false to "scene is Gone" + combine( + sceneInteractor.transitionState.mapNotNull { state -> + when (state) { + is ObservableTransitionState.Idle -> { + if (state.scene != Scenes.Gone) { + true to "scene is not Gone" + } else { + false to "scene is Gone" + } } - } - is ObservableTransitionState.Transition -> { - if (state.fromScene == Scenes.Gone) { - true to "scene transitioning away from Gone" - } else { - null + is ObservableTransitionState.Transition -> { + if (state.fromScene == Scenes.Gone) { + true to "scene transitioning away from Gone" + } else { + null + } } } - } - } - .combine(headsUpInteractor.isHeadsUpOrAnimatingAway) { + }, + headsUpInteractor.isHeadsUpOrAnimatingAway, + occlusionInteractor.invisibleDueToOcclusion, + ) { visibilityForTransitionState, - isHeadsUpOrAnimatingAway -> - if (isHeadsUpOrAnimatingAway) { - true to "showing a HUN" - } else { - visibilityForTransitionState + isHeadsUpOrAnimatingAway, + invisibleDueToOcclusion, + -> + when { + isHeadsUpOrAnimatingAway -> true to "showing a HUN" + invisibleDueToOcclusion -> false to "invisible due to occlusion" + else -> visibilityForTransitionState } } .distinctUntilChanged() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 7bef01a7a5ce..3926f92b44e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -73,13 +74,17 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { } private val testScope = kosmos.testScope - private val underTest = kosmos.fromAodTransitionInteractor + private lateinit var underTest: FromAodTransitionInteractor - private val powerInteractor = kosmos.powerInteractor - private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + private lateinit var powerInteractor: PowerInteractor + private lateinit var transitionRepository: FakeKeyguardTransitionRepository @Before fun setup() { + powerInteractor = kosmos.powerInteractor + transitionRepository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.fromAodTransitionInteractor + underTest.start() // Transition to AOD and set the power interactor asleep. diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 258dbf3efbae..cded2a4ec27c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -77,13 +78,17 @@ class FromDozingTransitionInteractorTest : SysuiTestCase() { } private val testScope = kosmos.testScope - private val underTest = kosmos.fromDozingTransitionInteractor + private lateinit var underTest: FromDozingTransitionInteractor - private val powerInteractor = kosmos.powerInteractor - private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + private lateinit var powerInteractor: PowerInteractor + private lateinit var transitionRepository: FakeKeyguardTransitionRepository @Before fun setup() { + powerInteractor = kosmos.powerInteractor + transitionRepository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.fromDozingTransitionInteractor + underTest.start() // Transition to DOZING and set the power interactor asleep. diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt new file mode 100644 index 000000000000..b32960a73178 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.domain.interactor + +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +val Kosmos.sceneContainerOcclusionInteractor by Fixture { + SceneContainerOcclusionInteractor( + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + sceneInteractor = sceneInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt index d79374021968..a90a9ffc8a33 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.domain.interactor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -27,10 +28,11 @@ import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.keyguardOcclusionInteractor by Kosmos.Fixture { KeyguardOcclusionInteractor( - scope = testScope.backgroundScope, + applicationScope = testScope.backgroundScope, repository = keyguardOcclusionRepository, powerInteractor = powerInteractor, transitionInteractor = keyguardTransitionInteractor, keyguardInteractor = keyguardInteractor, + deviceUnlockedInteractor = { deviceUnlockedInteractor }, ) } |