summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Alejandro Nijamkin <nijamkin@google.com> 2025-01-08 17:36:55 -0800
committer Alejandro Nijamkin <nijamkin@google.com> 2025-01-09 08:27:03 -0800
commit865eb36b47322bf7f46870f4733040796bdedb88 (patch)
tree520c3d1d9925af27af5c5ecdc0f686538a066761
parentc80f98baf41a59e07fabf3a648d661fe87946ebb (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
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt2
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,
)
}