Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1d6cde27 authored by Luca Zuccarini's avatar Luca Zuccarini Committed by Android (Google) Code Review
Browse files

Merge "Make long-lived transition animations asynchronous." into main

parents 48fe994f 1ca9e829
Loading
Loading
Loading
Loading
+83 −35
Original line number Diff line number Diff line
@@ -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"

@@ -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()
            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 {
                    performAnimation(delegate)
                // 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,
+25 −10
Original line number Diff line number Diff line
@@ -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() {
        with(kosmos) {
            testScope.runTest {
                val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
        val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
                val controllerFactory =
                    mock(ActivityTransitionAnimator.ControllerFactory::class.java)

        underTest.registerTransition(cookie, controllerFactory)
                underTest.registerTransition(cookie, controllerFactory, testScope)

        verify(activityStarterInternal).registerTransition(eq(cookie), eq(controllerFactory))
                verify(activityStarterInternal)
                    .registerTransition(eq(cookie), eq(controllerFactory), eq(testScope))
            }
        }
    }

    @DisableFlags(
@@ -83,12 +93,17 @@ class ActivityStarterImplTest : SysuiTestCase() {
    )
    @Test
    fun registerTransition_doesNotForwardTheRequest_whenFlaggedOff() {
        with(kosmos) {
            testScope.runTest {
                val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
        val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::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(
+25 −11
Original line number Diff line number Diff line
@@ -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() {
        with(kosmos) {
            testScope.runTest {
                val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
        val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::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() {
        with(kosmos) {
            testScope.runTest {
                val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
        val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
                val controllerFactory =
                    mock(ActivityTransitionAnimator.ControllerFactory::class.java)

                assertThrows(IllegalStateException::class.java) {
            underTest.registerTransition(cookie, controllerFactory)
                    underTest.registerTransition(cookie, controllerFactory, testScope)
                }

        verify(activityTransitionAnimator, never()).register(any(), any())
                verify(activityTransitionAnimator, never()).register(any(), any(), any())
            }
        }
    }

    @EnableFlags(
+5 −2
Original line number Diff line number Diff line
@@ -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
+3 −1
Original line number Diff line number Diff line
@@ -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) {
Loading