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