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.Looper 28 import android.os.RemoteException 29 import android.util.Log 30 import android.view.IRemoteAnimationFinishedCallback 31 import android.view.IRemoteAnimationRunner 32 import android.view.RemoteAnimationAdapter 33 import android.view.RemoteAnimationTarget 34 import android.view.SyncRtSurfaceTransactionApplier 35 import android.view.View 36 import android.view.ViewGroup 37 import android.view.WindowManager 38 import android.view.animation.Interpolator 39 import android.view.animation.PathInterpolator 40 import com.android.internal.annotations.VisibleForTesting 41 import com.android.internal.policy.ScreenDecorationsUtils 42 import kotlin.math.roundToInt 43 44 private const val TAG = "ActivityLaunchAnimator" 45 46 /** 47 * A class that allows activities to be started in a seamless way from a view that is transforming 48 * nicely into the starting window. 49 */ 50 class ActivityLaunchAnimator( 51 private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS) 52 ) { 53 companion object { 54 @JvmField 55 val TIMINGS = LaunchAnimator.Timings( 56 totalDuration = 500L, 57 contentBeforeFadeOutDelay = 0L, 58 contentBeforeFadeOutDuration = 150L, 59 contentAfterFadeInDelay = 150L, 60 contentAfterFadeInDuration = 183L 61 ) 62 63 val INTERPOLATORS = LaunchAnimator.Interpolators( 64 positionInterpolator = Interpolators.EMPHASIZED, 65 positionXInterpolator = createPositionXInterpolator(), 66 contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, 67 contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) 68 ) 69 70 /** Durations & interpolators for the navigation bar fading in & out. */ 71 private const val ANIMATION_DURATION_NAV_FADE_IN = 266L 72 private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L 73 private val ANIMATION_DELAY_NAV_FADE_IN = 74 TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN 75 76 private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE 77 private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) 78 79 /** The time we wait before timing out the remote animation after starting the intent. */ 80 private const val LAUNCH_TIMEOUT = 1000L 81 82 private fun createPositionXInterpolator(): Interpolator { 83 val path = Path().apply { 84 moveTo(0f, 0f) 85 cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f) 86 cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f) 87 } 88 return PathInterpolator(path) 89 } 90 } 91 92 /** 93 * The callback of this animator. This should be set before any call to 94 * [start(Pending)IntentWithAnimation]. 95 */ 96 var callback: Callback? = null 97 98 /** 99 * Start an intent and animate the opening window. The intent will be started by running 100 * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch 101 * result. [controller] is responsible from animating the view from which the intent was started 102 * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window 103 * opening. 104 * 105 * If [controller] is null or [animate] is false, then the intent will be started and no 106 * animation will run. 107 * 108 * If possible, you should pass the [packageName] of the intent that will be started so that 109 * trampoline activity launches will also be animated. 110 * 111 * If the device is currently locked, the user will have to unlock it before the intent is 112 * started unless [showOverLockscreen] is true. In that case, the activity will be started 113 * directly over the lockscreen. 114 * 115 * This method will throw any exception thrown by [intentStarter]. 116 */ 117 @JvmOverloads 118 fun startIntentWithAnimation( 119 controller: Controller?, 120 animate: Boolean = true, 121 packageName: String? = null, 122 showOverLockscreen: Boolean = false, 123 intentStarter: (RemoteAnimationAdapter?) -> Int 124 ) { 125 if (controller == null || !animate) { 126 Log.i(TAG, "Starting intent with no animation") 127 intentStarter(null) 128 controller?.callOnIntentStartedOnMainThread(willAnimate = false) 129 return 130 } 131 132 val callback = this.callback ?: throw IllegalStateException( 133 "ActivityLaunchAnimator.callback must be set before using this animator") 134 val runner = Runner(controller) 135 val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen 136 137 // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the 138 // keyguard with the animation 139 val animationAdapter = if (!hideKeyguardWithAnimation) { 140 RemoteAnimationAdapter( 141 runner, 142 TIMINGS.totalDuration, 143 TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ 144 ) 145 } else { 146 null 147 } 148 149 // Register the remote animation for the given package to also animate trampoline 150 // activity launches. 151 if (packageName != null && animationAdapter != null) { 152 try { 153 ActivityTaskManager.getService().registerRemoteAnimationForNextActivityStart( 154 packageName, animationAdapter) 155 } catch (e: RemoteException) { 156 Log.w(TAG, "Unable to register the remote animation", e) 157 } 158 } 159 160 val launchResult = intentStarter(animationAdapter) 161 162 // Only animate if the app is not already on top and will be opened, unless we are on the 163 // keyguard. 164 val willAnimate = 165 launchResult == ActivityManager.START_TASK_TO_FRONT || 166 launchResult == ActivityManager.START_SUCCESS || 167 (launchResult == ActivityManager.START_DELIVERED_TO_TOP && 168 hideKeyguardWithAnimation) 169 170 Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + 171 "hideKeyguardWithAnimation=$hideKeyguardWithAnimation") 172 controller.callOnIntentStartedOnMainThread(willAnimate) 173 174 // If we expect an animation, post a timeout to cancel it in case the remote animation is 175 // never started. 176 if (willAnimate) { 177 runner.postTimeout() 178 179 // Hide the keyguard using the launch animation instead of the default unlock animation. 180 if (hideKeyguardWithAnimation) { 181 callback.hideKeyguardWithAnimation(runner) 182 } 183 } 184 } 185 186 private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { 187 if (Looper.myLooper() != Looper.getMainLooper()) { 188 this.launchContainer.context.mainExecutor.execute { 189 this.onIntentStarted(willAnimate) 190 } 191 } else { 192 this.onIntentStarted(willAnimate) 193 } 194 } 195 196 /** 197 * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a 198 * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful 199 * for Java caller starting a [PendingIntent]. 200 * 201 * If possible, you should pass the [packageName] of the intent that will be started so that 202 * trampoline activity launches will also be animated. 203 */ 204 @Throws(PendingIntent.CanceledException::class) 205 @JvmOverloads 206 fun startPendingIntentWithAnimation( 207 controller: Controller?, 208 animate: Boolean = true, 209 packageName: String? = null, 210 intentStarter: PendingIntentStarter 211 ) { 212 startIntentWithAnimation(controller, animate, packageName) { 213 intentStarter.startPendingIntent(it) 214 } 215 } 216 217 /** Create a new animation [Runner] controlled by [controller]. */ 218 @VisibleForTesting 219 fun createRunner(controller: Controller): Runner = Runner(controller) 220 221 interface PendingIntentStarter { 222 /** 223 * Start a pending intent using the provided [animationAdapter] and return the launch 224 * result. 225 */ 226 @Throws(PendingIntent.CanceledException::class) 227 fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int 228 } 229 230 interface Callback { 231 /** Whether we are currently on the keyguard or not. */ 232 fun isOnKeyguard(): Boolean 233 234 /** Hide the keyguard and animate using [runner]. */ 235 fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) 236 237 /** Enable/disable window blur so they don't overlap with the window launch animation **/ 238 fun setBlursDisabledForAppLaunch(disabled: Boolean) 239 240 /* Get the background color of [task]. */ 241 fun getBackgroundColor(task: TaskInfo): Int 242 } 243 244 /** 245 * A controller that takes care of applying the animation to an expanding view. 246 * 247 * Note that all callbacks (onXXX methods) are all called on the main thread. 248 */ 249 interface Controller : LaunchAnimator.Controller { 250 companion object { 251 /** 252 * Return a [Controller] that will animate and expand [view] into the opening window. 253 * 254 * Important: The view must be attached to a [ViewGroup] when calling this function and 255 * during the animation. For safety, this method will return null when it is not. 256 */ 257 @JvmStatic 258 fun fromView(view: View, cujType: Int? = null): Controller? { 259 if (view.parent !is ViewGroup) { 260 // TODO(b/192194319): Throw instead of just logging. 261 Log.wtf( 262 TAG, 263 "Skipping animation as view $view is not attached to a ViewGroup", 264 Exception() 265 ) 266 return null 267 } 268 269 return GhostedViewLaunchAnimatorController(view, cujType) 270 } 271 } 272 273 /** 274 * The intent was started. If [willAnimate] is false, nothing else will happen and the 275 * animation will not be started. 276 */ 277 fun onIntentStarted(willAnimate: Boolean) {} 278 279 /** 280 * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after 281 * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called 282 * before the cancellation. 283 */ 284 fun onLaunchAnimationCancelled() {} 285 } 286 287 @VisibleForTesting 288 inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() { 289 private val launchContainer = controller.launchContainer 290 private val context = launchContainer.context 291 private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer) 292 293 private val matrix = Matrix() 294 private val invertMatrix = Matrix() 295 private var windowCrop = Rect() 296 private var windowCropF = RectF() 297 private var timedOut = false 298 private var cancelled = false 299 private var animation: LaunchAnimator.Animation? = null 300 301 // A timeout to cancel the remote animation if it is not started within X milliseconds after 302 // the intent was started. 303 // 304 // Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise 305 // it will be automatically converted when posted and we wouldn't be able to remove it after 306 // posting it. 307 private var onTimeout = Runnable { onAnimationTimedOut() } 308 309 internal fun postTimeout() { 310 launchContainer.postDelayed(onTimeout, LAUNCH_TIMEOUT) 311 } 312 313 private fun removeTimeout() { 314 launchContainer.removeCallbacks(onTimeout) 315 } 316 317 override fun onAnimationStart( 318 @WindowManager.TransitionOldType transit: Int, 319 apps: Array<out RemoteAnimationTarget>?, 320 wallpapers: Array<out RemoteAnimationTarget>?, 321 nonApps: Array<out RemoteAnimationTarget>?, 322 iCallback: IRemoteAnimationFinishedCallback? 323 ) { 324 removeTimeout() 325 326 // The animation was started too late and we already notified the controller that it 327 // timed out. 328 if (timedOut) { 329 iCallback?.invoke() 330 return 331 } 332 333 // This should not happen, but let's make sure we don't start the animation if it was 334 // cancelled before and we already notified the controller. 335 if (cancelled) { 336 return 337 } 338 339 context.mainExecutor.execute { 340 startAnimation(apps, nonApps, iCallback) 341 } 342 } 343 344 private fun startAnimation( 345 apps: Array<out RemoteAnimationTarget>?, 346 nonApps: Array<out RemoteAnimationTarget>?, 347 iCallback: IRemoteAnimationFinishedCallback? 348 ) { 349 if (LaunchAnimator.DEBUG) { 350 Log.d(TAG, "Remote animation started") 351 } 352 353 val window = apps?.firstOrNull { 354 it.mode == RemoteAnimationTarget.MODE_OPENING 355 } 356 357 if (window == null) { 358 Log.i(TAG, "Aborting the animation as no window is opening") 359 removeTimeout() 360 iCallback?.invoke() 361 controller.onLaunchAnimationCancelled() 362 return 363 } 364 365 val navigationBar = nonApps?.firstOrNull { 366 it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR 367 } 368 369 val windowBounds = window.screenSpaceBounds 370 val endState = LaunchAnimator.State( 371 top = windowBounds.top, 372 bottom = windowBounds.bottom, 373 left = windowBounds.left, 374 right = windowBounds.right 375 ) 376 val callback = this@ActivityLaunchAnimator.callback!! 377 val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo) 378 379 // TODO(b/184121838): We should somehow get the top and bottom radius of the window 380 // instead of recomputing isExpandingFullyAbove here. 381 val isExpandingFullyAbove = 382 launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) 383 val endRadius = if (isExpandingFullyAbove) { 384 // Most of the time, expanding fully above the root view means expanding in full 385 // screen. 386 ScreenDecorationsUtils.getWindowCornerRadius(context) 387 } else { 388 // This usually means we are in split screen mode, so 2 out of 4 corners will have 389 // a radius of 0. 390 0f 391 } 392 endState.topCornerRadius = endRadius 393 endState.bottomCornerRadius = endRadius 394 395 // We animate the opening window and delegate the view expansion to [this.controller]. 396 val delegate = this.controller 397 val controller = object : LaunchAnimator.Controller by delegate { 398 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { 399 callback.setBlursDisabledForAppLaunch(true) 400 delegate.onLaunchAnimationStart(isExpandingFullyAbove) 401 } 402 403 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { 404 callback.setBlursDisabledForAppLaunch(false) 405 iCallback?.invoke() 406 delegate.onLaunchAnimationEnd(isExpandingFullyAbove) 407 } 408 409 override fun onLaunchAnimationProgress( 410 state: LaunchAnimator.State, 411 progress: Float, 412 linearProgress: Float 413 ) { 414 applyStateToWindow(window, state) 415 navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } 416 delegate.onLaunchAnimationProgress(state, progress, linearProgress) 417 } 418 } 419 420 // We draw a hole when the additional layer is fading out to reveal the opening window. 421 animation = launchAnimator.startAnimation( 422 controller, endState, windowBackgroundColor, drawHole = true) 423 } 424 425 private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) { 426 val screenBounds = window.screenSpaceBounds 427 val centerX = (screenBounds.left + screenBounds.right) / 2f 428 val centerY = (screenBounds.top + screenBounds.bottom) / 2f 429 val width = screenBounds.right - screenBounds.left 430 val height = screenBounds.bottom - screenBounds.top 431 432 // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no 433 // blank space on any side. 434 val widthRatio = state.width.toFloat() / width 435 val heightRatio = state.height.toFloat() / height 436 val scale = maxOf(widthRatio, heightRatio) 437 matrix.reset() 438 matrix.setScale(scale, scale, centerX, centerY) 439 440 // Align it to the top and center it in the x-axis. 441 val heightChange = height * scale - height 442 val translationX = state.centerX - centerX 443 val translationY = state.top - screenBounds.top + heightChange / 2f 444 matrix.postTranslate(translationX, translationY) 445 446 // Crop it. The matrix will also be applied to the crop, so we apply the inverse 447 // operation. Given that we only scale (by factor > 0) then translate, we can assume 448 // that the matrix is invertible. 449 val cropX = state.left.toFloat() - screenBounds.left 450 val cropY = state.top.toFloat() - screenBounds.top 451 windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height) 452 matrix.invert(invertMatrix) 453 invertMatrix.mapRect(windowCropF) 454 windowCrop.set( 455 windowCropF.left.roundToInt(), 456 windowCropF.top.roundToInt(), 457 windowCropF.right.roundToInt(), 458 windowCropF.bottom.roundToInt() 459 ) 460 461 // The scale will also be applied to the corner radius, so we divide by the scale to 462 // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to 463 // make sure that the window does not draw itself behind the expanding view. This is 464 // especially important for lock screen animations, where the window is not clipped by 465 // the shade. 466 val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale 467 val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) 468 .withAlpha(1f) 469 .withMatrix(matrix) 470 .withWindowCrop(windowCrop) 471 .withCornerRadius(cornerRadius) 472 .withVisibility(true) 473 .build() 474 475 transactionApplier.scheduleApply(params) 476 } 477 478 private fun applyStateToNavigationBar( 479 navigationBar: RemoteAnimationTarget, 480 state: LaunchAnimator.State, 481 linearProgress: Float 482 ) { 483 val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 484 ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT) 485 486 val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) 487 if (fadeInProgress > 0) { 488 matrix.reset() 489 matrix.setTranslate( 490 0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat()) 491 windowCrop.set(state.left, 0, state.right, state.height) 492 params 493 .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) 494 .withMatrix(matrix) 495 .withWindowCrop(windowCrop) 496 .withVisibility(true) 497 } else { 498 val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0, 499 ANIMATION_DURATION_NAV_FADE_OUT) 500 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) 501 } 502 503 transactionApplier.scheduleApply(params.build()) 504 } 505 506 private fun onAnimationTimedOut() { 507 if (cancelled) { 508 return 509 } 510 511 Log.i(TAG, "Remote animation timed out") 512 timedOut = true 513 controller.onLaunchAnimationCancelled() 514 } 515 516 override fun onAnimationCancelled() { 517 if (timedOut) { 518 return 519 } 520 521 Log.i(TAG, "Remote animation was cancelled") 522 cancelled = true 523 removeTimeout() 524 context.mainExecutor.execute { 525 animation?.cancel() 526 controller.onLaunchAnimationCancelled() 527 } 528 } 529 530 private fun IRemoteAnimationFinishedCallback.invoke() { 531 try { 532 onAnimationFinished() 533 } catch (e: RemoteException) { 534 e.printStackTrace() 535 } 536 } 537 } 538 } 539