diff options
| author | 2025-01-08 17:36:55 -0800 | |
|---|---|---|
| committer | 2025-01-09 08:27:03 -0800 | |
| commit | 865eb36b47322bf7f46870f4733040796bdedb88 (patch) | |
| tree | 520c3d1d9925af27af5c5ecdc0f686538a066761 | |
| parent | c80f98baf41a59e07fabf3a648d661fe87946ebb (diff) | |
[flexiglass] Force visible while transitioning to activities
Fix: 387352978
Test: Unit test included
Test: manually verified using the verification steps in the bug and its
duplicates
Flag: com.android.systemui.scene_container
Change-Id: I76120ac33d40618f74ffc1319a248b3c8d668ac2
7 files changed, 174 insertions, 33 deletions
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 48edded5df18..de54e75281aa 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 @@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isNotEqualTo(disabledScene) } + + @Test + fun transitionAnimations() = + kosmos.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationCancelled() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "test") + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationStart() + underTest.onTransitionAnimationStart() + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // One animation is still active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + } } 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 06dd046564df..51f056aa18da 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 @@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -141,6 +143,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @SmallTest @@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val msdlPlayer = kosmos.fakeMSDLPlayer private val authInteractionProperties = AuthInteractionProperties() + private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>() private lateinit var underTest: SceneContainerStartable @@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(true) + kosmos.activityTransitionAnimator = mockActivityTransitionAnimator + underTest = kosmos.sceneContainerStartable } @@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun hydrateActivityTransitionAnimationState() = + kosmos.runTest { + underTest.start() + + val isVisible by collectLastValue(sceneInteractor.isVisible) + assertThat(isVisible).isTrue() + + sceneInteractor.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>() + verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture()) + + val listeners = argumentCaptor.allValues + listeners.forEach { it.onTransitionAnimationStart() } + assertThat(isVisible).isTrue() + listeners.forEach { it.onTransitionAnimationEnd() } + assertThat(isVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, 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 d60f05e685bb..0488962fd076 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 @@ -90,22 +90,15 @@ constructor( initialValue = defaultTransitionState, ) - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) { - dataSource.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + /** Number of currently active transition animations. */ + val activeTransitionAnimationCount = MutableStateFlow(0) + + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) { + dataSource.changeScene(toScene = toScene, transitionKey = transitionKey) } - fun snapToScene( - toScene: SceneKey, - ) { - dataSource.snapToScene( - toScene = toScene, - ) + fun snapToScene(toScene: SceneKey) { + dataSource.snapToScene(toScene = toScene) } /** @@ -116,10 +109,7 @@ constructor( * [overlay] is already shown. */ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -130,10 +120,7 @@ constructor( * if [overlay] is already hidden. */ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -143,11 +130,7 @@ constructor( * This throws if [from] is not currently shown or if [to] is already shown. */ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } /** Sets whether the container is visible. */ 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 0e6fc36fb96a..ba9dc7625769 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 @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update /** * Generic business logic and app state accessors for the scene framework. @@ -165,12 +166,15 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = - combine(repository.isVisible, repository.isRemoteUserInputOngoing) { - isVisible, - isRemoteUserInteractionOngoing -> + combine( + repository.isVisible, + repository.isRemoteUserInputOngoing, + repository.activeTransitionAnimationCount, + ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount -> isVisibleInternal( raw = isVisible, isRemoteUserInputOngoing = isRemoteUserInteractionOngoing, + activeTransitionAnimationCount = activeTransitionAnimationCount, ) } .stateIn( @@ -436,8 +440,9 @@ constructor( private fun isVisibleInternal( raw: Boolean = repository.isVisible.value, isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value, + activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value, ): Boolean { - return raw || isRemoteUserInputOngoing + return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0 } /** @@ -525,4 +530,50 @@ constructor( ): Flow<Map<UserAction, UserActionResult>> { return disabledContentInteractor.filteredUserActions(unfiltered) } + + /** + * Notifies that a transition animation has started. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationStart() { + repository.activeTransitionAnimationCount.update { current -> + (current + 1).also { + check(it < 10) { + "Number of active transition animations is too high. Something must be" + + " calling onTransitionAnimationStart too many times!" + } + } + } + } + + /** + * Notifies that a transition animation has ended. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationEnd() { + decrementActiveTransitionAnimationCount() + } + + /** + * Notifies that a transition animation has been canceled. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationCancelled() { + decrementActiveTransitionAnimationCount() + } + + private fun decrementActiveTransitionAnimationCount() { + repository.activeTransitionAnimationCount.update { current -> + (current - 1).also { + check(it >= 0) { + "Number of active transition animations is negative. Something must be" + + " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" + + " many times!" + } + } + } + } } 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 8d8c24eae9e2..3a23a71cf7bf 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 @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable import com.android.systemui.Flags +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -146,6 +147,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val disabledContentInteractor: DisabledContentInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -169,6 +171,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() + hydrateActivityTransitionAnimationState() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -929,6 +932,35 @@ constructor( } } + /** + * Wires the scene framework to activity transition animations that originate from anywhere. A + * subset of these may actually originate from UI inside one of the scenes in the framework. + * + * Telling the scene framework about ongoing activity transition animations is critical so the + * scene framework doesn't make its scene container invisible during a transition. + * + * As it turns out, making the scene container view invisible during a transition animation + * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ + * ending or being canceled. + */ + private fun hydrateActivityTransitionAnimationState() { + activityTransitionAnimator.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + sceneInteractor.onTransitionAnimationStart() + } + + override fun onTransitionAnimationEnd() { + sceneInteractor.onTransitionAnimationEnd() + } + + override fun onTransitionAnimationCancelled() { + sceneInteractor.onTransitionAnimationCancelled() + } + } + ) + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index 5ac41ec6741c..f380789968f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock val Kosmos.mockActivityTransitionAnimatorController by Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } -val Kosmos.activityTransitionAnimator by +var Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator( // The main thread is checked in a bunch of places inside the different transitions // animators, so we have to pass the real main executor here. - mainExecutor = testCase.context.mainExecutor, + mainExecutor = testCase.context.mainExecutor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 82b5f6332b23..72c75000ebf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture { vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, disabledContentInteractor = disabledContentInteractor, + activityTransitionAnimator = activityTransitionAnimator, ) } |