diff options
9 files changed, 291 insertions, 194 deletions
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index c8d3430bf54b..f03bd3d9a2a7 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -73,6 +73,9 @@ import com.android.wm.shell.shared.ShellTransitions import com.android.wm.shell.shared.TransitionUtil import java.util.concurrent.Executor import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull private const val TAG = "ActivityTransitionAnimator" @@ -241,7 +244,7 @@ constructor( override fun onTransitionAnimationProgress(linearProgress: Float) { LinkedHashSet(listeners).forEach { - it.onTransitionAnimationProgress(linearProgress) + it.onTransitionAnimationProgress(linearProgress) } } @@ -494,15 +497,19 @@ constructor( /** * Create a new animation [Runner] controlled by the [Controller] that [controllerFactory] can - * create based on [forLaunch]. + * create based on [forLaunch] and within the given [scope]. * * This method must only be used for long-lived registrations. Otherwise, use * [createEphemeralRunner]. */ @VisibleForTesting - fun createLongLivedRunner(controllerFactory: ControllerFactory, forLaunch: Boolean): Runner { + fun createLongLivedRunner( + controllerFactory: ControllerFactory, + scope: CoroutineScope, + forLaunch: Boolean, + ): Runner { assertLongLivedReturnAnimations() - return Runner(callback!!, transitionAnimator, lifecycleListener) { + return Runner(scope, callback!!, transitionAnimator, lifecycleListener) { controllerFactory.createController(forLaunch) } } @@ -564,7 +571,7 @@ constructor( * Creates a [Controller] for launching or returning from the activity linked to [cookie] * and [component]. */ - abstract fun createController(forLaunch: Boolean): Controller + abstract suspend fun createController(forLaunch: Boolean): Controller } /** @@ -691,9 +698,14 @@ constructor( * animations. * * The [Controller]s created by [controllerFactory] will only be used for transitions matching - * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. + * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. These + * [Controller]s can only be created within [scope]. */ - fun register(cookie: TransitionCookie, controllerFactory: ControllerFactory) { + fun register( + cookie: TransitionCookie, + controllerFactory: ControllerFactory, + scope: CoroutineScope, + ) { assertLongLivedReturnAnimations() if (transitionRegister == null) { @@ -725,7 +737,7 @@ constructor( } val launchRemoteTransition = RemoteTransition( - OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = true)), + OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)), "${cookie}_launchTransition", ) transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true) @@ -749,7 +761,9 @@ constructor( } val returnRemoteTransition = RemoteTransition( - OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = false)), + OriginTransition( + createLongLivedRunner(controllerFactory, scope, forLaunch = false) + ), "${cookie}_returnTransition", ) transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true) @@ -952,7 +966,9 @@ constructor( * Reusable factory to generate single-use controllers. In case of an ephemeral [Runner], * this must be null and [controller] must be defined instead. */ - private val controllerFactory: (() -> Controller)?, + private val controllerFactory: (suspend () -> Controller)?, + /** The scope to use when this runner is based on [controllerFactory]. */ + private val scope: CoroutineScope? = null, private val callback: Callback, /** The animator to use to animate the window transition. */ private val transitionAnimator: TransitionAnimator, @@ -973,13 +989,15 @@ constructor( ) constructor( + scope: CoroutineScope, callback: Callback, transitionAnimator: TransitionAnimator, listener: Listener? = null, - controllerFactory: () -> Controller, + controllerFactory: suspend () -> Controller, ) : this( controller = null, controllerFactory = controllerFactory, + scope = scope, callback = callback, transitionAnimator = transitionAnimator, listener = listener, @@ -994,12 +1012,12 @@ constructor( assert((controller != null).xor(controllerFactory != null)) delegate = null - if (controller != null) { + controller?.let { // Ephemeral launches bundle the runner with the launch request (instead of being // registered ahead of time for later use). This means that there could be a timeout // between creation and invocation, so the delegate needs to exist from the // beginning in order to handle such timeout. - createDelegate() + createDelegate(it) } } @@ -1040,49 +1058,79 @@ constructor( finishedCallback: IRemoteAnimationFinishedCallback?, performAnimation: (AnimationDelegate) -> Unit, ) { - maybeSetUp() - val delegate = delegate - mainExecutor.execute { - if (delegate == null) { - Log.i(TAG, "onAnimationStart called after completion") - // Animation started too late and timed out already. We need to still - // signal back that we're done with it. - finishedCallback?.onAnimationFinished() - } else { - performAnimation(delegate) + val controller = controller + val controllerFactory = controllerFactory + + if (controller != null) { + maybeSetUp(controller) + val success = startAnimation(performAnimation) + if (!success) finishedCallback?.onAnimationFinished() + } else if (controllerFactory != null) { + scope?.launch { + val success = + withTimeoutOrNull(TRANSITION_TIMEOUT) { + setUp(controllerFactory) + startAnimation(performAnimation) + } ?: false + if (!success) finishedCallback?.onAnimationFinished() } + } else { + // This should never happen, as either the controller or factory should always be + // defined. This final call is for safety in case something goes wrong. + Log.wtf(TAG, "initAndRun with neither a controller nor factory") + finishedCallback?.onAnimationFinished() + } + } + + /** Tries to start the animation on the main thread and returns whether it succeeded. */ + @BinderThread + private fun startAnimation(performAnimation: (AnimationDelegate) -> Unit): Boolean { + val delegate = delegate + return if (delegate != null) { + mainExecutor.execute { performAnimation(delegate) } + true + } else { + // Animation started too late and timed out already. + Log.i(TAG, "startAnimation called after completion") + false } } @BinderThread override fun onAnimationCancelled() { val delegate = delegate - mainExecutor.execute { - delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion") - delegate?.onAnimationCancelled() + if (delegate != null) { + mainExecutor.execute { delegate.onAnimationCancelled() } + } else { + Log.wtf(TAG, "onAnimationCancelled called after completion") } } + /** + * Posts the default animation timeouts. Since this only applies to ephemeral launches, this + * method is a no-op if [controller] is not defined. + */ @VisibleForTesting @UiThread fun postTimeouts() { - maybeSetUp() + controller?.let { maybeSetUp(it) } delegate?.postTimeouts() } @AnyThread - private fun maybeSetUp() { - if (controllerFactory == null || delegate != null) return - createDelegate() + private fun maybeSetUp(controller: Controller) { + if (delegate != null) return + createDelegate(controller) } @AnyThread - private fun createDelegate() { - var controller = controller - val factory = controllerFactory - if (controller == null && factory == null) return + private suspend fun setUp(createController: suspend () -> Controller) { + val controller = createController() + createDelegate(controller) + } - controller = controller ?: factory!!.invoke() + @AnyThread + private fun createDelegate(controller: Controller) { delegate = AnimationDelegate( mainExecutor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index b0b80a9419e2..52c41a07198d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -25,11 +25,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -48,6 +51,7 @@ class ActivityStarterImplTest : SysuiTestCase() { @Mock private lateinit var activityStarterInternal: ActivityStarterInternalImpl @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController private lateinit var underTest: ActivityStarterImpl + private val kosmos = testKosmos() private val mainExecutor = FakeExecutor(FakeSystemClock()) @Before @@ -69,12 +73,18 @@ class ActivityStarterImplTest : SysuiTestCase() { @EnableSceneContainer @Test fun registerTransition_forwardsTheRequest() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) - - underTest.registerTransition(cookie, controllerFactory) - - verify(activityStarterInternal).registerTransition(eq(cookie), eq(controllerFactory)) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) + + underTest.registerTransition(cookie, controllerFactory, testScope) + + verify(activityStarterInternal) + .registerTransition(eq(cookie), eq(controllerFactory), eq(testScope)) + } + } } @DisableFlags( @@ -83,12 +93,17 @@ class ActivityStarterImplTest : SysuiTestCase() { ) @Test fun registerTransition_doesNotForwardTheRequest_whenFlaggedOff() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) - underTest.registerTransition(cookie, controllerFactory) + underTest.registerTransition(cookie, controllerFactory, testScope) - verify(activityStarterInternal, never()).registerTransition(any(), any()) + verify(activityStarterInternal, never()).registerTransition(any(), any(), any()) + } + } } @EnableFlags( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt index 5406acf694ff..dfa5c9a26d79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSettingsInteracto import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController @@ -58,12 +59,14 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test @@ -109,6 +112,7 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor @Mock private lateinit var communalSettingsInteractor: CommunalSettingsInteractor private lateinit var underTest: LegacyActivityStarterInternalImpl + private val kosmos = testKosmos() private val mainExecutor = FakeExecutor(FakeSystemClock()) private val shadeAnimationInteractor = ShadeAnimationInteractorLegacyImpl(ShadeAnimationRepository(), FakeShadeRepository()) @@ -157,13 +161,18 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) @Test fun registerTransition_registers() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) - `when`(controllerFactory.cookie).thenReturn(cookie) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) + `when`(controllerFactory.cookie).thenReturn(cookie) - underTest.registerTransition(cookie, controllerFactory) + underTest.registerTransition(cookie, controllerFactory, testScope) - verify(activityTransitionAnimator).register(eq(cookie), any()) + verify(activityTransitionAnimator).register(eq(cookie), any(), eq(testScope)) + } + } } @DisableFlags( @@ -172,14 +181,19 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) @Test fun registerTransition_throws_whenFlagsAreDisabled() { - val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java) + with(kosmos) { + testScope.runTest { + val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val controllerFactory = + mock(ActivityTransitionAnimator.ControllerFactory::class.java) - assertThrows(IllegalStateException::class.java) { - underTest.registerTransition(cookie, controllerFactory) - } + assertThrows(IllegalStateException::class.java) { + underTest.registerTransition(cookie, controllerFactory, testScope) + } - verify(activityTransitionAnimator, never()).register(any(), any()) + verify(activityTransitionAnimator, never()).register(any(), any(), any()) + } + } } @EnableFlags( diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index ca98cbf20c3a..18891dba4b0d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -25,6 +25,8 @@ import android.view.View; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.plugins.annotations.ProvidesInterface; +import kotlinx.coroutines.CoroutineScope; + /** * An interface to start activities. This is used as a callback from the views to * {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the @@ -37,11 +39,12 @@ public interface ActivityStarter { /** * Registers the given {@link ActivityTransitionAnimator.ControllerFactory} for launching and * closing transitions matching the {@link ActivityTransitionAnimator.TransitionCookie} and the - * {@link ComponentName} that it contains. + * {@link ComponentName} that it contains, within the given {@link CoroutineScope}. */ void registerTransition( ActivityTransitionAnimator.TransitionCookie cookie, - ActivityTransitionAnimator.ControllerFactory controllerFactory); + ActivityTransitionAnimator.ControllerFactory controllerFactory, + CoroutineScope scope); /** * Unregisters the {@link ActivityTransitionAnimator.ControllerFactory} previously registered diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 5a63c0cd84e6..bd1d7f755a74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** Handles start activity logic in SystemUI. */ @SysUISingleton @@ -52,9 +53,10 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { if (!TransitionAnimator.longLivedReturnAnimationsEnabled()) return - activityStarterInternal.registerTransition(cookie, controllerFactory) + activityStarterInternal.registerTransition(cookie, controllerFactory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt index 5e427fbf1f7e..015ec3052134 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt @@ -25,15 +25,17 @@ import android.view.View import com.android.systemui.ActivityIntentHelper import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.plugins.ActivityStarter +import kotlinx.coroutines.CoroutineScope interface ActivityStarterInternal { /** * Registers the given [controllerFactory] for launching and closing transitions matching the - * [cookie] and the [ComponentName] that it contains. + * [cookie] and the [ComponentName] that it contains, within the given [scope]. */ fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt index 7289c2ed5897..6e82d7f7401a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt @@ -66,6 +66,7 @@ import com.android.systemui.util.kotlin.getOrNull import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled. @@ -105,6 +106,7 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { check(TransitionAnimator.longLivedReturnAnimationsEnabled()) @@ -116,7 +118,7 @@ constructor( controllerFactory.launchCujType, controllerFactory.returnCujType, ) { - override fun createController( + override suspend fun createController( forLaunch: Boolean ): ActivityTransitionAnimator.Controller { val baseController = controllerFactory.createController(forLaunch) @@ -132,7 +134,7 @@ constructor( } } - activityTransitionAnimator.register(cookie, factory) + activityTransitionAnimator.register(cookie, factory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt index d7a29c36f2ce..76f67dc6c146 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt @@ -64,6 +64,7 @@ import com.android.systemui.util.kotlin.getOrNull import dagger.Lazy import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope /** Encapsulates the activity logic for activity starter. */ @SysUISingleton @@ -102,6 +103,7 @@ constructor( override fun registerTransition( cookie: ActivityTransitionAnimator.TransitionCookie, controllerFactory: ActivityTransitionAnimator.ControllerFactory, + scope: CoroutineScope, ) { check(TransitionAnimator.longLivedReturnAnimationsEnabled()) @@ -113,7 +115,7 @@ constructor( controllerFactory.launchCujType, controllerFactory.returnCujType, ) { - override fun createController( + override suspend fun createController( forLaunch: Boolean ): ActivityTransitionAnimator.Controller { val baseController = controllerFactory.createController(forLaunch) @@ -129,7 +131,7 @@ constructor( } } - activityTransitionAnimator.register(cookie, factory) + activityTransitionAnimator.register(cookie, factory, scope) } override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index fd751d9cc7c3..845be0252581 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -27,7 +27,10 @@ import android.window.WindowAnimationState import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope import com.android.systemui.shared.Flags +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.wm.shell.shared.ShellTransitions import com.google.common.truth.Truth.assertThat @@ -38,6 +41,9 @@ import junit.framework.Assert.assertTrue import junit.framework.AssertionFailedError import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -54,10 +60,12 @@ import org.mockito.Mockito.`when` import org.mockito.Spy import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class ActivityTransitionAnimatorTest : SysuiTestCase() { + private val kosmos = testKosmos() private val transitionContainer = LinearLayout(mContext) private val mainExecutor = context.mainExecutor private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor) @@ -67,12 +75,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Spy private val controller = TestTransitionAnimatorController(transitionContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback - private lateinit var activityTransitionAnimator: ActivityTransitionAnimator + private lateinit var underTest: ActivityTransitionAnimator @get:Rule val rule = MockitoJUnit.rule() @Before fun setup() { - activityTransitionAnimator = + underTest = ActivityTransitionAnimator( mainExecutor, ActivityTransitionAnimator.TransitionRegister.fromShellTransitions( @@ -82,17 +90,17 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, disableWmTimeout = true, ) - activityTransitionAnimator.callback = callback - activityTransitionAnimator.addListener(listener) + underTest.callback = callback + underTest.addListener(listener) } @After fun tearDown() { - activityTransitionAnimator.removeListener(listener) + underTest.removeListener(listener) } private fun startIntentWithAnimation( - animator: ActivityTransitionAnimator = this.activityTransitionAnimator, + animator: ActivityTransitionAnimator = underTest, controller: ActivityTransitionAnimator.Controller? = this.controller, animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int, @@ -157,7 +165,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(activityTransitionAnimator) { adapter -> + startIntentWithAnimation(underTest) { adapter -> animationAdapter = adapter ActivityManager.START_DELIVERED_TO_TOP } @@ -185,9 +193,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { fun registersReturnIffCookieIsPresent() { `when`(callback.isOnKeyguard()).thenReturn(false) - startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } waitForIdleSync() assertTrue(testShellTransitions.remotes.isEmpty()) @@ -199,9 +205,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { get() = ActivityTransitionAnimator.TransitionCookie("testCookie") } - startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } waitForIdleSync() assertEquals(1, testShellTransitions.remotes.size) @@ -214,13 +218,15 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun registersLongLivedTransition() { - var factory = controllerFactory() - activityTransitionAnimator.register(factory.cookie, factory) - assertEquals(2, testShellTransitions.remotes.size) - - factory = controllerFactory() - activityTransitionAnimator.register(factory.cookie, factory) - assertEquals(4, testShellTransitions.remotes.size) + kosmos.runTest { + var factory = controllerFactory() + underTest.register(factory.cookie, factory, testScope) + assertEquals(2, testShellTransitions.remotes.size) + + factory = controllerFactory() + underTest.register(factory.cookie, factory, testScope) + assertEquals(4, testShellTransitions.remotes.size) + } } @EnableFlags( @@ -229,49 +235,55 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun registersLongLivedTransitionOverridingPreviousRegistration() { - val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") - var factory = controllerFactory(cookie) - activityTransitionAnimator.register(cookie, factory) - val transitions = testShellTransitions.remotes.values.toList() - - factory = controllerFactory(cookie) - activityTransitionAnimator.register(cookie, factory) - assertEquals(2, testShellTransitions.remotes.size) - for (transition in transitions) { - assertThat(testShellTransitions.remotes.values).doesNotContain(transition) + kosmos.runTest { + val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") + var factory = controllerFactory(cookie) + underTest.register(cookie, factory, testScope) + val transitions = testShellTransitions.remotes.values.toList() + + factory = controllerFactory(cookie) + underTest.register(cookie, factory, testScope) + assertEquals(2, testShellTransitions.remotes.size) + for (transition in transitions) { + assertThat(testShellTransitions.remotes.values).doesNotContain(transition) + } } } @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { - val factory = controllerFactory(component = null) - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) + kosmos.runTest { + val factory = controllerFactory(component = null) + assertThrows(IllegalStateException::class.java) { + underTest.register(factory.cookie, factory, testScope) + } } } @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { - // No ComponentName - var factory = controllerFactory(component = null) - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) - } + kosmos.runTest { + // No ComponentName + var factory = controllerFactory(component = null) + assertThrows(IllegalStateException::class.java) { + underTest.register(factory.cookie, factory, testScope) + } - // No TransitionRegister - activityTransitionAnimator = - ActivityTransitionAnimator( - mainExecutor, - transitionRegister = null, - testTransitionAnimator, - testTransitionAnimator, - disableWmTimeout = true, - ) - factory = controllerFactory() - assertThrows(IllegalStateException::class.java) { - activityTransitionAnimator.register(factory.cookie, factory) + // No TransitionRegister + val activityTransitionAnimator = + ActivityTransitionAnimator( + mainExecutor, + transitionRegister = null, + testTransitionAnimator, + testTransitionAnimator, + disableWmTimeout = true, + ) + factory = controllerFactory() + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(factory.cookie, factory, testScope) + } } } @@ -281,27 +293,29 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun unregistersLongLivedTransition() { - val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) + kosmos.runTest { + val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) - for (index in 0 until 3) { - cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val factory = controllerFactory(cookies[index]!!) - activityTransitionAnimator.register(factory.cookie, factory) - } + for (index in 0 until 3) { + cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) + val factory = controllerFactory(cookies[index]!!) + underTest.register(factory.cookie, factory, testScope) + } - activityTransitionAnimator.unregister(cookies[0]!!) - assertEquals(4, testShellTransitions.remotes.size) + underTest.unregister(cookies[0]!!) + assertEquals(4, testShellTransitions.remotes.size) - activityTransitionAnimator.unregister(cookies[2]!!) - assertEquals(2, testShellTransitions.remotes.size) + underTest.unregister(cookies[2]!!) + assertEquals(2, testShellTransitions.remotes.size) - activityTransitionAnimator.unregister(cookies[1]!!) - assertThat(testShellTransitions.remotes).isEmpty() + underTest.unregister(cookies[1]!!) + assertThat(testShellTransitions.remotes).isEmpty() + } } @Test fun doesNotStartIfAnimationIsCancelled() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationCancelled() runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) @@ -315,7 +329,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun cancelsIfNoOpeningWindowIsFound() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() @@ -328,7 +342,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun startsAnimationIfWindowIsOpening() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart( TRANSIT_NONE, arrayOf(fakeWindow()), @@ -354,9 +368,11 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() { - assertThrows(IllegalStateException::class.java) { - val factory = controllerFactory() - activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) + kosmos.runTest { + assertThrows(IllegalStateException::class.java) { + val factory = controllerFactory() + underTest.createLongLivedRunner(factory, testScope, forLaunch = true) + } } } @@ -365,44 +381,34 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test - fun runnerCreatesDelegateLazily_whenPostingTimeouts() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) - assertNull(runner.delegate) - runner.postTimeouts() - assertNotNull(runner.delegate) - } - - @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, - ) - @Test fun runnerCreatesDelegateLazily_onAnimationStart() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true) - assertNull(runner.delegate) - - var delegateInitialized = false - activityTransitionAnimator.addListener( - object : ActivityTransitionAnimator.Listener { - override fun onTransitionAnimationStart() { - // This is called iff the delegate was initialized, so it's a good proxy for - // checking the initialization. - delegateInitialized = true + kosmos.runTest { + val factory = controllerFactory() + val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true) + assertNull(runner.delegate) + + var delegateInitialized = false + underTest.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + // This is called iff the delegate was initialized, so it's a good proxy for + // checking the initialization. + delegateInitialized = true + } } - } - ) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) + ) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + testScope.advanceUntilIdle() + waitForIdleSync() - waitForIdleSync() - assertTrue(delegateInitialized) + assertTrue(delegateInitialized) + } } @EnableFlags( @@ -411,29 +417,32 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun runnerCreatesDelegateLazily_onAnimationTakeover() { - val factory = controllerFactory() - val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = false) - assertNull(runner.delegate) - - var delegateInitialized = false - activityTransitionAnimator.addListener( - object : ActivityTransitionAnimator.Listener { - override fun onTransitionAnimationStart() { - // This is called iff the delegate was initialized, so it's a good proxy for - // checking the initialization. - delegateInitialized = true + kosmos.runTest { + val factory = controllerFactory() + val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false) + assertNull(runner.delegate) + + var delegateInitialized = false + underTest.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + // This is called iff the delegate was initialized, so it's a good proxy for + // checking the initialization. + delegateInitialized = true + } } - } - ) - runner.takeOverAnimation( - arrayOf(fakeWindow(MODE_CLOSING)), - arrayOf(WindowAnimationState()), - SurfaceControl.Transaction(), - iCallback, - ) + ) + runner.takeOverAnimation( + arrayOf(fakeWindow(MODE_CLOSING)), + arrayOf(WindowAnimationState()), + SurfaceControl.Transaction(), + iCallback, + ) + testScope.advanceUntilIdle() + waitForIdleSync() - waitForIdleSync() - assertTrue(delegateInitialized) + assertTrue(delegateInitialized) + } } @DisableFlags( @@ -442,7 +451,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun animationTakeoverThrows_whenTheFlagsAreDisabled() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) assertThrows(IllegalStateException::class.java) { runner.takeOverAnimation( arrayOf(fakeWindow()), @@ -459,7 +468,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { ) @Test fun disposeRunner_delegateDereferenced() { - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) assertNotNull(runner.delegate) runner.dispose() waitForIdleSync() @@ -469,13 +478,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun concurrentListenerModification_doesNotThrow() { // Need a second listener to trigger the concurrent modification. - activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {}) + underTest.addListener(object : ActivityTransitionAnimator.Listener {}) `when`(listener.onTransitionAnimationStart()).thenAnswer { - activityTransitionAnimator.removeListener(listener) + underTest.removeListener(listener) listener } - val runner = activityTransitionAnimator.createEphemeralRunner(controller) + val runner = underTest.createEphemeralRunner(controller) runner.onAnimationStart( TRANSIT_NONE, arrayOf(fakeWindow()), @@ -494,7 +503,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { component: ComponentName? = mock(ComponentName::class.java), ): ActivityTransitionAnimator.ControllerFactory { return object : ActivityTransitionAnimator.ControllerFactory(cookie, component) { - override fun createController(forLaunch: Boolean) = + override suspend fun createController(forLaunch: Boolean) = object : DelegateTransitionAnimatorController(controller) { override val isLaunching: Boolean get() = forLaunch |