From abd997f638908509390589cf8f0dd6ce5b9f4511 Mon Sep 17 00:00:00 2001 From: Luca Zuccarini Date: Tue, 21 May 2024 13:08:13 +0000 Subject: Introduce long-lived registrations. These are used for any launch/return animations that can happen more than once and aren't necessarily linked in pairs (i.e. only return if the associated launch has happened). Bug: 323863002 Flag: ACONFIG com.android.systemui.shared.return_animation_framework_library DISABLED Test: unit tests and tested with ongoing call chip Change-Id: Id6ced4fc5ea3665cf2f5946356e3b752fdf86085 --- .../animation/ActivityTransitionAnimator.kt | 121 +++++++++++++++++- .../com/android/systemui/animation/Expandable.kt | 15 ++- .../GhostedViewTransitionAnimatorController.kt | 2 + .../compose/animation/ExpandableController.kt | 7 +- .../animation/ActivityTransitionAnimatorTest.kt | 135 +++++++++++++++++++++ 5 files changed, 272 insertions(+), 8 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 23df26fdb246..b6e4e9b13a1c 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -20,6 +20,8 @@ import android.app.ActivityManager import android.app.ActivityTaskManager import android.app.PendingIntent import android.app.TaskInfo +import android.app.WindowConfiguration +import android.content.ComponentName import android.graphics.Matrix import android.graphics.Rect import android.graphics.RectF @@ -38,7 +40,9 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.view.animation.PathInterpolator import android.window.RemoteTransition import android.window.TransitionFilter @@ -203,6 +207,10 @@ constructor( } } + /** Book-keeping for long-lived transitions that are currently registered. */ + private val longLivedTransitions = + HashMap>() + /** * Start an intent and animate the opening window. The intent will be started by running * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch @@ -497,6 +505,7 @@ constructor( view: View, cujType: Int? = null, cookie: TransitionCookie? = null, + component: ComponentName? = null, returnCujType: Int? = null ): Controller? { // Make sure the View we launch from implements LaunchableView to avoid visibility @@ -519,7 +528,13 @@ constructor( return null } - return GhostedViewTransitionAnimatorController(view, cujType, cookie, returnCujType) + return GhostedViewTransitionAnimatorController( + view, + cujType, + cookie, + component, + returnCujType + ) } } @@ -553,6 +568,16 @@ constructor( val transitionCookie: TransitionCookie? get() = null + /** + * The [ComponentName] of the activity whose window is tied to this [Controller]. + * + * This is used as a fallback when a cookie is defined but there is no match (e.g. when a + * matching activity was launched by a mean different from the launchable in this + * [Controller]), and should be defined for all long-lived registered [Controller]s. + */ + val component: ComponentName? + get() = null + /** * The intent was started. If [willAnimate] is false, nothing else will happen and the * animation will not be started. @@ -571,6 +596,91 @@ constructor( fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {} } + /** + * Registers [controller] as a long-lived transition handler for launch and return animations. + * + * The [controller] will only be used for transitions matching the [TransitionCookie] defined + * within it, or the [ComponentName] if the cookie matching fails. Both fields are mandatory for + * this registration. + */ + fun register(controller: Controller) { + check(returnAnimationFrameworkLibrary()) { + "Long-lived registrations cannot be used when the returnAnimationFrameworkLibrary " + + "flag is disabled" + } + + if (transitionRegister == null) { + throw IllegalStateException( + "A RemoteTransitionRegister must be provided when creating this animator in " + + "order to use long-lived animations" + ) + } + + val cookie = + controller.transitionCookie + ?: throw IllegalStateException( + "A cookie must be defined in order to use long-lived animations" + ) + val component = + controller.component + ?: throw IllegalStateException( + "A component must be defined in order to use long-lived animations" + ) + + // Make sure that any previous registrations linked to the same cookie are gone. + unregister(cookie) + + val launchFilter = + TransitionFilter().apply { + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD + mModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT) + mTopActivity = component + } + ) + } + val launchRemoteTransition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(createRunner(controller)), + "${cookie}_launchTransition" + ) + transitionRegister.register(launchFilter, launchRemoteTransition) + + val returnController = + object : Controller by controller { + override val isLaunching: Boolean = false + } + val returnFilter = + TransitionFilter().apply { + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD + mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + mTopActivity = component + } + ) + } + val returnRemoteTransition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(createRunner(returnController)), + "${cookie}_returnTransition" + ) + transitionRegister.register(returnFilter, returnRemoteTransition) + + longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition) + } + + /** Unregisters all controllers previously registered that contain [cookie]. */ + fun unregister(cookie: TransitionCookie) { + val transitions = longLivedTransitions[cookie] ?: return + transitionRegister?.unregister(transitions.first) + transitionRegister?.unregister(transitions.second) + longLivedTransitions.remove(cookie) + } + /** * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all * events to the passed [delegate]. @@ -817,13 +927,16 @@ constructor( if (it.mode == targetMode) { if (activityTransitionUseLargestWindow()) { if (returnAnimationFrameworkLibrary()) { - // If the controller contains a cookie, _only_ match if the candidate - // contains the matching cookie. + // If the controller contains a cookie, _only_ match if either the + // candidate contains the matching cookie, or a component is also + // defined and is a match. if ( controller.transitionCookie != null && it.taskInfo ?.launchCookies - ?.contains(controller.transitionCookie) != true + ?.contains(controller.transitionCookie) != true && + (controller.component == null || + it.taskInfo?.topActivity != controller.component) ) { continue } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index 21557b8bb402..3ba9a2974846 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -16,6 +16,7 @@ package com.android.systemui.animation +import android.content.ComponentName import android.view.View /** A piece of UI that can be expanded into a Dialog or an Activity. */ @@ -28,13 +29,16 @@ interface Expandable { * @param launchCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] * associated to the launch that will use this controller. * @param cookie The unique cookie associated with the launch that will use this controller. - * This is required iff the a return animation should be included. + * This is required iff a return animation should be included. + * @param component The name of the activity that will be launched by this controller. This is + * required for long-lived registrations only. * @param returnCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] * associated to the return animation that will use this controller. */ fun activityTransitionController( launchCujType: Int? = null, cookie: ActivityTransitionAnimator.TransitionCookie? = null, + component: ComponentName? = null, returnCujType: Int? = null ): ActivityTransitionAnimator.Controller? @@ -47,7 +51,12 @@ interface Expandable { fun activityTransitionController( launchCujType: Int? = null ): ActivityTransitionAnimator.Controller? { - return activityTransitionController(launchCujType, cookie = null, returnCujType = null) + return activityTransitionController( + launchCujType, + cookie = null, + component = null, + returnCujType = null + ) } /** @@ -70,12 +79,14 @@ interface Expandable { override fun activityTransitionController( launchCujType: Int?, cookie: ActivityTransitionAnimator.TransitionCookie?, + component: ComponentName?, returnCujType: Int? ): ActivityTransitionAnimator.Controller? { return ActivityTransitionAnimator.Controller.fromView( view, launchCujType, cookie, + component, returnCujType ) } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 9d4507337e51..e626c04675e1 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -16,6 +16,7 @@ package com.android.systemui.animation +import android.content.ComponentName import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Insets @@ -62,6 +63,7 @@ constructor( /** The [CujType] associated to this launch animation. */ private val launchCujType: Int? = null, override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null, + override val component: ComponentName? = null, /** The [CujType] associated to this return animation. */ private val returnCujType: Int? = null, diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt index 17a606171a9e..a55df2b36a80 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt @@ -16,6 +16,7 @@ package com.android.compose.animation +import android.content.ComponentName import android.view.View import android.view.ViewGroup import android.view.ViewGroupOverlay @@ -136,13 +137,14 @@ internal class ExpandableControllerImpl( override fun activityTransitionController( launchCujType: Int?, cookie: ActivityTransitionAnimator.TransitionCookie?, + component: ComponentName?, returnCujType: Int? ): ActivityTransitionAnimator.Controller? { if (!isComposed.value) { return null } - return activityController(launchCujType, cookie, returnCujType) + return activityController(launchCujType, cookie, component, returnCujType) } override fun dialogTransitionController( @@ -170,7 +172,6 @@ internal class ExpandableControllerImpl( override var transitionContainer: ViewGroup = composeViewRoot.rootView as ViewGroup - // TODO(b/323863002): update to be dependant on usage. override val isLaunching: Boolean = true override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { @@ -267,6 +268,7 @@ internal class ExpandableControllerImpl( private fun activityController( launchCujType: Int?, cookie: ActivityTransitionAnimator.TransitionCookie?, + component: ComponentName?, returnCujType: Int? ): ActivityTransitionAnimator.Controller { val delegate = transitionController() @@ -284,6 +286,7 @@ internal class ExpandableControllerImpl( } override val transitionCookie = cookie + override val component = component override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { delegate.onTransitionAnimationStart(isExpandingFullyAbove) 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 fd37cad72371..70a544ca6d2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.shared.Flags import com.android.systemui.util.mockito.any import com.android.wm.shell.shared.ShellTransitions +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertNull @@ -202,6 +203,140 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) } + @Test + fun registersLongLivedTransition() { + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + + activityTransitionAnimator.register( + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = + ActivityTransitionAnimator.TransitionCookie("test_cookie_1") + override val component = ComponentName("com.test.package", "Test1") + } + ) + assertEquals(2, testShellTransitions.remotes.size) + + activityTransitionAnimator.register( + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = + ActivityTransitionAnimator.TransitionCookie("test_cookie_2") + override val component = ComponentName("com.test.package", "Test2") + } + ) + assertEquals(4, testShellTransitions.remotes.size) + } + + @Test + fun registersLongLivedTransitionOverridingPreviousRegistration() { + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + + val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") + activityTransitionAnimator.register( + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = cookie + override val component = ComponentName("com.test.package", "Test1") + } + ) + val transitions = testShellTransitions.remotes.values.toList() + + activityTransitionAnimator.register( + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = cookie + override val component = ComponentName("com.test.package", "Test2") + } + ) + assertEquals(2, testShellTransitions.remotes.size) + for (transition in transitions) { + assertThat(testShellTransitions.remotes.values).doesNotContain(transition) + } + } + + @Test + fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { + setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + + val controller = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = + ActivityTransitionAnimator.TransitionCookie("test_cookie") + override val component = ComponentName("com.test.package", "Test") + } + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(controller) + } + } + + @Test + fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + + // No TransitionCookie + val controllerWithoutCookie = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = null + } + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(controllerWithoutCookie) + } + + // No ComponentName + val controllerWithoutComponent = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = + ActivityTransitionAnimator.TransitionCookie("test_cookie") + override val component = null + } + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(controllerWithoutComponent) + } + + // No TransitionRegister + activityTransitionAnimator = + ActivityTransitionAnimator( + mainExecutor, + transitionRegister = null, + testTransitionAnimator, + testTransitionAnimator, + disableWmTimeout = true, + ) + val validController = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = + ActivityTransitionAnimator.TransitionCookie("test_cookie") + override val component = ComponentName("com.test.package", "Test") + } + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.register(validController) + } + } + + @Test + fun unregistersLongLivedTransition() { + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + + val cookies = arrayOfNulls(3) + + for (index in 0 until 3) { + cookies[index] = ActivityTransitionAnimator.TransitionCookie("test_cookie_$index") + + val controller = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie = cookies[index] + override val component = ComponentName("foo.bar", "Foobar") + } + activityTransitionAnimator.register(controller) + } + + activityTransitionAnimator.unregister(cookies[0]!!) + assertEquals(4, testShellTransitions.remotes.size) + + activityTransitionAnimator.unregister(cookies[2]!!) + assertEquals(2, testShellTransitions.remotes.size) + + activityTransitionAnimator.unregister(cookies[1]!!) + assertThat(testShellTransitions.remotes).isEmpty() + } + @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityTransitionAnimator.createRunner(controller) -- cgit v1.2.3-59-g8ed1b