| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.animation |
| |
| import android.app.ActivityManager |
| import android.app.ActivityTaskManager |
| import android.app.PendingIntent |
| import android.app.TaskInfo |
| import android.graphics.Matrix |
| import android.graphics.Rect |
| import android.graphics.RectF |
| import android.os.Build |
| import android.os.Handler |
| import android.os.Looper |
| import android.os.RemoteException |
| import android.util.Log |
| import android.view.IRemoteAnimationFinishedCallback |
| import android.view.IRemoteAnimationRunner |
| import android.view.RemoteAnimationAdapter |
| import android.view.RemoteAnimationTarget |
| import android.view.SyncRtSurfaceTransactionApplier |
| import android.view.View |
| import android.view.ViewGroup |
| import android.view.WindowManager |
| import android.view.animation.PathInterpolator |
| import androidx.annotation.AnyThread |
| import androidx.annotation.BinderThread |
| import androidx.annotation.UiThread |
| import com.android.app.animation.Interpolators |
| import com.android.internal.annotations.VisibleForTesting |
| import com.android.internal.policy.ScreenDecorationsUtils |
| import kotlin.math.roundToInt |
| |
| private const val TAG = "ActivityLaunchAnimator" |
| |
| /** |
| * A class that allows activities to be started in a seamless way from a view that is transforming |
| * nicely into the starting window. |
| */ |
| class ActivityLaunchAnimator( |
| /** The animator used when animating a View into an app. */ |
| private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, |
| |
| /** The animator used when animating a Dialog into an app. */ |
| // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to |
| // TIMINGS.contentBeforeFadeOutDuration. |
| private val dialogToAppAnimator: LaunchAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR, |
| |
| /** |
| * Whether we should disable the WindowManager timeout. This should be set to true in tests |
| * only. |
| */ |
| // TODO(b/301385865): Remove this flag. |
| private val disableWmTimeout: Boolean = false, |
| ) { |
| companion object { |
| /** The timings when animating a View into an app. */ |
| @JvmField |
| val TIMINGS = |
| LaunchAnimator.Timings( |
| totalDuration = 500L, |
| contentBeforeFadeOutDelay = 0L, |
| contentBeforeFadeOutDuration = 150L, |
| contentAfterFadeInDelay = 150L, |
| contentAfterFadeInDuration = 183L |
| ) |
| |
| /** |
| * The timings when animating a Dialog into an app. We need to wait at least 200ms before |
| * showing the app (which is under the dialog window) so that the dialog window dim is fully |
| * faded out, to avoid flicker. |
| */ |
| val DIALOG_TIMINGS = |
| TIMINGS.copy(contentBeforeFadeOutDuration = 200L, contentAfterFadeInDelay = 200L) |
| |
| /** The interpolators when animating a View or a dialog into an app. */ |
| val INTERPOLATORS = |
| LaunchAnimator.Interpolators( |
| positionInterpolator = Interpolators.EMPHASIZED, |
| positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT, |
| contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, |
| contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) |
| ) |
| |
| // TODO(b/288507023): Remove this flag. |
| @JvmField val DEBUG_LAUNCH_ANIMATION = Build.IS_DEBUGGABLE |
| |
| private val DEFAULT_LAUNCH_ANIMATOR = LaunchAnimator(TIMINGS, INTERPOLATORS) |
| private val DEFAULT_DIALOG_TO_APP_ANIMATOR = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS) |
| |
| /** Durations & interpolators for the navigation bar fading in & out. */ |
| private const val ANIMATION_DURATION_NAV_FADE_IN = 266L |
| private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L |
| private val ANIMATION_DELAY_NAV_FADE_IN = |
| TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN |
| |
| private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE |
| private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) |
| |
| /** The time we wait before timing out the remote animation after starting the intent. */ |
| private const val LAUNCH_TIMEOUT = 1_000L |
| |
| /** |
| * The time we wait before we Log.wtf because the remote animation was neither started or |
| * cancelled by WM. |
| */ |
| private const val LONG_LAUNCH_TIMEOUT = 5_000L |
| } |
| |
| /** |
| * The callback of this animator. This should be set before any call to |
| * [start(Pending)IntentWithAnimation]. |
| */ |
| var callback: Callback? = null |
| |
| /** The set of [Listener] that should be notified of any animation started by this animator. */ |
| private val listeners = LinkedHashSet<Listener>() |
| |
| /** Top-level listener that can be used to notify all registered [listeners]. */ |
| private val lifecycleListener = |
| object : Listener { |
| override fun onLaunchAnimationStart() { |
| listeners.forEach { it.onLaunchAnimationStart() } |
| } |
| |
| override fun onLaunchAnimationEnd() { |
| listeners.forEach { it.onLaunchAnimationEnd() } |
| } |
| |
| override fun onLaunchAnimationProgress(linearProgress: Float) { |
| listeners.forEach { it.onLaunchAnimationProgress(linearProgress) } |
| } |
| |
| override fun onLaunchAnimationCancelled() { |
| listeners.forEach { it.onLaunchAnimationCancelled() } |
| } |
| } |
| |
| /** |
| * Start an intent and animate the opening window. The intent will be started by running |
| * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch |
| * result. [controller] is responsible from animating the view from which the intent was started |
| * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window |
| * opening. |
| * |
| * If [controller] is null or [animate] is false, then the intent will be started and no |
| * animation will run. |
| * |
| * If possible, you should pass the [packageName] of the intent that will be started so that |
| * trampoline activity launches will also be animated. |
| * |
| * If the device is currently locked, the user will have to unlock it before the intent is |
| * started unless [showOverLockscreen] is true. In that case, the activity will be started |
| * directly over the lockscreen. |
| * |
| * This method will throw any exception thrown by [intentStarter]. |
| */ |
| @JvmOverloads |
| fun startIntentWithAnimation( |
| controller: Controller?, |
| animate: Boolean = true, |
| packageName: String? = null, |
| showOverLockscreen: Boolean = false, |
| intentStarter: (RemoteAnimationAdapter?) -> Int |
| ) { |
| if (controller == null || !animate) { |
| Log.i(TAG, "Starting intent with no animation") |
| intentStarter(null) |
| controller?.callOnIntentStartedOnMainThread(willAnimate = false) |
| return |
| } |
| |
| val callback = |
| this.callback |
| ?: throw IllegalStateException( |
| "ActivityLaunchAnimator.callback must be set before using this animator" |
| ) |
| val runner = createRunner(controller) |
| val runnerDelegate = runner.delegate!! |
| val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen |
| |
| // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the |
| // keyguard with the animation |
| val animationAdapter = |
| if (!hideKeyguardWithAnimation) { |
| RemoteAnimationAdapter( |
| runner, |
| TIMINGS.totalDuration, |
| TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ |
| ) |
| } else { |
| null |
| } |
| |
| // Register the remote animation for the given package to also animate trampoline |
| // activity launches. |
| if (packageName != null && animationAdapter != null) { |
| try { |
| ActivityTaskManager.getService() |
| .registerRemoteAnimationForNextActivityStart( |
| packageName, |
| animationAdapter, |
| null /* launchCookie */ |
| ) |
| } catch (e: RemoteException) { |
| Log.w(TAG, "Unable to register the remote animation", e) |
| } |
| } |
| |
| val launchResult = intentStarter(animationAdapter) |
| |
| // Only animate if the app is not already on top and will be opened, unless we are on the |
| // keyguard. |
| val willAnimate = |
| launchResult == ActivityManager.START_TASK_TO_FRONT || |
| launchResult == ActivityManager.START_SUCCESS || |
| (launchResult == ActivityManager.START_DELIVERED_TO_TOP && |
| hideKeyguardWithAnimation) |
| |
| Log.i( |
| TAG, |
| "launchResult=$launchResult willAnimate=$willAnimate " + |
| "hideKeyguardWithAnimation=$hideKeyguardWithAnimation" |
| ) |
| controller.callOnIntentStartedOnMainThread(willAnimate) |
| |
| // If we expect an animation, post a timeout to cancel it in case the remote animation is |
| // never started. |
| if (willAnimate) { |
| runnerDelegate.postTimeouts() |
| |
| // Hide the keyguard using the launch animation instead of the default unlock animation. |
| if (hideKeyguardWithAnimation) { |
| callback.hideKeyguardWithAnimation(runner) |
| } |
| } else { |
| // We need to make sure delegate references are dropped to avoid memory leaks. |
| runner.dispose() |
| } |
| } |
| |
| private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { |
| if (Looper.myLooper() != Looper.getMainLooper()) { |
| this.launchContainer.context.mainExecutor.execute { |
| callOnIntentStartedOnMainThread(willAnimate) |
| } |
| } else { |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d( |
| TAG, |
| "Calling controller.onIntentStarted(willAnimate=$willAnimate) " + |
| "[controller=$this]" |
| ) |
| } |
| this.onIntentStarted(willAnimate) |
| } |
| } |
| |
| /** |
| * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a |
| * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful |
| * for Java caller starting a [PendingIntent]. |
| * |
| * If possible, you should pass the [packageName] of the intent that will be started so that |
| * trampoline activity launches will also be animated. |
| */ |
| @Throws(PendingIntent.CanceledException::class) |
| @JvmOverloads |
| fun startPendingIntentWithAnimation( |
| controller: Controller?, |
| animate: Boolean = true, |
| packageName: String? = null, |
| showOverLockscreen: Boolean = false, |
| intentStarter: PendingIntentStarter |
| ) { |
| startIntentWithAnimation(controller, animate, packageName, showOverLockscreen) { |
| intentStarter.startPendingIntent(it) |
| } |
| } |
| |
| /** Add a [Listener] that can listen to launch animations. */ |
| fun addListener(listener: Listener) { |
| listeners.add(listener) |
| } |
| |
| /** Remove a [Listener]. */ |
| fun removeListener(listener: Listener) { |
| listeners.remove(listener) |
| } |
| |
| /** Create a new animation [Runner] controlled by [controller]. */ |
| @VisibleForTesting |
| fun createRunner(controller: Controller): Runner { |
| // Make sure we use the modified timings when animating a dialog into an app. |
| val launchAnimator = |
| if (controller.isDialogLaunch) { |
| dialogToAppAnimator |
| } else { |
| launchAnimator |
| } |
| |
| return Runner(controller, callback!!, launchAnimator, lifecycleListener) |
| } |
| |
| interface PendingIntentStarter { |
| /** |
| * Start a pending intent using the provided [animationAdapter] and return the launch |
| * result. |
| */ |
| @Throws(PendingIntent.CanceledException::class) |
| fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int |
| } |
| |
| interface Callback { |
| /** Whether we are currently on the keyguard or not. */ |
| fun isOnKeyguard(): Boolean = false |
| |
| /** Hide the keyguard and animate using [runner]. */ |
| fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) { |
| throw UnsupportedOperationException() |
| } |
| |
| /* Get the background color of [task]. */ |
| fun getBackgroundColor(task: TaskInfo): Int |
| } |
| |
| interface Listener { |
| /** Called when an activity launch animation started. */ |
| fun onLaunchAnimationStart() {} |
| |
| /** |
| * Called when an activity launch animation is finished. This will be called if and only if |
| * [onLaunchAnimationStart] was called earlier. |
| */ |
| fun onLaunchAnimationEnd() {} |
| |
| /** |
| * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after |
| * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called |
| * before the cancellation. |
| */ |
| fun onLaunchAnimationCancelled() {} |
| |
| /** Called when an activity launch animation made progress. */ |
| fun onLaunchAnimationProgress(linearProgress: Float) {} |
| } |
| |
| /** |
| * A controller that takes care of applying the animation to an expanding view. |
| * |
| * Note that all callbacks (onXXX methods) are all called on the main thread. |
| */ |
| interface Controller : LaunchAnimator.Controller { |
| companion object { |
| /** |
| * Return a [Controller] that will animate and expand [view] into the opening window. |
| * |
| * Important: The view must be attached to a [ViewGroup] when calling this function and |
| * during the animation. For safety, this method will return null when it is not. The |
| * view must also implement [LaunchableView], otherwise this method will throw. |
| * |
| * Note: The background of [view] should be a (rounded) rectangle so that it can be |
| * properly animated. |
| */ |
| @JvmStatic |
| fun fromView(view: View, cujType: Int? = null): Controller? { |
| // Make sure the View we launch from implements LaunchableView to avoid visibility |
| // issues. |
| if (view !is LaunchableView) { |
| throw IllegalArgumentException( |
| "An ActivityLaunchAnimator.Controller was created from a View that does " + |
| "not implement LaunchableView. This can lead to subtle bugs where the" + |
| " visibility of the View we are launching from is not what we expected." |
| ) |
| } |
| |
| if (view.parent !is ViewGroup) { |
| Log.e( |
| TAG, |
| "Skipping animation as view $view is not attached to a ViewGroup", |
| Exception() |
| ) |
| return null |
| } |
| |
| return GhostedViewLaunchAnimatorController(view, cujType) |
| } |
| } |
| |
| /** |
| * Whether this controller is controlling a dialog launch. This will be used to adapt the |
| * timings, making sure we don't show the app until the dialog dim had the time to fade out. |
| */ |
| // TODO(b/218989950): Remove this. |
| val isDialogLaunch: Boolean |
| get() = false |
| |
| /** |
| * Whether the expandable controller by this [Controller] is below the launching window that |
| * is going to be animated. |
| * |
| * This should be `false` when launching an app from the shade or status bar, given that |
| * they are drawn above all apps. This is usually `true` when using this launcher in a |
| * normal app or a launcher, that are drawn below the animating activity/window. |
| */ |
| val isBelowAnimatingWindow: Boolean |
| get() = false |
| |
| /** |
| * The intent was started. If [willAnimate] is false, nothing else will happen and the |
| * animation will not be started. |
| */ |
| fun onIntentStarted(willAnimate: Boolean) {} |
| |
| /** |
| * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after |
| * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called |
| * before the cancellation. |
| * |
| * If this launch animation affected the occlusion state of the keyguard, WM will provide us |
| * with [newKeyguardOccludedState] so that we can set the occluded state appropriately. |
| */ |
| fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {} |
| } |
| |
| /** |
| * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all |
| * events to the passed [delegate]. |
| */ |
| @VisibleForTesting |
| inner class DelegatingAnimationCompletionListener( |
| private val delegate: Listener?, |
| private val onAnimationComplete: () -> Unit |
| ) : Listener { |
| var cancelled = false |
| |
| override fun onLaunchAnimationStart() { |
| delegate?.onLaunchAnimationStart() |
| } |
| |
| override fun onLaunchAnimationProgress(linearProgress: Float) { |
| delegate?.onLaunchAnimationProgress(linearProgress) |
| } |
| |
| override fun onLaunchAnimationEnd() { |
| delegate?.onLaunchAnimationEnd() |
| if (!cancelled) { |
| onAnimationComplete.invoke() |
| } |
| } |
| |
| override fun onLaunchAnimationCancelled() { |
| cancelled = true |
| delegate?.onLaunchAnimationCancelled() |
| onAnimationComplete.invoke() |
| } |
| } |
| |
| @VisibleForTesting |
| inner class Runner( |
| controller: Controller, |
| callback: Callback, |
| /** The animator to use to animate the window launch. */ |
| launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, |
| /** Listener for animation lifecycle events. */ |
| listener: Listener? = null |
| ) : IRemoteAnimationRunner.Stub() { |
| private val context = controller.launchContainer.context |
| |
| // This is being passed across IPC boundaries and cycles (through PendingIntentRecords, |
| // etc.) are possible. So we need to make sure we drop any references that might |
| // transitively cause leaks when we're done with animation. |
| @VisibleForTesting var delegate: AnimationDelegate? |
| |
| init { |
| delegate = |
| AnimationDelegate( |
| controller, |
| callback, |
| DelegatingAnimationCompletionListener(listener, this::dispose), |
| launchAnimator, |
| disableWmTimeout |
| ) |
| } |
| |
| @BinderThread |
| override fun onAnimationStart( |
| transit: Int, |
| apps: Array<out RemoteAnimationTarget>?, |
| wallpapers: Array<out RemoteAnimationTarget>?, |
| nonApps: Array<out RemoteAnimationTarget>?, |
| finishedCallback: IRemoteAnimationFinishedCallback? |
| ) { |
| val delegate = delegate |
| context.mainExecutor.execute { |
| if (delegate == null) { |
| Log.i(TAG, "onAnimationStart called after completion") |
| // Animation started too late and timed out already. We need to still |
| // signal back that we're done with it. |
| finishedCallback?.onAnimationFinished() |
| } else { |
| delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) |
| } |
| } |
| } |
| |
| @BinderThread |
| override fun onAnimationCancelled() { |
| val delegate = delegate |
| context.mainExecutor.execute { |
| delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion") |
| delegate?.onAnimationCancelled() |
| } |
| } |
| |
| @AnyThread |
| fun dispose() { |
| // Drop references to animation controller once we're done with the animation |
| // to avoid leaking. |
| context.mainExecutor.execute { delegate = null } |
| } |
| } |
| |
| class AnimationDelegate |
| @JvmOverloads |
| constructor( |
| private val controller: Controller, |
| private val callback: Callback, |
| /** Listener for animation lifecycle events. */ |
| private val listener: Listener? = null, |
| /** The animator to use to animate the window launch. */ |
| private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, |
| |
| /** |
| * Whether we should disable the WindowManager timeout. This should be set to true in tests |
| * only. |
| */ |
| // TODO(b/301385865): Remove this flag. |
| disableWmTimeout: Boolean = false, |
| ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { |
| private val launchContainer = controller.launchContainer |
| private val context = launchContainer.context |
| private val transactionApplierView = |
| controller.openingWindowSyncView ?: controller.launchContainer |
| private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView) |
| private val timeoutHandler = |
| if (!disableWmTimeout) { |
| Handler(Looper.getMainLooper()) |
| } else { |
| null |
| } |
| |
| private val matrix = Matrix() |
| private val invertMatrix = Matrix() |
| private var windowCrop = Rect() |
| private var windowCropF = RectF() |
| private var timedOut = false |
| private var cancelled = false |
| private var animation: LaunchAnimator.Animation? = null |
| |
| /** |
| * A timeout to cancel the launch animation if the remote animation is not started or |
| * cancelled within [LAUNCH_TIMEOUT] milliseconds after the intent was started. |
| * |
| * Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise |
| * it will be automatically converted when posted and we wouldn't be able to remove it after |
| * posting it. |
| */ |
| private var onTimeout = Runnable { onAnimationTimedOut() } |
| |
| /** |
| * A long timeout to Log.wtf (signaling a bug in WM) when the remote animation wasn't |
| * started or cancelled within [LONG_LAUNCH_TIMEOUT] milliseconds after the intent was |
| * started. |
| */ |
| private var onLongTimeout = Runnable { |
| Log.wtf( |
| TAG, |
| "The remote animation was neither cancelled or started within $LONG_LAUNCH_TIMEOUT" |
| ) |
| } |
| |
| @UiThread |
| internal fun postTimeouts() { |
| if (timeoutHandler != null) { |
| timeoutHandler.postDelayed(onTimeout, LAUNCH_TIMEOUT) |
| timeoutHandler.postDelayed(onLongTimeout, LONG_LAUNCH_TIMEOUT) |
| } |
| } |
| |
| private fun removeTimeouts() { |
| if (timeoutHandler != null) { |
| timeoutHandler.removeCallbacks(onTimeout) |
| timeoutHandler.removeCallbacks(onLongTimeout) |
| } |
| } |
| |
| @UiThread |
| override fun onAnimationStart( |
| @WindowManager.TransitionOldType transit: Int, |
| apps: Array<out RemoteAnimationTarget>?, |
| wallpapers: Array<out RemoteAnimationTarget>?, |
| nonApps: Array<out RemoteAnimationTarget>?, |
| callback: IRemoteAnimationFinishedCallback? |
| ) { |
| removeTimeouts() |
| |
| // The animation was started too late and we already notified the controller that it |
| // timed out. |
| if (timedOut) { |
| callback?.invoke() |
| return |
| } |
| |
| // This should not happen, but let's make sure we don't start the animation if it was |
| // cancelled before and we already notified the controller. |
| if (cancelled) { |
| return |
| } |
| |
| startAnimation(apps, nonApps, callback) |
| } |
| |
| private fun findRootTaskIfPossible( |
| apps: Array<out RemoteAnimationTarget>? |
| ): RemoteAnimationTarget? { |
| if (apps == null) { |
| return null |
| } |
| var candidate: RemoteAnimationTarget? = null |
| for (it in apps) { |
| if (it.mode == RemoteAnimationTarget.MODE_OPENING) { |
| if (it.taskInfo != null && !it.hasAnimatingParent) { |
| return it |
| } |
| if (candidate == null) { |
| candidate = it |
| } |
| } |
| } |
| return candidate |
| } |
| |
| private fun startAnimation( |
| apps: Array<out RemoteAnimationTarget>?, |
| nonApps: Array<out RemoteAnimationTarget>?, |
| iCallback: IRemoteAnimationFinishedCallback? |
| ) { |
| if (LaunchAnimator.DEBUG) { |
| Log.d(TAG, "Remote animation started") |
| } |
| |
| val window = findRootTaskIfPossible(apps) |
| if (window == null) { |
| Log.i(TAG, "Aborting the animation as no window is opening") |
| iCallback?.invoke() |
| |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d( |
| TAG, |
| "Calling controller.onLaunchAnimationCancelled() [no window opening]" |
| ) |
| } |
| controller.onLaunchAnimationCancelled() |
| listener?.onLaunchAnimationCancelled() |
| return |
| } |
| |
| val navigationBar = |
| nonApps?.firstOrNull { |
| it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR |
| } |
| |
| val windowBounds = window.screenSpaceBounds |
| val endState = |
| LaunchAnimator.State( |
| top = windowBounds.top, |
| bottom = windowBounds.bottom, |
| left = windowBounds.left, |
| right = windowBounds.right |
| ) |
| val windowBackgroundColor = |
| window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor |
| |
| // TODO(b/184121838): We should somehow get the top and bottom radius of the window |
| // instead of recomputing isExpandingFullyAbove here. |
| val isExpandingFullyAbove = |
| launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) |
| val endRadius = |
| if (isExpandingFullyAbove) { |
| // Most of the time, expanding fully above the root view means expanding in full |
| // screen. |
| ScreenDecorationsUtils.getWindowCornerRadius(context) |
| } else { |
| // This usually means we are in split screen mode, so 2 out of 4 corners will |
| // have |
| // a radius of 0. |
| 0f |
| } |
| endState.topCornerRadius = endRadius |
| endState.bottomCornerRadius = endRadius |
| |
| // We animate the opening window and delegate the view expansion to [this.controller]. |
| val delegate = this.controller |
| val controller = |
| object : Controller by delegate { |
| override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { |
| listener?.onLaunchAnimationStart() |
| |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d( |
| TAG, |
| "Calling controller.onLaunchAnimationStart(isExpandingFullyAbove=" + |
| "$isExpandingFullyAbove) [controller=$delegate]" |
| ) |
| } |
| delegate.onLaunchAnimationStart(isExpandingFullyAbove) |
| } |
| |
| override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { |
| listener?.onLaunchAnimationEnd() |
| iCallback?.invoke() |
| |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d( |
| TAG, |
| "Calling controller.onLaunchAnimationEnd(isExpandingFullyAbove=" + |
| "$isExpandingFullyAbove) [controller=$delegate]" |
| ) |
| } |
| delegate.onLaunchAnimationEnd(isExpandingFullyAbove) |
| } |
| |
| override fun onLaunchAnimationProgress( |
| state: LaunchAnimator.State, |
| progress: Float, |
| linearProgress: Float |
| ) { |
| // Apply the state to the window only if it is visible, i.e. when the |
| // expanding view is *not* visible. |
| if (!state.visible) { |
| applyStateToWindow(window, state, linearProgress) |
| } |
| navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } |
| |
| listener?.onLaunchAnimationProgress(linearProgress) |
| delegate.onLaunchAnimationProgress(state, progress, linearProgress) |
| } |
| } |
| |
| animation = |
| launchAnimator.startAnimation( |
| controller, |
| endState, |
| windowBackgroundColor, |
| fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow, |
| drawHole = !controller.isBelowAnimatingWindow, |
| ) |
| } |
| |
| private fun applyStateToWindow( |
| window: RemoteAnimationTarget, |
| state: LaunchAnimator.State, |
| linearProgress: Float, |
| ) { |
| if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) { |
| // Don't apply any transaction if the view root we synchronize with was detached or |
| // if the SurfaceControl associated with [window] is not valid, as |
| // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. |
| return |
| } |
| |
| val screenBounds = window.screenSpaceBounds |
| val centerX = (screenBounds.left + screenBounds.right) / 2f |
| val centerY = (screenBounds.top + screenBounds.bottom) / 2f |
| val width = screenBounds.right - screenBounds.left |
| val height = screenBounds.bottom - screenBounds.top |
| |
| // Scale the window. We use the max of (widthRatio, heightRatio) so that there is no |
| // blank space on any side. |
| val widthRatio = state.width.toFloat() / width |
| val heightRatio = state.height.toFloat() / height |
| val scale = maxOf(widthRatio, heightRatio) |
| matrix.reset() |
| matrix.setScale(scale, scale, centerX, centerY) |
| |
| // Align it to the top and center it in the x-axis. |
| val heightChange = height * scale - height |
| val translationX = state.centerX - centerX |
| val translationY = state.top - screenBounds.top + heightChange / 2f |
| matrix.postTranslate(translationX, translationY) |
| |
| // Crop it. The matrix will also be applied to the crop, so we apply the inverse |
| // operation. Given that we only scale (by factor > 0) then translate, we can assume |
| // that the matrix is invertible. |
| val cropX = state.left.toFloat() - screenBounds.left |
| val cropY = state.top.toFloat() - screenBounds.top |
| windowCropF.set(cropX, cropY, cropX + state.width, cropY + state.height) |
| matrix.invert(invertMatrix) |
| invertMatrix.mapRect(windowCropF) |
| windowCrop.set( |
| windowCropF.left.roundToInt(), |
| windowCropF.top.roundToInt(), |
| windowCropF.right.roundToInt(), |
| windowCropF.bottom.roundToInt() |
| ) |
| |
| // The alpha of the opening window. If it opens above the expandable, then it should |
| // fade in progressively. Otherwise, it should be fully opaque and will be progressively |
| // revealed as the window background color layer above the window fades out. |
| val alpha = |
| if (controller.isBelowAnimatingWindow) { |
| val windowProgress = |
| LaunchAnimator.getProgress( |
| TIMINGS, |
| linearProgress, |
| TIMINGS.contentAfterFadeInDelay, |
| TIMINGS.contentAfterFadeInDuration |
| ) |
| |
| INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress) |
| } else { |
| 1f |
| } |
| |
| // The scale will also be applied to the corner radius, so we divide by the scale to |
| // keep the original radius. We use the max of (topCornerRadius, bottomCornerRadius) to |
| // make sure that the window does not draw itself behind the expanding view. This is |
| // especially important for lock screen animations, where the window is not clipped by |
| // the shade. |
| val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale |
| val params = |
| SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) |
| .withAlpha(alpha) |
| .withMatrix(matrix) |
| .withWindowCrop(windowCrop) |
| .withCornerRadius(cornerRadius) |
| .withVisibility(true) |
| .build() |
| |
| transactionApplier.scheduleApply(params) |
| } |
| |
| private fun applyStateToNavigationBar( |
| navigationBar: RemoteAnimationTarget, |
| state: LaunchAnimator.State, |
| linearProgress: Float |
| ) { |
| if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) { |
| // Don't apply any transaction if the view root we synchronize with was detached or |
| // if the SurfaceControl associated with [navigationBar] is not valid, as |
| // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw. |
| return |
| } |
| |
| val fadeInProgress = |
| LaunchAnimator.getProgress( |
| TIMINGS, |
| linearProgress, |
| ANIMATION_DELAY_NAV_FADE_IN, |
| ANIMATION_DURATION_NAV_FADE_OUT |
| ) |
| |
| val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) |
| if (fadeInProgress > 0) { |
| matrix.reset() |
| matrix.setTranslate( |
| 0f, |
| (state.top - navigationBar.sourceContainerBounds.top).toFloat() |
| ) |
| windowCrop.set(state.left, 0, state.right, state.height) |
| params |
| .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) |
| .withMatrix(matrix) |
| .withWindowCrop(windowCrop) |
| .withVisibility(true) |
| } else { |
| val fadeOutProgress = |
| LaunchAnimator.getProgress( |
| TIMINGS, |
| linearProgress, |
| 0, |
| ANIMATION_DURATION_NAV_FADE_OUT |
| ) |
| params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) |
| } |
| |
| transactionApplier.scheduleApply(params.build()) |
| } |
| |
| private fun onAnimationTimedOut() { |
| // The remote animation was cancelled by WM, so we already cancelled the launch |
| // animation. |
| if (cancelled) { |
| return |
| } |
| |
| Log.w(TAG, "Remote animation timed out") |
| timedOut = true |
| |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d(TAG, "Calling controller.onLaunchAnimationCancelled() [animation timed out]") |
| } |
| controller.onLaunchAnimationCancelled() |
| listener?.onLaunchAnimationCancelled() |
| } |
| |
| @UiThread |
| override fun onAnimationCancelled() { |
| removeTimeouts() |
| |
| // The short timeout happened, so we already cancelled the launch animation. |
| if (timedOut) { |
| return |
| } |
| |
| Log.i(TAG, "Remote animation was cancelled") |
| cancelled = true |
| |
| animation?.cancel() |
| |
| if (DEBUG_LAUNCH_ANIMATION) { |
| Log.d( |
| TAG, |
| "Calling controller.onLaunchAnimationCancelled() [remote animation cancelled]", |
| ) |
| } |
| controller.onLaunchAnimationCancelled() |
| listener?.onLaunchAnimationCancelled() |
| } |
| |
| private fun IRemoteAnimationFinishedCallback.invoke() { |
| try { |
| onAnimationFinished() |
| } catch (e: RemoteException) { |
| e.printStackTrace() |
| } |
| } |
| } |
| } |