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.graphics.Matrix 24 import android.graphics.Path 25 import android.graphics.Rect 26 import android.graphics.RectF 27 import android.os.Build 28 import android.os.Handler 29 import android.os.Looper 30 import android.os.RemoteException 31 import android.util.Log 32 import android.view.IRemoteAnimationFinishedCallback 33 import android.view.IRemoteAnimationRunner 34 import android.view.RemoteAnimationAdapter 35 import android.view.RemoteAnimationTarget 36 import android.view.SyncRtSurfaceTransactionApplier 37 import android.view.View 38 import android.view.ViewGroup 39 import android.view.WindowManager 40 import android.view.animation.Interpolator 41 import android.view.animation.PathInterpolator 42 import androidx.annotation.AnyThread 43 import androidx.annotation.BinderThread 44 import androidx.annotation.UiThread 45 import com.android.app.animation.Interpolators 46 import com.android.internal.annotations.VisibleForTesting 47 import com.android.internal.policy.ScreenDecorationsUtils 48 import kotlin.math.roundToInt 49 50 private const val TAG = "ActivityLaunchAnimator" 51 52 /** 53 * A class that allows activities to be started in a seamless way from a view that is transforming 54 * nicely into the starting window. 55 */ 56 class ActivityLaunchAnimator( 57 /** The animator used when animating a View into an app. */ 58 private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, 59 60 /** The animator used when animating a Dialog into an app. */ 61 // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to 62 // TIMINGS.contentBeforeFadeOutDuration. 63 private val dialogToAppAnimator: LaunchAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR, 64 65 /** 66 * Whether we should disable the WindowManager timeout. This should be set to true in tests 67 * only. 68 */ 69 // TODO(b/301385865): Remove this flag. 70 private val disableWmTimeout: Boolean = false, 71 ) { 72 companion object { 73 /** The timings when animating a View into an app. */ 74 @JvmField 75 val TIMINGS = 76 LaunchAnimator.Timings( 77 totalDuration = 500L, 78 contentBeforeFadeOutDelay = 0L, 79 contentBeforeFadeOutDuration = 150L, 80 contentAfterFadeInDelay = 150L, 81 contentAfterFadeInDuration = 183L 82 ) 83 84 /** 85 * The timings when animating a Dialog into an app. We need to wait at least 200ms before 86 * showing the app (which is under the dialog window) so that the dialog window dim is fully 87 * faded out, to avoid flicker. 88 */ 89 val DIALOG_TIMINGS = 90 TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L) 91 92 /** The interpolators when animating a View or a dialog into an app. */ 93 val INTERPOLATORS = 94 LaunchAnimator.Interpolators( 95 positionInterpolator = Interpolators.EMPHASIZED, 96 positionXInterpolator = createPositionXInterpolator(), 97 contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, 98 contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) 99 ) 100 101 // TODO(b/288507023): Remove this flag. 102 @JvmField val DEBUG_LAUNCH_ANIMATION = Build.IS_DEBUGGABLE 103 104 private val DEFAULT_LAUNCH_ANIMATOR = LaunchAnimator(TIMINGS, INTERPOLATORS) 105 private val DEFAULT_DIALOG_TO_APP_ANIMATOR = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS) 106 107 /** Durations & interpolators for the navigation bar fading in & out. */ 108 private const val ANIMATION_DURATION_NAV_FADE_IN = 266L 109 private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L 110 private val ANIMATION_DELAY_NAV_FADE_IN = 111 TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN 112 113 private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE 114 private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) 115 116 /** The time we wait before timing out the remote animation after starting the intent. */ 117 private const val LAUNCH_TIMEOUT = 1000L 118 119 private fun createPositionXInterpolator(): Interpolator { 120 val path = 121 Path().apply { 122 moveTo(0f, 0f) 123 cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f) 124 cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f) 125 } 126 return PathInterpolator(path) 127 } 128 } 129 130 /** 131 * The callback of this animator. This should be set before any call to 132 * [start(Pending)IntentWithAnimation]. 133 */ 134 var callback: Callback? = null 135 136 /** The set of [Listener] that should be notified of any animation started by this animator. */ 137 private val listeners = LinkedHashSet<Listener>() 138 139 /** Top-level listener that can be used to notify all registered [listeners]. */ 140 private val lifecycleListener = 141 object : Listener { 142 override fun onLaunchAnimationStart() { 143 listeners.forEach { it.onLaunchAnimationStart() } 144 } 145 146 override fun onLaunchAnimationEnd() { 147 listeners.forEach { it.onLaunchAnimationEnd() } 148 } 149 150 override fun onLaunchAnimationProgress(linearProgress: Float) { 151 listeners.forEach { it.onLaunchAnimationProgress(linearProgress) } 152 } 153 154 override fun onLaunchAnimationCancelled() { 155 listeners.forEach { it.onLaunchAnimationCancelled() } 156 } 157 } 158 159 /** 160 * Start an intent and animate the opening window. The intent will be started by running 161 * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch 162 * result. [controller] is responsible from animating the view from which the intent was started 163 * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window 164 * opening. 165 * 166 * If [controller] is null or [animate] is false, then the intent will be started and no 167 * animation will run. 168 * 169 * If possible, you should pass the [packageName] of the intent that will be started so that 170 * trampoline activity launches will also be animated. 171 * 172 * If the device is currently locked, the user will have to unlock it before the intent is 173 * started unless [showOverLockscreen] is true. In that case, the activity will be started 174 * directly over the lockscreen. 175 * 176 * This method will throw any exception thrown by [intentStarter]. 177 */ 178 @JvmOverloads 179 fun startIntentWithAnimation( 180 controller: Controller?, 181 animate: Boolean = true, 182 packageName: String? = null, 183 showOverLockscreen: Boolean = false, 184 intentStarter: (RemoteAnimationAdapter?) -> Int 185 ) { 186 if (controller == null || !animate) { 187 Log.i(TAG, "Starting intent with no animation") 188 intentStarter(null) 189 controller?.callOnIntentStartedOnMainThread(willAnimate = false) 190 return 191 } 192 193 val callback = 194 this.callback 195 ?: throw IllegalStateException( 196 "ActivityLaunchAnimator.callback must be set before using this animator" 197 ) 198 val runner = createRunner(controller) 199 val runnerDelegate = runner.delegate!! 200 val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen 201 202 // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the 203 // keyguard with the animation 204 val animationAdapter = 205 if (!hideKeyguardWithAnimation) { 206 RemoteAnimationAdapter( 207 runner, 208 TIMINGS.totalDuration, 209 TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ 210 ) 211 } else { 212 null 213 } 214 215 // Register the remote animation for the given package to also animate trampoline 216 // activity launches. 217 if (packageName != null && animationAdapter != null) { 218 try { 219 ActivityTaskManager.getService() 220 .registerRemoteAnimationForNextActivityStart( 221 packageName, 222 animationAdapter, 223 null /* launchCookie */ 224 ) 225 } catch (e: RemoteException) { 226 Log.w(TAG, "Unable to register the remote animation", e) 227 } 228 } 229 230 val launchResult = intentStarter(animationAdapter) 231 232 // Only animate if the app is not already on top and will be opened, unless we are on the 233 // keyguard. 234 val willAnimate = 235 launchResult == ActivityManager.START_TASK_TO_FRONT || 236 launchResult == ActivityManager.START_SUCCESS || 237 (launchResult == ActivityManager.START_DELIVERED_TO_TOP && 238 hideKeyguardWithAnimation) 239 240 Log.i( 241 TAG, 242 "launchResult=$launchResult willAnimate=$willAnimate " + 243 "hideKeyguardWithAnimation=$hideKeyguardWithAnimation" 244 ) 245 controller.callOnIntentStartedOnMainThread(willAnimate) 246 247 // If we expect an animation, post a timeout to cancel it in case the remote animation is 248 // never started. 249 if (willAnimate) { 250 runnerDelegate.postTimeout() 251 252 // Hide the keyguard using the launch animation instead of the default unlock animation. 253 if (hideKeyguardWithAnimation) { 254 callback.hideKeyguardWithAnimation(runner) 255 } 256 } else { 257 // We need to make sure delegate references are dropped to avoid memory leaks. 258 runner.dispose() 259 } 260 } 261 262 private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { 263 if (Looper.myLooper() != Looper.getMainLooper()) { 264 this.launchContainer.context.mainExecutor.execute { 265 callOnIntentStartedOnMainThread(willAnimate) 266 } 267 } else { 268 if (DEBUG_LAUNCH_ANIMATION) { 269 Log.d( 270 TAG, 271 "Calling controller.onIntentStarted(willAnimate=$willAnimate) " + 272 "[controller=$this]" 273 ) 274 } 275 this.onIntentStarted(willAnimate) 276 } 277 } 278 279 /** 280 * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a 281 * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful 282 * for Java caller starting a [PendingIntent]. 283 * 284 * If possible, you should pass the [packageName] of the intent that will be started so that 285 * trampoline activity launches will also be animated. 286 */ 287 @Throws(PendingIntent.CanceledException::class) 288 @JvmOverloads 289 fun startPendingIntentWithAnimation( 290 controller: Controller?, 291 animate: Boolean = true, 292 packageName: String? = null, 293 intentStarter: PendingIntentStarter 294 ) { 295 startIntentWithAnimation(controller, animate, packageName) { 296 intentStarter.startPendingIntent(it) 297 } 298 } 299 300 /** Add a [Listener] that can listen to launch animations. */ 301 fun addListener(listener: Listener) { 302 listeners.add(listener) 303 } 304 305 /** Remove a [Listener]. */ 306 fun removeListener(listener: Listener) { 307 listeners.remove(listener) 308 } 309 310 /** Create a new animation [Runner] controlled by [controller]. */ 311 @VisibleForTesting 312 fun createRunner(controller: Controller): Runner { 313 // Make sure we use the modified timings when animating a dialog into an app. 314 val launchAnimator = 315 if (controller.isDialogLaunch) { 316 dialogToAppAnimator 317 } else { 318 launchAnimator 319 } 320 321 return Runner(controller, callback!!, launchAnimator, lifecycleListener) 322 } 323 324 interface PendingIntentStarter { 325 /** 326 * Start a pending intent using the provided [animationAdapter] and return the launch 327 * result. 328 */ 329 @Throws(PendingIntent.CanceledException::class) 330 fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int 331 } 332 333 interface Callback { 334 /** Whether we are currently on the keyguard or not. */ 335 @JvmDefault fun isOnKeyguard(): Boolean = false 336 337 /** Hide the keyguard and animate using [runner]. */ 338 @JvmDefault 339 fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) { 340 throw UnsupportedOperationException() 341 } 342 343 /* Get the background color of [task]. */ 344 fun getBackgroundColor(task: TaskInfo): Int 345 } 346 347 interface Listener { 348 /** Called when an activity launch animation started. */ 349 @JvmDefault fun onLaunchAnimationStart() {} 350 351 /** 352 * Called when an activity launch animation is finished. This will be called if and only if 353 * [onLaunchAnimationStart] was called earlier. 354 */ 355 @JvmDefault fun onLaunchAnimationEnd() {} 356 357 /** 358 * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after 359 * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called 360 * before the cancellation. 361 */ 362 fun onLaunchAnimationCancelled() {} 363 364 /** Called when an activity launch animation made progress. */ 365 @JvmDefault fun onLaunchAnimationProgress(linearProgress: Float) {} 366 } 367 368 /** 369 * A controller that takes care of applying the animation to an expanding view. 370 * 371 * Note that all callbacks (onXXX methods) are all called on the main thread. 372 */ 373 interface Controller : LaunchAnimator.Controller { 374 companion object { 375 /** 376 * Return a [Controller] that will animate and expand [view] into the opening window. 377 * 378 * Important: The view must be attached to a [ViewGroup] when calling this function and 379 * during the animation. For safety, this method will return null when it is not. The 380 * view must also implement [LaunchableView], otherwise this method will throw. 381 * 382 * Note: The background of [view] should be a (rounded) rectangle so that it can be 383 * properly animated. 384 */ 385 @JvmStatic 386 fun fromView(view: View, cujType: Int? = null): Controller? { 387 // Make sure the View we launch from implements LaunchableView to avoid visibility 388 // issues. 389 if (view !is LaunchableView) { 390 throw IllegalArgumentException( 391 "An ActivityLaunchAnimator.Controller was created from a View that does " + 392 "not implement LaunchableView. This can lead to subtle bugs where the" + 393 " visibility of the View we are launching from is not what we expected." 394 ) 395 } 396 397 if (view.parent !is ViewGroup) { 398 Log.e( 399 TAG, 400 "Skipping animation as view $view is not attached to a ViewGroup", 401 Exception() 402 ) 403 return null 404 } 405 406 return GhostedViewLaunchAnimatorController(view, cujType) 407 } 408 } 409 410 /** 411 * Whether this controller is controlling a dialog launch. This will be used to adapt the 412 * timings, making sure we don't show the app until the dialog dim had the time to fade out. 413 */ 414 // TODO(b/218989950): Remove this. 415 val isDialogLaunch: Boolean 416 get() = false 417 418 /** 419 * Whether the expandable controller by this [Controller] is below the launching window that 420 * is going to be animated. 421 * 422 * This should be `false` when launching an app from the shade or status bar, given that 423 * they are drawn above all apps. This is usually `true` when using this launcher in a 424 * normal app or a launcher, that are drawn below the animating activity/window. 425 */ 426 val isBelowAnimatingWindow: Boolean 427 get() = false 428 429 /** 430 * The intent was started. If [willAnimate] is false, nothing else will happen and the 431 * animation will not be started. 432 */ 433 fun onIntentStarted(willAnimate: Boolean) {} 434 435 /** 436 * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after 437 * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called 438 * before the cancellation. 439 * 440 * If this launch animation affected the occlusion state of the keyguard, WM will provide us 441 * with [newKeyguardOccludedState] so that we can set the occluded state appropriately. 442 */ 443 fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {} 444 } 445 446 /** 447 * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all 448 * events to the passed [delegate]. 449 */ 450 @VisibleForTesting 451 inner class DelegatingAnimationCompletionListener( 452 private val delegate: Listener?, 453 private val onAnimationComplete: () -> Unit 454 ) : Listener { 455 var cancelled = false 456 457 override fun onLaunchAnimationStart() { 458 delegate?.onLaunchAnimationStart() 459 } 460 461 override fun onLaunchAnimationProgress(linearProgress: Float) { 462 delegate?.onLaunchAnimationProgress(linearProgress) 463 } 464 465 override fun onLaunchAnimationEnd() { 466 delegate?.onLaunchAnimationEnd() 467 if (!cancelled) { 468 onAnimationComplete.invoke() 469 } 470 } 471 472 override fun onLaunchAnimationCancelled() { 473 cancelled = true 474 delegate?.onLaunchAnimationCancelled() 475 onAnimationComplete.invoke() 476 } 477 } 478 479 @VisibleForTesting 480 inner class Runner( 481 controller: Controller, 482 callback: Callback, 483 /** The animator to use to animate the window launch. */ 484 launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, 485 /** Listener for animation lifecycle events. */ 486 listener: Listener? = null 487 ) : IRemoteAnimationRunner.Stub() { 488 private val context = controller.launchContainer.context 489 490 // This is being passed across IPC boundaries and cycles (through PendingIntentRecords, 491 // etc.) are possible. So we need to make sure we drop any references that might 492 // transitively cause leaks when we're done with animation. 493 @VisibleForTesting var delegate: AnimationDelegate? 494 495 init { 496 delegate = 497 AnimationDelegate( 498 controller, 499 callback, 500 DelegatingAnimationCompletionListener(listener, this::dispose), 501 launchAnimator, 502 disableWmTimeout 503 ) 504 } 505 506 @BinderThread 507 override fun onAnimationStart( 508 transit: Int, 509 apps: Array<out RemoteAnimationTarget>?, 510 wallpapers: Array<out RemoteAnimationTarget>?, 511 nonApps: Array<out RemoteAnimationTarget>?, 512 finishedCallback: IRemoteAnimationFinishedCallback? 513 ) { 514 val delegate = delegate 515 context.mainExecutor.execute { 516 if (delegate == null) { 517 Log.i(TAG, "onAnimationStart called after completion") 518 // Animation started too late and timed out already. We need to still 519 // signal back that we're done with it. 520 finishedCallback?.onAnimationFinished() 521 } else { 522 delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) 523 } 524 } 525 } 526 527 @BinderThread 528 override fun onAnimationCancelled() { 529 val delegate = delegate 530 context.mainExecutor.execute { 531 delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion") 532 delegate?.onAnimationCancelled() 533 } 534 } 535 536 @AnyThread 537 fun dispose() { 538 // Drop references to animation controller once we're done with the animation 539 // to avoid leaking. 540 context.mainExecutor.execute { delegate = null } 541 } 542 } 543 544 class AnimationDelegate 545 @JvmOverloads 546 constructor( 547 private val controller: Controller, 548 private val callback: Callback, 549 /** Listener for animation lifecycle events. */ 550 private val listener: Listener? = null, 551 /** The animator to use to animate the window launch. */ 552 private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, 553 554 /** 555 * Whether we should disable the WindowManager timeout. This should be set to true in tests 556 * only. 557 */ 558 // TODO(b/301385865): Remove this flag. 559 disableWmTimeout: Boolean = false, 560 ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { 561 private val launchContainer = controller.launchContainer 562 private val context = launchContainer.context 563 private val transactionApplierView = 564 controller.openingWindowSyncView ?: controller.launchContainer 565 private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView) 566 private val timeoutHandler = 567 if (!disableWmTimeout) { 568 Handler(Looper.getMainLooper()) 569 } else { 570 null 571 } 572 573 private val matrix = Matrix() 574 private val invertMatrix = Matrix() 575 private var windowCrop = Rect() 576 private var windowCropF = RectF() 577 private var timedOut = false 578 private var cancelled = false 579 private var animation: LaunchAnimator.Animation? = null 580 581 // A timeout to cancel the remote animation if it is not started within X milliseconds after 582 // the intent was started. 583 // 584 // Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise 585 // it will be automatically converted when posted and we wouldn't be able to remove it after 586 // posting it. 587 private var onTimeout = Runnable { onAnimationTimedOut() } 588 589 @UiThread 590 internal fun postTimeout() { 591 timeoutHandler?.postDelayed(onTimeout, LAUNCH_TIMEOUT) 592 } 593 594 private fun removeTimeout() { 595 timeoutHandler?.removeCallbacks(onTimeout) 596 } 597 598 @UiThread 599 override fun onAnimationStart( 600 @WindowManager.TransitionOldType transit: Int, 601 apps: Array<out RemoteAnimationTarget>?, 602 wallpapers: Array<out RemoteAnimationTarget>?, 603 nonApps: Array<out RemoteAnimationTarget>?, 604 callback: IRemoteAnimationFinishedCallback? 605 ) { 606 removeTimeout() 607 608 // The animation was started too late and we already notified the controller that it 609 // timed out. 610 if (timedOut) { 611 callback?.invoke() 612 return 613 } 614 615 // This should not happen, but let's make sure we don't start the animation if it was 616 // cancelled before and we already notified the controller. 617 if (cancelled) { 618 return 619 } 620 621 startAnimation(apps, nonApps, callback) 622 } 623 624 private fun findRootTaskIfPossible( 625 apps: Array<out RemoteAnimationTarget>? 626 ): RemoteAnimationTarget? { 627 if (apps == null) { 628 return null 629 } 630 var candidate: RemoteAnimationTarget? = null 631 for (it in apps) { 632 if (it.mode == RemoteAnimationTarget.MODE_OPENING) { 633 if (it.taskInfo != null && !it.hasAnimatingParent) { 634 return it 635 } 636 if (candidate == null) { 637 candidate = it 638 } 639 } 640 } 641 return candidate 642 } 643 644 private fun startAnimation( 645 apps: Array<out RemoteAnimationTarget>?, 646 nonApps: Array<out RemoteAnimationTarget>?, 647 iCallback: IRemoteAnimationFinishedCallback? 648 ) { 649 if (LaunchAnimator.DEBUG) { 650 Log.d(TAG, "Remote animation started") 651 } 652 653 val window = findRootTaskIfPossible(apps) 654 if (window == null) { 655 Log.i(TAG, "Aborting the animation as no window is opening") 656 removeTimeout() 657 iCallback?.invoke() 658 659 if (DEBUG_LAUNCH_ANIMATION) { 660 Log.d( 661 TAG, 662 "Calling controller.onLaunchAnimationCancelled() [no window opening]" 663 ) 664 } 665 controller.onLaunchAnimationCancelled() 666 listener?.onLaunchAnimationCancelled() 667 return 668 } 669 670 val navigationBar = 671 nonApps?.firstOrNull { 672 it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR 673 } 674 675 val windowBounds = window.screenSpaceBounds 676 val endState = 677 LaunchAnimator.State( 678 top = windowBounds.top, 679 bottom = windowBounds.bottom, 680 left = windowBounds.left, 681 right = windowBounds.right 682 ) 683 val windowBackgroundColor = 684 window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor 685 686 // TODO(b/184121838): We should somehow get the top and bottom radius of the window 687 // instead of recomputing isExpandingFullyAbove here. 688 val isExpandingFullyAbove = 689 launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) 690 val endRadius = 691 if (isExpandingFullyAbove) { 692 // Most of the time, expanding fully above the root view means expanding in full 693 // screen. 694 ScreenDecorationsUtils.getWindowCornerRadius(context) 695 } else { 696 // This usually means we are in split screen mode, so 2 out of 4 corners will 697 // have 698 // a radius of 0. 699 0f 700 } 701 endState.topCornerRadius = endRadius 702 endState.bottomCornerRadius = endRadius 703 704 // We animate the opening window and delegate the view expansion to [this.controller]. 705 val delegate = this.controller 706 val controller = 707 object : Controller by delegate { 708 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 709 listener?.onLaunchAnimationStart() 710 711 if (DEBUG_LAUNCH_ANIMATION) { 712 Log.d( 713 TAG, 714 "Calling controller.onLaunchAnimationStart(isExpandingFullyAbove=" + 715 "$isExpandingFullyAbove) [controller=$delegate]" 716 ) 717 } 718 delegate.onLaunchAnimationStart(isExpandingFullyAbove) 719 } 720 721 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { 722 listener?.onLaunchAnimationEnd() 723 iCallback?.invoke() 724 725 if (DEBUG_LAUNCH_ANIMATION) { 726 Log.d( 727 TAG, 728 "Calling controller.onLaunchAnimationEnd(isExpandingFullyAbove=" + 729 "$isExpandingFullyAbove) [controller=$delegate]" 730 ) 731 } 732 delegate.onLaunchAnimationEnd(isExpandingFullyAbove) 733 } 734 735 override fun onLaunchAnimationProgress( 736 state: LaunchAnimator.State, 737 progress: Float, 738 linearProgress: Float 739 ) { 740 // Apply the state to the window only if it is visible, i.e. when the 741 // expanding view is *not* visible. 742 if (!state.visible) { 743 applyStateToWindow(window, state, linearProgress) 744 } 745 navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } 746 747 listener?.onLaunchAnimationProgress(linearProgress) 748 delegate.onLaunchAnimationProgress(state, progress, linearProgress) 749 } 750 } 751 752 animation = 753 launchAnimator.startAnimation( 754 controller, 755 endState, 756 windowBackgroundColor, 757 fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow, 758 drawHole = !controller.isBelowAnimatingWindow, 759 ) 760 } 761 762 private fun applyStateToWindow( 763 window: RemoteAnimationTarget, 764 state: LaunchAnimator.State, 765 linearProgress: Float, 766 ) { 767 if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) { 768 // Don't apply any transaction if the view root we synchronize with was detached or 769 // if the SurfaceControl associated with [window] is not valid, as 770 // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. 771 return 772 } 773 774 val screenBounds = window.screenSpaceBounds 775 val centerX = (screenBounds.left + screenBounds.right) / 2f 776 val centerY = (screenBounds.top + screenBounds.bottom) / 2f 777 val width = screenBounds.right - screenBounds.left 778 val height = screenBounds.bottom - screenBounds.top 779 780 // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no 781 // blank space on any side. 782 val widthRatio = state.width.toFloat() / width 783 val heightRatio = state.height.toFloat() / height 784 val scale = maxOf(widthRatio, heightRatio) 785 matrix.reset() 786 matrix.setScale(scale, scale, centerX, centerY) 787 788 // Align it to the top and center it in the x-axis. 789 val heightChange = height * scale - height 790 val translationX = state.centerX - centerX 791 val translationY = state.top - screenBounds.top + heightChange / 2f 792 matrix.postTranslate(translationX, translationY) 793 794 // Crop it. The matrix will also be applied to the crop, so we apply the inverse 795 // operation. Given that we only scale (by factor > 0) then translate, we can assume 796 // that the matrix is invertible. 797 val cropX = state.left.toFloat() - screenBounds.left 798 val cropY = state.top.toFloat() - screenBounds.top 799 windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height) 800 matrix.invert(invertMatrix) 801 invertMatrix.mapRect(windowCropF) 802 windowCrop.set( 803 windowCropF.left.roundToInt(), 804 windowCropF.top.roundToInt(), 805 windowCropF.right.roundToInt(), 806 windowCropF.bottom.roundToInt() 807 ) 808 809 // The alpha of the opening window. If it opens above the expandable, then it should 810 // fade in progressively. Otherwise, it should be fully opaque and will be progressively 811 // revealed as the window background color layer above the window fades out. 812 val alpha = 813 if (controller.isBelowAnimatingWindow) { 814 val windowProgress = 815 LaunchAnimator.getProgress( 816 TIMINGS, 817 linearProgress, 818 TIMINGS.contentAfterFadeInDelay, 819 TIMINGS.contentAfterFadeInDuration 820 ) 821 822 INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress) 823 } else { 824 1f 825 } 826 827 // The scale will also be applied to the corner radius, so we divide by the scale to 828 // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to 829 // make sure that the window does not draw itself behind the expanding view. This is 830 // especially important for lock screen animations, where the window is not clipped by 831 // the shade. 832 val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale 833 val params = 834 SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) 835 .withAlpha(alpha) 836 .withMatrix(matrix) 837 .withWindowCrop(windowCrop) 838 .withCornerRadius(cornerRadius) 839 .withVisibility(true) 840 .build() 841 842 transactionApplier.scheduleApply(params) 843 } 844 845 private fun applyStateToNavigationBar( 846 navigationBar: RemoteAnimationTarget, 847 state: LaunchAnimator.State, 848 linearProgress: Float 849 ) { 850 if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) { 851 // Don't apply any transaction if the view root we synchronize with was detached or 852 // if the SurfaceControl associated with [navigationBar] is not valid, as 853 // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. 854 return 855 } 856 857 val fadeInProgress = 858 LaunchAnimator.getProgress( 859 TIMINGS, 860 linearProgress, 861 ANIMATION_DELAY_NAV_FADE_IN, 862 ANIMATION_DURATION_NAV_FADE_OUT 863 ) 864 865 val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) 866 if (fadeInProgress > 0) { 867 matrix.reset() 868 matrix.setTranslate( 869 0f, 870 (state.top - navigationBar.sourceContainerBounds.top).toFloat() 871 ) 872 windowCrop.set(state.left, 0, state.right, state.height) 873 params 874 .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) 875 .withMatrix(matrix) 876 .withWindowCrop(windowCrop) 877 .withVisibility(true) 878 } else { 879 val fadeOutProgress = 880 LaunchAnimator.getProgress( 881 TIMINGS, 882 linearProgress, 883 0, 884 ANIMATION_DURATION_NAV_FADE_OUT 885 ) 886 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) 887 } 888 889 transactionApplier.scheduleApply(params.build()) 890 } 891 892 private fun onAnimationTimedOut() { 893 if (cancelled) { 894 return 895 } 896 897 Log.i(TAG, "Remote animation timed out") 898 timedOut = true 899 900 if (DEBUG_LAUNCH_ANIMATION) { 901 Log.d(TAG, "Calling controller.onLaunchAnimationCancelled() [animation timed out]") 902 } 903 controller.onLaunchAnimationCancelled() 904 listener?.onLaunchAnimationCancelled() 905 } 906 907 @UiThread 908 override fun onAnimationCancelled() { 909 if (timedOut) { 910 return 911 } 912 913 Log.i(TAG, "Remote animation was cancelled") 914 cancelled = true 915 removeTimeout() 916 917 animation?.cancel() 918 919 if (DEBUG_LAUNCH_ANIMATION) { 920 Log.d( 921 TAG, 922 "Calling controller.onLaunchAnimationCancelled() [remote animation cancelled]", 923 ) 924 } 925 controller.onLaunchAnimationCancelled() 926 listener?.onLaunchAnimationCancelled() 927 } 928 929 private fun IRemoteAnimationFinishedCallback.invoke() { 930 try { 931 onAnimationFinished() 932 } catch (e: RemoteException) { 933 e.printStackTrace() 934 } 935 } 936 } 937 } 938