diff options
8 files changed, 131 insertions, 11 deletions
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 fff0a316cbf4..667f516317be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -615,6 +615,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun TestScope.emulatePendingTransitionProgress( expectedVisible: Boolean = true, ) { + val isVisible by collectLastValue(sceneContainerViewModel.isVisible) assertWithMessage("The FakeSceneDataSource has to be paused for this to do anything.") .that(fakeSceneDataSource.isPaused) .isTrue() @@ -651,7 +652,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { runCurrent() assertWithMessage("Visibility mismatch after scene transition from $from to $to!") - .that(sceneContainerViewModel.isVisible.value) + .that(isVisible) .isEqualTo(expectedVisible) assertThat(sceneContainerViewModel.currentScene.value).isEqualTo(to) 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 dd3eb6845789..db94c39e1cb1 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.scene.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -31,6 +33,7 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent @@ -275,4 +278,18 @@ class SceneInteractorTest : SysuiTestCase() { underTest.setVisible(true, "reason") assertThat(isVisible).isTrue() } + + @Test + fun isVisible_duringRemoteUserInteraction_forcedVisible() = + testScope.runTest { + underTest.setVisible(false, "reason") + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isFalse() + underTest.onRemoteUserInteractionStarted("reason") + assertThat(isVisible).isTrue() + + underTest.onUserInteractionFinished() + + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index ffbdafe338e7..27ae8b60009c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -14,10 +14,9 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.scene.ui.viewmodel +import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,9 +34,9 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -50,7 +49,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope by lazy { kosmos.testScope } - private val interactor by lazy { kosmos.sceneInteractor } + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource = kosmos.fakeSceneDataSource private val sceneContainerConfig = kosmos.sceneContainerConfig private val falsingManager = kosmos.fakeFalsingManager @@ -62,7 +61,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { kosmos.fakeSceneContainerFlags.enabled = true underTest = SceneContainerViewModel( - sceneInteractor = interactor, + sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, ) @@ -74,10 +73,10 @@ class SceneContainerViewModelTest : SysuiTestCase() { val isVisible by collectLastValue(underTest.isVisible) assertThat(isVisible).isTrue() - interactor.setVisible(false, "reason") + sceneInteractor.setVisible(false, "reason") assertThat(isVisible).isFalse() - interactor.setVisible(true, "reason") + sceneInteractor.setVisible(true, "reason") assertThat(isVisible).isTrue() } @@ -199,4 +198,20 @@ class SceneContainerViewModelTest : SysuiTestCase() { underTest.onMotionEvent(mock()) assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } + + @Test + fun remoteUserInteraction_keepsContainerVisible() = + testScope.runTest { + sceneInteractor.setVisible(false, "reason") + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isFalse() + sceneInteractor.onRemoteUserInteractionStarted("reason") + assertThat(isVisible).isTrue() + + underTest.onMotionEvent( + mock { whenever(actionMasked).thenReturn(MotionEvent.ACTION_UP) } + ) + + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 000f3c09d84c..5f8b5ddc9de6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -221,7 +221,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis // If scene framework is enabled, set the scene container window to // visible and let the touch "slip" into that window. if (mSceneContainerFlags.isEnabled()) { - mSceneInteractor.get().setVisible(true, "swipe down on launcher"); + mSceneInteractor.get().onRemoteUserInteractionStarted("launcher swipe"); } else { mShadeViewControllerLazy.get().startInputFocusTransfer(); } diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index a3021946713f..e60dff183148 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -49,6 +49,13 @@ constructor( private val _isVisible = MutableStateFlow(true) val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() + /** + * Whether there's an ongoing remotely-initiated user interaction. + * + * For more information see the logic in `SceneInteractor` that mutates this. + */ + val isRemoteUserInteractionOngoing = MutableStateFlow(false) + private val defaultTransitionState = ObservableTransitionState.Idle(config.initialSceneKey) private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null) val transitionState: StateFlow<ObservableTransitionState> = 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 0add4443fa7a..6b7c672fbfe0 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 @@ -31,6 +31,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -121,7 +122,21 @@ constructor( ) /** Whether the scene container is visible. */ - val isVisible: StateFlow<Boolean> = repository.isVisible + val isVisible: StateFlow<Boolean> = + combine( + repository.isVisible, + repository.isRemoteUserInteractionOngoing, + ) { isVisible, isRemoteUserInteractionOngoing -> + isVisibleInternal( + raw = isVisible, + isRemoteUserInteractionOngoing = isRemoteUserInteractionOngoing, + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = isVisibleInternal() + ) /** * Returns the keys of all scenes in the container. @@ -164,7 +179,14 @@ constructor( repository.changeScene(toScene, transitionKey) } - /** Sets the visibility of the container. */ + /** + * Sets the visibility of the container. + * + * Please do not call this from outside of the scene framework. If you are trying to force the + * visibility to visible or invisible, prefer making changes to the existing caller of this + * method or to upstream state used to calculate [isVisible]; for an example of the latter, + * please see [onRemoteUserInteractionStarted] and [onUserInteractionFinished]. + */ fun setVisible(isVisible: Boolean, loggingReason: String) { val wasVisible = repository.isVisible.value if (wasVisible == isVisible) { @@ -180,6 +202,31 @@ constructor( } /** + * Notifies that a remote user interaction has begun. + * + * This is a user interaction that originates outside of the UI of the scene container and + * possibly outside of the System UI process itself. + * + * As an example, consider the dragging that can happen in the launcher that expands the shade. + * This is a user interaction that begins remotely (as it starts in the launcher process) and is + * then rerouted by window manager to System UI. While the user interaction definitely continues + * within the System UI process and code, it also originates remotely. + */ + fun onRemoteUserInteractionStarted(loggingReason: String) { + logger.logRemoteUserInteractionStarted(loggingReason) + repository.isRemoteUserInteractionOngoing.value = true + } + + /** + * Notifies that the current user interaction (internally or remotely started, see + * [onRemoteUserInteractionStarted]) has finished. + */ + fun onUserInteractionFinished() { + logger.logUserInteractionFinished() + repository.isRemoteUserInteractionOngoing.value = false + } + + /** * Binds the given flow so the system remembers it. * * Note that you must call is with `null` when the UI is done or risk a memory leak. @@ -187,4 +234,11 @@ constructor( fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { repository.setTransitionState(transitionState) } + + private fun isVisibleInternal( + raw: Boolean = repository.isVisible.value, + isRemoteUserInteractionOngoing: Boolean = repository.isRemoteUserInteractionOngoing.value, + ): Boolean { + return raw || isRemoteUserInteractionOngoing + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index d59fcff34796..cbf7b3e7a971 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -95,6 +95,26 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } + fun logRemoteUserInteractionStarted( + reason: String, + ) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { str1 = reason }, + messagePrinter = { "remote user interaction started, reason: $str3" }, + ) + } + + fun logUserInteractionFinished() { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = {}, + messagePrinter = { "user interaction finished" }, + ) + } + companion object { private const val TAG = "SceneFramework" } 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 4cd3baa7a52e..91861aa5c29a 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 @@ -68,6 +68,12 @@ constructor( fun onMotionEvent(event: MotionEvent) { powerInteractor.onUserTouch() falsingInteractor.onTouchEvent(event) + if ( + event.actionMasked == MotionEvent.ACTION_UP || + event.actionMasked == MotionEvent.ACTION_CANCEL + ) { + sceneInteractor.onUserInteractionFinished() + } } /** |