1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.animation
18 
19 import android.app.ActivityManager
20 import android.app.ActivityTaskManager
21 import android.app.PendingIntent
22 import android.app.TaskInfo
23 import android.app.WindowConfiguration
24 import android.content.ComponentName
25 import android.graphics.Color
26 import android.graphics.Matrix
27 import android.graphics.Rect
28 import android.graphics.RectF
29 import android.os.Binder
30 import android.os.Build
31 import android.os.Handler
32 import android.os.Looper
33 import android.os.RemoteException
34 import android.util.Log
35 import android.view.IRemoteAnimationFinishedCallback
36 import android.view.IRemoteAnimationRunner
37 import android.view.RemoteAnimationAdapter
38 import android.view.RemoteAnimationTarget
39 import android.view.SyncRtSurfaceTransactionApplier
40 import android.view.View
41 import android.view.ViewGroup
42 import android.view.WindowManager
43 import android.view.WindowManager.TRANSIT_CLOSE
44 import android.view.WindowManager.TRANSIT_OPEN
45 import android.view.WindowManager.TRANSIT_TO_BACK
46 import android.view.WindowManager.TRANSIT_TO_FRONT
47 import android.view.animation.PathInterpolator
48 import android.window.RemoteTransition
49 import android.window.TransitionFilter
50 import androidx.annotation.AnyThread
51 import androidx.annotation.BinderThread
52 import androidx.annotation.UiThread
53 import com.android.app.animation.Interpolators
54 import com.android.internal.annotations.VisibleForTesting
55 import com.android.internal.policy.ScreenDecorationsUtils
56 import com.android.systemui.Flags.activityTransitionUseLargestWindow
57 import com.android.systemui.Flags.translucentOccludingActivityFix
58 import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
59 import com.android.wm.shell.shared.IShellTransitions
60 import com.android.wm.shell.shared.ShellTransitions
61 import java.util.concurrent.Executor
62 import kotlin.math.roundToInt
63 
64 private const val TAG = "ActivityTransitionAnimator"
65 
66 /**
67  * A class that allows activities to be started in a seamless way from a view that is transforming
68  * nicely into the starting window.
69  */
70 class ActivityTransitionAnimator
71 @JvmOverloads
72 constructor(
73     /** The executor that runs on the main thread. */
74     private val mainExecutor: Executor,
75 
76     /** The object used to register ephemeral returns and long-lived transitions. */
77     private val transitionRegister: TransitionRegister? = null,
78 
79     /** The animator used when animating a View into an app. */
80     private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
81 
82     /** The animator used when animating a Dialog into an app. */
83     // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
84     // TIMINGS.contentBeforeFadeOutDuration.
85     private val dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
86 
87     /**
88      * Whether we should disable the WindowManager timeout. This should be set to true in tests
89      * only.
90      */
91     // TODO(b/301385865): Remove this flag.
92     private val disableWmTimeout: Boolean = false,
93 ) {
94     @JvmOverloads
95     constructor(
96         mainExecutor: Executor,
97         shellTransitions: ShellTransitions,
98         transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
99         dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
100         disableWmTimeout: Boolean = false,
101     ) : this(
102         mainExecutor,
103         TransitionRegister.fromShellTransitions(shellTransitions),
104         transitionAnimator,
105         dialogToAppAnimator,
106         disableWmTimeout,
107     )
108 
109     @JvmOverloads
110     constructor(
111         mainExecutor: Executor,
112         iShellTransitions: IShellTransitions,
113         transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
114         dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
115         disableWmTimeout: Boolean = false,
116     ) : this(
117         mainExecutor,
118         TransitionRegister.fromIShellTransitions(iShellTransitions),
119         transitionAnimator,
120         dialogToAppAnimator,
121         disableWmTimeout,
122     )
123 
124     companion object {
125         /** The timings when animating a View into an app. */
126         @JvmField
127         val TIMINGS =
128             TransitionAnimator.Timings(
129                 totalDuration = 500L,
130                 contentBeforeFadeOutDelay = 0L,
131                 contentBeforeFadeOutDuration = 150L,
132                 contentAfterFadeInDelay = 150L,
133                 contentAfterFadeInDuration = 183L
134             )
135 
136         /**
137          * The timings when animating a Dialog into an app. We need to wait at least 200ms before
138          * showing the app (which is under the dialog window) so that the dialog window dim is fully
139          * faded out, to avoid flicker.
140          */
141         val DIALOG_TIMINGS =
142             TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L)
143 
144         /** The interpolators when animating a View or a dialog into an app. */
145         val INTERPOLATORS =
146             TransitionAnimator.Interpolators(
147                 positionInterpolator = Interpolators.EMPHASIZED,
148                 positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT,
149                 contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
150                 contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
151             )
152 
153         // TODO(b/288507023): Remove this flag.
154         @JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE
155 
156         /** Durations & interpolators for the navigation bar fading in & out. */
157         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
158         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
159         private val ANIMATION_DELAY_NAV_FADE_IN =
160             TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN
161 
162         private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
163         private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
164 
165         /** The time we wait before timing out the remote animation after starting the intent. */
166         private const val TRANSITION_TIMEOUT = 1_000L
167 
168         /**
169          * The time we wait before we Log.wtf because the remote animation was neither started or
170          * cancelled by WM.
171          */
172         private const val LONG_TRANSITION_TIMEOUT = 5_000L
173 
defaultTransitionAnimatornull174         private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator {
175             return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS)
176         }
177 
defaultDialogToAppAnimatornull178         private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator {
179             return TransitionAnimator(mainExecutor, DIALOG_TIMINGS, INTERPOLATORS)
180         }
181     }
182 
183     /**
184      * The callback of this animator. This should be set before any call to
185      * [start(Pending)IntentWithAnimation].
186      */
187     var callback: Callback? = null
188 
189     /** The set of [Listener] that should be notified of any animation started by this animator. */
190     private val listeners = LinkedHashSet<Listener>()
191 
192     /** Top-level listener that can be used to notify all registered [listeners]. */
193     private val lifecycleListener =
194         object : Listener {
onTransitionAnimationStartnull195             override fun onTransitionAnimationStart() {
196                 listeners.forEach { it.onTransitionAnimationStart() }
197             }
198 
onTransitionAnimationEndnull199             override fun onTransitionAnimationEnd() {
200                 listeners.forEach { it.onTransitionAnimationEnd() }
201             }
202 
onTransitionAnimationProgressnull203             override fun onTransitionAnimationProgress(linearProgress: Float) {
204                 listeners.forEach { it.onTransitionAnimationProgress(linearProgress) }
205             }
206 
onTransitionAnimationCancellednull207             override fun onTransitionAnimationCancelled() {
208                 listeners.forEach { it.onTransitionAnimationCancelled() }
209             }
210         }
211 
212     /** Book-keeping for long-lived transitions that are currently registered. */
213     private val longLivedTransitions =
214         HashMap<TransitionCookie, Pair<RemoteTransition, RemoteTransition>>()
215 
216     /**
217      * Start an intent and animate the opening window. The intent will be started by running
218      * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
219      * result. [controller] is responsible from animating the view from which the intent was started
220      * in [Controller.onTransitionAnimationProgress]. No animation will start if there is no window
221      * opening.
222      *
223      * If [controller] is null or [animate] is false, then the intent will be started and no
224      * animation will run.
225      *
226      * If possible, you should pass the [packageName] of the intent that will be started so that
227      * trampoline activity launches will also be animated.
228      *
229      * If the device is currently locked, the user will have to unlock it before the intent is
230      * started unless [showOverLockscreen] is true. In that case, the activity will be started
231      * directly over the lockscreen.
232      *
233      * This method will throw any exception thrown by [intentStarter].
234      */
235     @JvmOverloads
startIntentWithAnimationnull236     fun startIntentWithAnimation(
237         controller: Controller?,
238         animate: Boolean = true,
239         packageName: String? = null,
240         showOverLockscreen: Boolean = false,
241         intentStarter: (RemoteAnimationAdapter?) -> Int
242     ) {
243         if (controller == null || !animate) {
244             Log.i(TAG, "Starting intent with no animation")
245             intentStarter(null)
246             controller?.callOnIntentStartedOnMainThread(willAnimate = false)
247             return
248         }
249 
250         val callback =
251             this.callback
252                 ?: throw IllegalStateException(
253                     "ActivityTransitionAnimator.callback must be set before using this animator"
254                 )
255         val runner = createRunner(controller)
256         val runnerDelegate = runner.delegate!!
257         val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
258 
259         // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the
260         // keyguard with the animation
261         val animationAdapter =
262             if (!hideKeyguardWithAnimation) {
263                 RemoteAnimationAdapter(
264                     runner,
265                     TIMINGS.totalDuration,
266                     TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
267                 )
268             } else {
269                 null
270             }
271 
272         // Register the remote animation for the given package to also animate trampoline
273         // activity launches.
274         if (packageName != null && animationAdapter != null) {
275             try {
276                 ActivityTaskManager.getService()
277                     .registerRemoteAnimationForNextActivityStart(
278                         packageName,
279                         animationAdapter,
280                         null /* launchCookie */
281                     )
282             } catch (e: RemoteException) {
283                 Log.w(TAG, "Unable to register the remote animation", e)
284             }
285         }
286 
287         if (animationAdapter != null && controller.transitionCookie != null) {
288             registerEphemeralReturnAnimation(controller, transitionRegister)
289         }
290 
291         val launchResult = intentStarter(animationAdapter)
292 
293         // Only animate if the app is not already on top and will be opened, unless we are on the
294         // keyguard.
295         val willAnimate =
296             launchResult == ActivityManager.START_TASK_TO_FRONT ||
297                 launchResult == ActivityManager.START_SUCCESS ||
298                 (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
299                     hideKeyguardWithAnimation)
300 
301         Log.i(
302             TAG,
303             "launchResult=$launchResult willAnimate=$willAnimate " +
304                 "hideKeyguardWithAnimation=$hideKeyguardWithAnimation"
305         )
306         controller.callOnIntentStartedOnMainThread(willAnimate)
307 
308         // If we expect an animation, post a timeout to cancel it in case the remote animation is
309         // never started.
310         if (willAnimate) {
311             runnerDelegate.postTimeouts()
312 
313             // Hide the keyguard using the launch animation instead of the default unlock animation.
314             if (hideKeyguardWithAnimation) {
315                 callback.hideKeyguardWithAnimation(runner)
316             }
317         } else {
318             // We need to make sure delegate references are dropped to avoid memory leaks.
319             runner.dispose()
320         }
321     }
322 
Controllernull323     private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
324         if (Looper.myLooper() != Looper.getMainLooper()) {
325             mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) }
326         } else {
327             if (DEBUG_TRANSITION_ANIMATION) {
328                 Log.d(
329                     TAG,
330                     "Calling controller.onIntentStarted(willAnimate=$willAnimate) " +
331                         "[controller=$this]"
332                 )
333             }
334             this.onIntentStarted(willAnimate)
335         }
336     }
337 
338     /**
339      * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a
340      * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful
341      * for Java caller starting a [PendingIntent].
342      *
343      * If possible, you should pass the [packageName] of the intent that will be started so that
344      * trampoline activity launches will also be animated.
345      */
346     @Throws(PendingIntent.CanceledException::class)
347     @JvmOverloads
startPendingIntentWithAnimationnull348     fun startPendingIntentWithAnimation(
349         controller: Controller?,
350         animate: Boolean = true,
351         packageName: String? = null,
352         showOverLockscreen: Boolean = false,
353         intentStarter: PendingIntentStarter
354     ) {
355         startIntentWithAnimation(controller, animate, packageName, showOverLockscreen) {
356             intentStarter.startPendingIntent(it)
357         }
358     }
359 
360     /**
361      * Uses [transitionRegister] to set up the return animation for the given [launchController].
362      *
363      * De-registration is set up automatically once the return animation is run.
364      *
365      * TODO(b/339194555): automatically de-register when the launchable is detached.
366      */
registerEphemeralReturnAnimationnull367     private fun registerEphemeralReturnAnimation(
368         launchController: Controller,
369         transitionRegister: TransitionRegister?
370     ) {
371         if (!returnAnimationFrameworkLibrary()) return
372 
373         var cleanUpRunnable: Runnable? = null
374         val returnRunner =
375             createRunner(
376                 object : DelegateTransitionAnimatorController(launchController) {
377                     override val isLaunching = false
378 
379                     override fun onTransitionAnimationCancelled(
380                         newKeyguardOccludedState: Boolean?
381                     ) {
382                         super.onTransitionAnimationCancelled(newKeyguardOccludedState)
383                         cleanUp()
384                     }
385 
386                     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
387                         super.onTransitionAnimationEnd(isExpandingFullyAbove)
388                         cleanUp()
389                     }
390 
391                     private fun cleanUp() {
392                         cleanUpRunnable?.run()
393                     }
394                 }
395             )
396 
397         // mTypeSet and mModes match back signals only, and not home. This is on purpose, because
398         // we only want ephemeral return animations triggered in these scenarios.
399         val filter =
400             TransitionFilter().apply {
401                 mTypeSet = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
402                 mRequirements =
403                     arrayOf(
404                         TransitionFilter.Requirement().apply {
405                             mLaunchCookie = launchController.transitionCookie
406                             mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
407                         }
408                     )
409             }
410         val transition =
411             RemoteTransition(
412                 RemoteAnimationRunnerCompat.wrap(returnRunner),
413                 "${launchController.transitionCookie}_returnTransition"
414             )
415 
416         transitionRegister?.register(filter, transition)
417         cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) }
418     }
419 
420     /** Add a [Listener] that can listen to transition animations. */
addListenernull421     fun addListener(listener: Listener) {
422         listeners.add(listener)
423     }
424 
425     /** Remove a [Listener]. */
removeListenernull426     fun removeListener(listener: Listener) {
427         listeners.remove(listener)
428     }
429 
430     /** Create a new animation [Runner] controlled by [controller]. */
431     @VisibleForTesting
createRunnernull432     fun createRunner(controller: Controller): Runner {
433         // Make sure we use the modified timings when animating a dialog into an app.
434         val transitionAnimator =
435             if (controller.isDialogLaunch) {
436                 dialogToAppAnimator
437             } else {
438                 transitionAnimator
439             }
440 
441         return Runner(controller, callback!!, transitionAnimator, lifecycleListener)
442     }
443 
444     interface PendingIntentStarter {
445         /**
446          * Start a pending intent using the provided [animationAdapter] and return the launch
447          * result.
448          */
449         @Throws(PendingIntent.CanceledException::class)
startPendingIntentnull450         fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int
451     }
452 
453     interface Callback {
454         /** Whether we are currently on the keyguard or not. */
455         fun isOnKeyguard(): Boolean = false
456 
457         /** Hide the keyguard and animate using [runner]. */
458         fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) {
459             throw UnsupportedOperationException()
460         }
461 
462         /* Get the background color of [task]. */
463         fun getBackgroundColor(task: TaskInfo): Int
464     }
465 
466     interface Listener {
467         /** Called when an activity transition animation started. */
onTransitionAnimationStartnull468         fun onTransitionAnimationStart() {}
469 
470         /**
471          * Called when an activity transition animation is finished. This will be called if and only
472          * if [onTransitionAnimationStart] was called earlier.
473          */
onTransitionAnimationEndnull474         fun onTransitionAnimationEnd() {}
475 
476         /**
477          * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called
478          * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was
479          * called before the cancellation.
480          */
onTransitionAnimationCancellednull481         fun onTransitionAnimationCancelled() {}
482 
483         /** Called when an activity transition animation made progress. */
onTransitionAnimationProgressnull484         fun onTransitionAnimationProgress(linearProgress: Float) {}
485     }
486 
487     /**
488      * A controller that takes care of applying the animation to an expanding view.
489      *
490      * Note that all callbacks (onXXX methods) are all called on the main thread.
491      */
492     interface Controller : TransitionAnimator.Controller {
493         companion object {
494             /**
495              * Return a [Controller] that will animate and expand [view] into the opening window.
496              *
497              * Important: The view must be attached to a [ViewGroup] when calling this function and
498              * during the animation. For safety, this method will return null when it is not. The
499              * view must also implement [LaunchableView], otherwise this method will throw.
500              *
501              * Note: The background of [view] should be a (rounded) rectangle so that it can be
502              * properly animated.
503              */
504             @JvmOverloads
505             @JvmStatic
fromViewnull506             fun fromView(
507                 view: View,
508                 cujType: Int? = null,
509                 cookie: TransitionCookie? = null,
510                 component: ComponentName? = null,
511                 returnCujType: Int? = null
512             ): Controller? {
513                 // Make sure the View we launch from implements LaunchableView to avoid visibility
514                 // issues.
515                 if (view !is LaunchableView) {
516                     throw IllegalArgumentException(
517                         "An ActivityTransitionAnimator.Controller was created from a View that " +
518                             "does not implement LaunchableView. This can lead to subtle bugs " +
519                             "where the visibility of the View we are launching from is not what " +
520                             "we expected."
521                     )
522                 }
523 
524                 if (view.parent !is ViewGroup) {
525                     Log.e(
526                         TAG,
527                         "Skipping animation as view $view is not attached to a ViewGroup",
528                         Exception()
529                     )
530                     return null
531                 }
532 
533                 return GhostedViewTransitionAnimatorController(
534                     view,
535                     cujType,
536                     cookie,
537                     component,
538                     returnCujType
539                 )
540             }
541         }
542 
543         /**
544          * Whether this controller is controlling a dialog launch. This will be used to adapt the
545          * timings, making sure we don't show the app until the dialog dim had the time to fade out.
546          */
547         // TODO(b/218989950): Remove this.
548         val isDialogLaunch: Boolean
549             get() = false
550 
551         /**
552          * Whether the expandable controller by this [Controller] is below the window that is going
553          * to be animated.
554          *
555          * This should be `false` when animating an app from or to the shade or status bar, given
556          * that they are drawn above all apps. This is usually `true` when using this animator in a
557          * normal app or a launcher, that are drawn below the animating activity/window.
558          */
559         val isBelowAnimatingWindow: Boolean
560             get() = false
561 
562         /**
563          * The cookie associated with the transition controlled by this [Controller].
564          *
565          * This should be defined for all return [Controller] (when [isLaunching] is false) and for
566          * their associated launch [Controller]s.
567          *
568          * For the recommended format, see [TransitionCookie].
569          */
570         val transitionCookie: TransitionCookie?
571             get() = null
572 
573         /**
574          * The [ComponentName] of the activity whose window is tied to this [Controller].
575          *
576          * This is used as a fallback when a cookie is defined but there is no match (e.g. when a
577          * matching activity was launched by a mean different from the launchable in this
578          * [Controller]), and should be defined for all long-lived registered [Controller]s.
579          */
580         val component: ComponentName?
581             get() = null
582 
583         /**
584          * The intent was started. If [willAnimate] is false, nothing else will happen and the
585          * animation will not be started.
586          */
onIntentStartednull587         fun onIntentStarted(willAnimate: Boolean) {}
588 
589         /**
590          * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called
591          * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was
592          * called before the cancellation.
593          *
594          * If this transition animation affected the occlusion state of the keyguard, WM will
595          * provide us with [newKeyguardOccludedState] so that we can set the occluded state
596          * appropriately.
597          */
onTransitionAnimationCancellednull598         fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
599     }
600 
601     /**
602      * Registers [controller] as a long-lived transition handler for launch and return animations.
603      *
604      * The [controller] will only be used for transitions matching the [TransitionCookie] defined
605      * within it, or the [ComponentName] if the cookie matching fails. Both fields are mandatory for
606      * this registration.
607      */
registernull608     fun register(controller: Controller) {
609         check(returnAnimationFrameworkLibrary()) {
610             "Long-lived registrations cannot be used when the returnAnimationFrameworkLibrary " +
611                 "flag is disabled"
612         }
613 
614         if (transitionRegister == null) {
615             throw IllegalStateException(
616                 "A RemoteTransitionRegister must be provided when creating this animator in " +
617                     "order to use long-lived animations"
618             )
619         }
620 
621         val cookie =
622             controller.transitionCookie
623                 ?: throw IllegalStateException(
624                     "A cookie must be defined in order to use long-lived animations"
625                 )
626         val component =
627             controller.component
628                 ?: throw IllegalStateException(
629                     "A component must be defined in order to use long-lived animations"
630                 )
631 
632         // Make sure that any previous registrations linked to the same cookie are gone.
633         unregister(cookie)
634 
635         val launchFilter =
636             TransitionFilter().apply {
637                 mRequirements =
638                     arrayOf(
639                         TransitionFilter.Requirement().apply {
640                             mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
641                             mModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
642                             mTopActivity = component
643                         }
644                     )
645             }
646         val launchRemoteTransition =
647             RemoteTransition(
648                 RemoteAnimationRunnerCompat.wrap(createRunner(controller)),
649                 "${cookie}_launchTransition"
650             )
651         transitionRegister.register(launchFilter, launchRemoteTransition)
652 
653         val returnController =
654             object : Controller by controller {
655                 override val isLaunching: Boolean = false
656             }
657         val returnFilter =
658             TransitionFilter().apply {
659                 mRequirements =
660                     arrayOf(
661                         TransitionFilter.Requirement().apply {
662                             mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
663                             mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
664                             mTopActivity = component
665                         }
666                     )
667             }
668         val returnRemoteTransition =
669             RemoteTransition(
670                 RemoteAnimationRunnerCompat.wrap(createRunner(returnController)),
671                 "${cookie}_returnTransition"
672             )
673         transitionRegister.register(returnFilter, returnRemoteTransition)
674 
675         longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition)
676     }
677 
678     /** Unregisters all controllers previously registered that contain [cookie]. */
unregisternull679     fun unregister(cookie: TransitionCookie) {
680         val transitions = longLivedTransitions[cookie] ?: return
681         transitionRegister?.unregister(transitions.first)
682         transitionRegister?.unregister(transitions.second)
683         longLivedTransitions.remove(cookie)
684     }
685 
686     /**
687      * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all
688      * events to the passed [delegate].
689      */
690     @VisibleForTesting
691     inner class DelegatingAnimationCompletionListener(
692         private val delegate: Listener?,
693         private val onAnimationComplete: () -> Unit
694     ) : Listener {
695         var cancelled = false
696 
onTransitionAnimationStartnull697         override fun onTransitionAnimationStart() {
698             delegate?.onTransitionAnimationStart()
699         }
700 
onTransitionAnimationProgressnull701         override fun onTransitionAnimationProgress(linearProgress: Float) {
702             delegate?.onTransitionAnimationProgress(linearProgress)
703         }
704 
onTransitionAnimationEndnull705         override fun onTransitionAnimationEnd() {
706             delegate?.onTransitionAnimationEnd()
707             if (!cancelled) {
708                 onAnimationComplete.invoke()
709             }
710         }
711 
onTransitionAnimationCancellednull712         override fun onTransitionAnimationCancelled() {
713             cancelled = true
714             delegate?.onTransitionAnimationCancelled()
715             onAnimationComplete.invoke()
716         }
717     }
718 
719     @VisibleForTesting
720     inner class Runner(
721         controller: Controller,
722         callback: Callback,
723         /** The animator to use to animate the window transition. */
724         transitionAnimator: TransitionAnimator,
725         /** Listener for animation lifecycle events. */
726         listener: Listener? = null
727     ) : IRemoteAnimationRunner.Stub() {
728         // This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
729         // etc.) are possible. So we need to make sure we drop any references that might
730         // transitively cause leaks when we're done with animation.
731         @VisibleForTesting var delegate: AnimationDelegate?
732 
733         init {
734             delegate =
735                 AnimationDelegate(
736                     mainExecutor,
737                     controller,
738                     callback,
739                     DelegatingAnimationCompletionListener(listener, this::dispose),
740                     transitionAnimator,
741                     disableWmTimeout,
742                 )
743         }
744 
745         @BinderThread
onAnimationStartnull746         override fun onAnimationStart(
747             transit: Int,
748             apps: Array<out RemoteAnimationTarget>?,
749             wallpapers: Array<out RemoteAnimationTarget>?,
750             nonApps: Array<out RemoteAnimationTarget>?,
751             finishedCallback: IRemoteAnimationFinishedCallback?
752         ) {
753             val delegate = delegate
754             mainExecutor.execute {
755                 if (delegate == null) {
756                     Log.i(TAG, "onAnimationStart called after completion")
757                     // Animation started too late and timed out already. We need to still
758                     // signal back that we're done with it.
759                     finishedCallback?.onAnimationFinished()
760                 } else {
761                     delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback)
762                 }
763             }
764         }
765 
766         @BinderThread
onAnimationCancellednull767         override fun onAnimationCancelled() {
768             val delegate = delegate
769             mainExecutor.execute {
770                 delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion")
771                 delegate?.onAnimationCancelled()
772             }
773         }
774 
775         @AnyThread
disposenull776         fun dispose() {
777             // Drop references to animation controller once we're done with the animation
778             // to avoid leaking.
779             mainExecutor.execute { delegate = null }
780         }
781     }
782 
783     class AnimationDelegate
784     @JvmOverloads
785     constructor(
786         private val mainExecutor: Executor,
787         private val controller: Controller,
788         private val callback: Callback,
789         /** Listener for animation lifecycle events. */
790         private val listener: Listener? = null,
791         /** The animator to use to animate the window transition. */
792         private val transitionAnimator: TransitionAnimator =
793             defaultTransitionAnimator(mainExecutor),
794 
795         /**
796          * Whether we should disable the WindowManager timeout. This should be set to true in tests
797          * only.
798          */
799         // TODO(b/301385865): Remove this flag.
800         disableWmTimeout: Boolean = false,
801     ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
802         private val transitionContainer = controller.transitionContainer
803         private val context = transitionContainer.context
804         private val transactionApplierView =
805             controller.openingWindowSyncView ?: controller.transitionContainer
806         private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)
807         private val timeoutHandler =
808             if (!disableWmTimeout) {
809                 Handler(Looper.getMainLooper())
810             } else {
811                 null
812             }
813 
814         private val matrix = Matrix()
815         private val invertMatrix = Matrix()
816         private var windowCrop = Rect()
817         private var windowCropF = RectF()
818         private var timedOut = false
819         private var cancelled = false
820         private var animation: TransitionAnimator.Animation? = null
821 
822         /**
823          * A timeout to cancel the transition animation if the remote animation is not started or
824          * cancelled within [TRANSITION_TIMEOUT] milliseconds after the intent was started.
825          *
826          * Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise
827          * it will be automatically converted when posted and we wouldn't be able to remove it after
828          * posting it.
829          */
<lambda>null830         private var onTimeout = Runnable { onAnimationTimedOut() }
831 
832         /**
833          * A long timeout to Log.wtf (signaling a bug in WM) when the remote animation wasn't
834          * started or cancelled within [LONG_TRANSITION_TIMEOUT] milliseconds after the intent was
835          * started.
836          */
<lambda>null837         private var onLongTimeout = Runnable {
838             Log.wtf(
839                 TAG,
840                 "The remote animation was neither cancelled or started within " +
841                     "$LONG_TRANSITION_TIMEOUT"
842             )
843         }
844 
<lambda>null845         init {
846             // We do this check here to cover all entry points, including Launcher which doesn't
847             // call startIntentWithAnimation()
848             if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag()
849         }
850 
851         @UiThread
postTimeoutsnull852         internal fun postTimeouts() {
853             if (timeoutHandler != null) {
854                 timeoutHandler.postDelayed(onTimeout, TRANSITION_TIMEOUT)
855                 timeoutHandler.postDelayed(onLongTimeout, LONG_TRANSITION_TIMEOUT)
856             }
857         }
858 
removeTimeoutsnull859         private fun removeTimeouts() {
860             if (timeoutHandler != null) {
861                 timeoutHandler.removeCallbacks(onTimeout)
862                 timeoutHandler.removeCallbacks(onLongTimeout)
863             }
864         }
865 
866         @UiThread
onAnimationStartnull867         override fun onAnimationStart(
868             @WindowManager.TransitionOldType transit: Int,
869             apps: Array<out RemoteAnimationTarget>?,
870             wallpapers: Array<out RemoteAnimationTarget>?,
871             nonApps: Array<out RemoteAnimationTarget>?,
872             callback: IRemoteAnimationFinishedCallback?
873         ) {
874             removeTimeouts()
875 
876             // The animation was started too late and we already notified the controller that it
877             // timed out.
878             if (timedOut) {
879                 callback?.invoke()
880                 return
881             }
882 
883             // This should not happen, but let's make sure we don't start the animation if it was
884             // cancelled before and we already notified the controller.
885             if (cancelled) {
886                 return
887             }
888 
889             val window = findTargetWindowIfPossible(apps)
890             if (window == null) {
891                 Log.i(TAG, "Aborting the animation as no window is opening")
892                 callback?.invoke()
893 
894                 if (DEBUG_TRANSITION_ANIMATION) {
895                     Log.d(
896                         TAG,
897                         "Calling controller.onTransitionAnimationCancelled() [no window opening]"
898                     )
899                 }
900                 controller.onTransitionAnimationCancelled()
901                 listener?.onTransitionAnimationCancelled()
902                 return
903             }
904 
905             val navigationBar =
906                 nonApps?.firstOrNull {
907                     it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
908                 }
909 
910             startAnimation(window, navigationBar, callback)
911         }
912 
findTargetWindowIfPossiblenull913         private fun findTargetWindowIfPossible(
914             apps: Array<out RemoteAnimationTarget>?
915         ): RemoteAnimationTarget? {
916             if (apps == null) {
917                 return null
918             }
919 
920             val targetMode =
921                 if (controller.isLaunching) {
922                     RemoteAnimationTarget.MODE_OPENING
923                 } else {
924                     RemoteAnimationTarget.MODE_CLOSING
925                 }
926             var candidate: RemoteAnimationTarget? = null
927 
928             for (it in apps) {
929                 if (it.mode == targetMode) {
930                     if (activityTransitionUseLargestWindow()) {
931                         if (returnAnimationFrameworkLibrary()) {
932                             // If the controller contains a cookie, _only_ match if either the
933                             // candidate contains the matching cookie, or a component is also
934                             // defined and is a match.
935                             if (
936                                 controller.transitionCookie != null &&
937                                     it.taskInfo
938                                         ?.launchCookies
939                                         ?.contains(controller.transitionCookie) != true &&
940                                     (controller.component == null ||
941                                         it.taskInfo?.topActivity != controller.component)
942                             ) {
943                                 continue
944                             }
945                         }
946 
947                         if (
948                             candidate == null ||
949                                 !it.hasAnimatingParent && candidate.hasAnimatingParent
950                         ) {
951                             candidate = it
952                             continue
953                         }
954                         if (
955                             !it.hasAnimatingParent &&
956                                 it.screenSpaceBounds.hasGreaterAreaThan(candidate.screenSpaceBounds)
957                         ) {
958                             candidate = it
959                         }
960                     } else {
961                         if (!it.hasAnimatingParent) {
962                             return it
963                         }
964                         if (candidate == null) {
965                             candidate = it
966                         }
967                     }
968                 }
969             }
970 
971             return candidate
972         }
973 
startAnimationnull974         private fun startAnimation(
975             window: RemoteAnimationTarget,
976             navigationBar: RemoteAnimationTarget?,
977             iCallback: IRemoteAnimationFinishedCallback?
978         ) {
979             if (TransitionAnimator.DEBUG) {
980                 Log.d(TAG, "Remote animation started")
981             }
982 
983             val windowBounds = window.screenSpaceBounds
984             val endState =
985                 if (controller.isLaunching) {
986                     TransitionAnimator.State(
987                         top = windowBounds.top,
988                         bottom = windowBounds.bottom,
989                         left = windowBounds.left,
990                         right = windowBounds.right
991                     )
992                 } else {
993                     controller.createAnimatorState()
994                 }
995             val windowBackgroundColor =
996                 if (translucentOccludingActivityFix() && window.isTranslucent) {
997                     Color.TRANSPARENT
998                 } else {
999                     window.taskInfo?.let { callback.getBackgroundColor(it) }
1000                         ?: window.backgroundColor
1001                 }
1002 
1003             // TODO(b/184121838): We should somehow get the top and bottom radius of the window
1004             // instead of recomputing isExpandingFullyAbove here.
1005             val isExpandingFullyAbove =
1006                 transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
1007             if (controller.isLaunching) {
1008                 val endRadius = getWindowRadius(isExpandingFullyAbove)
1009                 endState.topCornerRadius = endRadius
1010                 endState.bottomCornerRadius = endRadius
1011             }
1012 
1013             // We animate the opening window and delegate the view expansion to [this.controller].
1014             val delegate = this.controller
1015             val controller =
1016                 object : Controller by delegate {
1017                     override fun createAnimatorState(): TransitionAnimator.State {
1018                         if (isLaunching) return delegate.createAnimatorState()
1019                         val windowRadius = getWindowRadius(isExpandingFullyAbove)
1020                         return TransitionAnimator.State(
1021                             top = windowBounds.top,
1022                             bottom = windowBounds.bottom,
1023                             left = windowBounds.left,
1024                             right = windowBounds.right,
1025                             topCornerRadius = windowRadius,
1026                             bottomCornerRadius = windowRadius
1027                         )
1028                     }
1029 
1030                     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
1031                         listener?.onTransitionAnimationStart()
1032 
1033                         if (DEBUG_TRANSITION_ANIMATION) {
1034                             Log.d(
1035                                 TAG,
1036                                 "Calling controller.onTransitionAnimationStart(" +
1037                                     "isExpandingFullyAbove=$isExpandingFullyAbove) " +
1038                                     "[controller=$delegate]"
1039                             )
1040                         }
1041                         delegate.onTransitionAnimationStart(isExpandingFullyAbove)
1042                     }
1043 
1044                     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
1045                         listener?.onTransitionAnimationEnd()
1046                         iCallback?.invoke()
1047 
1048                         if (DEBUG_TRANSITION_ANIMATION) {
1049                             Log.d(
1050                                 TAG,
1051                                 "Calling controller.onTransitionAnimationEnd(" +
1052                                     "isExpandingFullyAbove=$isExpandingFullyAbove) " +
1053                                     "[controller=$delegate]"
1054                             )
1055                         }
1056                         delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
1057                     }
1058 
1059                     override fun onTransitionAnimationProgress(
1060                         state: TransitionAnimator.State,
1061                         progress: Float,
1062                         linearProgress: Float
1063                     ) {
1064                         applyStateToWindow(window, state, linearProgress)
1065                         navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
1066 
1067                         listener?.onTransitionAnimationProgress(linearProgress)
1068                         delegate.onTransitionAnimationProgress(state, progress, linearProgress)
1069                     }
1070                 }
1071 
1072             animation =
1073                 transitionAnimator.startAnimation(
1074                     controller,
1075                     endState,
1076                     windowBackgroundColor,
1077                     fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
1078                     drawHole = !controller.isBelowAnimatingWindow,
1079                 )
1080         }
1081 
getWindowRadiusnull1082         private fun getWindowRadius(isExpandingFullyAbove: Boolean): Float {
1083             return if (isExpandingFullyAbove) {
1084                 // Most of the time, expanding fully above the root view means
1085                 // expanding in full screen.
1086                 ScreenDecorationsUtils.getWindowCornerRadius(context)
1087             } else {
1088                 // This usually means we are in split screen mode, so 2 out of 4
1089                 // corners will have a radius of 0.
1090                 0f
1091             }
1092         }
1093 
applyStateToWindownull1094         private fun applyStateToWindow(
1095             window: RemoteAnimationTarget,
1096             state: TransitionAnimator.State,
1097             linearProgress: Float,
1098         ) {
1099             if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) {
1100                 // Don't apply any transaction if the view root we synchronize with was detached or
1101                 // if the SurfaceControl associated with [window] is not valid, as
1102                 // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw.
1103                 return
1104             }
1105 
1106             val screenBounds = window.screenSpaceBounds
1107             val centerX = (screenBounds.left + screenBounds.right) / 2f
1108             val centerY = (screenBounds.top + screenBounds.bottom) / 2f
1109             val width = screenBounds.right - screenBounds.left
1110             val height = screenBounds.bottom - screenBounds.top
1111 
1112             // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no
1113             // blank space on any side.
1114             val widthRatio = state.width.toFloat() / width
1115             val heightRatio = state.height.toFloat() / height
1116             val scale = maxOf(widthRatio, heightRatio)
1117             matrix.reset()
1118             matrix.setScale(scale, scale, centerX, centerY)
1119 
1120             // Align it to the top and center it in the x-axis.
1121             val heightChange = height * scale - height
1122             val translationX = state.centerX - centerX
1123             val translationY = state.top - screenBounds.top + heightChange / 2f
1124             matrix.postTranslate(translationX, translationY)
1125 
1126             // Crop it. The matrix will also be applied to the crop, so we apply the inverse
1127             // operation. Given that we only scale (by factor > 0) then translate, we can assume
1128             // that the matrix is invertible.
1129             val cropX = state.left.toFloat() - screenBounds.left
1130             val cropY = state.top.toFloat() - screenBounds.top
1131             windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height)
1132             matrix.invert(invertMatrix)
1133             invertMatrix.mapRect(windowCropF)
1134             windowCrop.set(
1135                 windowCropF.left.roundToInt(),
1136                 windowCropF.top.roundToInt(),
1137                 windowCropF.right.roundToInt(),
1138                 windowCropF.bottom.roundToInt()
1139             )
1140 
1141             val windowAnimationDelay =
1142                 if (controller.isLaunching) {
1143                     TIMINGS.contentAfterFadeInDelay
1144                 } else {
1145                     TIMINGS.contentBeforeFadeOutDelay
1146                 }
1147             val windowAnimationDuration =
1148                 if (controller.isLaunching) {
1149                     TIMINGS.contentAfterFadeInDuration
1150                 } else {
1151                     TIMINGS.contentBeforeFadeOutDuration
1152                 }
1153             val windowProgress =
1154                 TransitionAnimator.getProgress(
1155                     TIMINGS,
1156                     linearProgress,
1157                     windowAnimationDelay,
1158                     windowAnimationDuration
1159                 )
1160 
1161             // The alpha of the opening window. If it opens above the expandable, then it should
1162             // fade in progressively. Otherwise, it should be fully opaque and will be progressively
1163             // revealed as the window background color layer above the window fades out.
1164             val alpha =
1165                 if (controller.isBelowAnimatingWindow) {
1166                     if (controller.isLaunching) {
1167                         INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(
1168                             windowProgress
1169                         )
1170                     } else {
1171                         1 -
1172                             INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation(
1173                                 windowProgress
1174                             )
1175                     }
1176                 } else {
1177                     1f
1178                 }
1179 
1180             // The scale will also be applied to the corner radius, so we divide by the scale to
1181             // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to
1182             // make sure that the window does not draw itself behind the expanding view. This is
1183             // especially important for lock screen animations, where the window is not clipped by
1184             // the shade.
1185             val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale
1186             val params =
1187                 SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
1188                     .withAlpha(alpha)
1189                     .withMatrix(matrix)
1190                     .withWindowCrop(windowCrop)
1191                     .withCornerRadius(cornerRadius)
1192                     .withVisibility(true)
1193                     .build()
1194 
1195             transactionApplier.scheduleApply(params)
1196         }
1197 
applyStateToNavigationBarnull1198         private fun applyStateToNavigationBar(
1199             navigationBar: RemoteAnimationTarget,
1200             state: TransitionAnimator.State,
1201             linearProgress: Float
1202         ) {
1203             if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) {
1204                 // Don't apply any transaction if the view root we synchronize with was detached or
1205                 // if the SurfaceControl associated with [navigationBar] is not valid, as
1206                 // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw.
1207                 return
1208             }
1209 
1210             val fadeInProgress =
1211                 TransitionAnimator.getProgress(
1212                     TIMINGS,
1213                     linearProgress,
1214                     ANIMATION_DELAY_NAV_FADE_IN,
1215                     ANIMATION_DURATION_NAV_FADE_OUT
1216                 )
1217 
1218             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
1219             if (fadeInProgress > 0) {
1220                 matrix.reset()
1221                 matrix.setTranslate(
1222                     0f,
1223                     (state.top - navigationBar.sourceContainerBounds.top).toFloat()
1224                 )
1225                 windowCrop.set(state.left, 0, state.right, state.height)
1226                 params
1227                     .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
1228                     .withMatrix(matrix)
1229                     .withWindowCrop(windowCrop)
1230                     .withVisibility(true)
1231             } else {
1232                 val fadeOutProgress =
1233                     TransitionAnimator.getProgress(
1234                         TIMINGS,
1235                         linearProgress,
1236                         0,
1237                         ANIMATION_DURATION_NAV_FADE_OUT
1238                     )
1239                 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
1240             }
1241 
1242             transactionApplier.scheduleApply(params.build())
1243         }
1244 
onAnimationTimedOutnull1245         private fun onAnimationTimedOut() {
1246             // The remote animation was cancelled by WM, so we already cancelled the transition
1247             // animation.
1248             if (cancelled) {
1249                 return
1250             }
1251 
1252             Log.w(TAG, "Remote animation timed out")
1253             timedOut = true
1254 
1255             if (DEBUG_TRANSITION_ANIMATION) {
1256                 Log.d(
1257                     TAG,
1258                     "Calling controller.onTransitionAnimationCancelled() [animation timed out]"
1259                 )
1260             }
1261             controller.onTransitionAnimationCancelled()
1262             listener?.onTransitionAnimationCancelled()
1263         }
1264 
1265         @UiThread
onAnimationCancellednull1266         override fun onAnimationCancelled() {
1267             removeTimeouts()
1268 
1269             // The short timeout happened, so we already cancelled the transition animation.
1270             if (timedOut) {
1271                 return
1272             }
1273 
1274             Log.i(TAG, "Remote animation was cancelled")
1275             cancelled = true
1276 
1277             animation?.cancel()
1278 
1279             if (DEBUG_TRANSITION_ANIMATION) {
1280                 Log.d(
1281                     TAG,
1282                     "Calling controller.onTransitionAnimationCancelled() [remote animation " +
1283                         "cancelled]",
1284                 )
1285             }
1286             controller.onTransitionAnimationCancelled()
1287             listener?.onTransitionAnimationCancelled()
1288         }
1289 
invokenull1290         private fun IRemoteAnimationFinishedCallback.invoke() {
1291             try {
1292                 onAnimationFinished()
1293             } catch (e: RemoteException) {
1294                 e.printStackTrace()
1295             }
1296         }
1297 
Rectnull1298         private fun Rect.hasGreaterAreaThan(other: Rect): Boolean {
1299             return (this.width() * this.height()) > (other.width() * other.height())
1300         }
1301     }
1302 
1303     /**
1304      * Wraps one of the two methods we have to register remote transitions with WM Shell:
1305      * - for in-process registrations (e.g. System UI) we use [ShellTransitions]
1306      * - for cross-process registrations (e.g. Launcher) we use [IShellTransitions]
1307      *
1308      * Important: each instance of this class must wrap exactly one of the two.
1309      */
1310     class TransitionRegister
1311     private constructor(
1312         private val shellTransitions: ShellTransitions? = null,
1313         private val iShellTransitions: IShellTransitions? = null,
1314     ) {
1315         init {
1316             assert((shellTransitions != null).xor(iShellTransitions != null))
1317         }
1318 
1319         companion object {
1320             /** Provides a [TransitionRegister] instance wrapping [ShellTransitions]. */
fromShellTransitionsnull1321             fun fromShellTransitions(shellTransitions: ShellTransitions): TransitionRegister {
1322                 return TransitionRegister(shellTransitions = shellTransitions)
1323             }
1324 
1325             /** Provides a [TransitionRegister] instance wrapping [IShellTransitions]. */
fromIShellTransitionsnull1326             fun fromIShellTransitions(iShellTransitions: IShellTransitions): TransitionRegister {
1327                 return TransitionRegister(iShellTransitions = iShellTransitions)
1328             }
1329         }
1330 
1331         /** Register [remoteTransition] with WM Shell using the given [filter]. */
registernull1332         internal fun register(
1333             filter: TransitionFilter,
1334             remoteTransition: RemoteTransition,
1335         ) {
1336             shellTransitions?.registerRemote(filter, remoteTransition)
1337             iShellTransitions?.registerRemote(filter, remoteTransition)
1338         }
1339 
1340         /** Unregister [remoteTransition] from WM Shell. */
unregisternull1341         internal fun unregister(remoteTransition: RemoteTransition) {
1342             shellTransitions?.unregisterRemote(remoteTransition)
1343             iShellTransitions?.unregisterRemote(remoteTransition)
1344         }
1345     }
1346 
1347     /**
1348      * A cookie used to uniquely identify a task launched using an
1349      * [ActivityTransitionAnimator.Controller].
1350      *
1351      * The [String] encapsulated by this class should be formatted in such a way to be unique across
1352      * the system, but reliably constant for the same associated launchable.
1353      *
1354      * Recommended naming scheme:
1355      * - DO use the fully qualified name of the class that owns the instance of the launchable,
1356      *   along with a concise and precise description of the purpose of the launchable in question.
1357      * - DO NOT introduce uniqueness through the use of timestamps or other runtime variables that
1358      *   will change if the instance is destroyed and re-created.
1359      *
1360      * Example: "com.not.the.real.class.name.ShadeController_openSettingsButton"
1361      *
1362      * Note that sometimes (e.g. in recycler views) there could be multiple instances of the same
1363      * launchable, and no static knowledge to adequately differentiate between them using a single
1364      * description. In this case, the recommendation is to append a unique identifier related to the
1365      * contents of the launchable.
1366      *
1367      * Example: “com.not.the.real.class.name.ToastWebResult_launchAga_id143256”
1368      */
1369     data class TransitionCookie(private val cookie: String) : Binder()
1370 }
1371