/* * Copyright (C) 2016 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.server.wm; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.os.Debug; import android.util.ArrayMap; import android.util.Slog; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.WindowManagerInternal; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Enables animating bounds of objects. * * In multi-window world bounds of both stack and tasks can change. When we need these bounds to * change smoothly and not require the app to relaunch (e.g. because it handles resizes and * relaunching it would cause poorer experience), these class provides a way to directly animate * the bounds of the resized object. * * The object that is resized needs to implement {@link BoundsAnimationTarget} interface. * * NOTE: All calls to methods in this class should be done on the Animation thread */ public class BoundsAnimationController { private static final boolean DEBUG_LOCAL = false; private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM; private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL ? "BoundsAnimationController" : TAG_WM; private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1; private static final int DEFAULT_TRANSITION_DURATION = 425; @Retention(RetentionPolicy.SOURCE) @IntDef({NO_PIP_MODE_CHANGED_CALLBACKS, SCHEDULE_PIP_MODE_CHANGED_ON_START, SCHEDULE_PIP_MODE_CHANGED_ON_END}) public @interface SchedulePipModeChangedState {} /** Do not schedule any PiP mode changed callbacks as a part of this animation. */ public static final int NO_PIP_MODE_CHANGED_CALLBACKS = 0; /** Schedule a PiP mode changed callback when this animation starts. */ public static final int SCHEDULE_PIP_MODE_CHANGED_ON_START = 1; /** Schedule a PiP mode changed callback when this animation ends. */ public static final int SCHEDULE_PIP_MODE_CHANGED_ON_END = 2; // Only accessed on UI thread. private ArrayMap mRunningAnimations = new ArrayMap<>(); private final class AppTransitionNotifier extends WindowManagerInternal.AppTransitionListener implements Runnable { public void onAppTransitionCancelledLocked() { if (DEBUG) Slog.d(TAG, "onAppTransitionCancelledLocked:" + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition); animationFinished(); } public void onAppTransitionFinishedLocked(IBinder token) { if (DEBUG) Slog.d(TAG, "onAppTransitionFinishedLocked:" + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition); animationFinished(); } private void animationFinished() { if (mFinishAnimationAfterTransition) { mHandler.removeCallbacks(this); // This might end up calling into activity manager which will be bad since we have // the window manager lock held at this point. Post a message to take care of the // processing so we don't deadlock. mHandler.post(this); } } @Override public void run() { for (int i = 0; i < mRunningAnimations.size(); i++) { final BoundsAnimator b = mRunningAnimations.valueAt(i); b.onAnimationEnd(null); } } } private final Handler mHandler; private final AppTransition mAppTransition; private final AppTransitionNotifier mAppTransitionNotifier = new AppTransitionNotifier(); private final Interpolator mFastOutSlowInInterpolator; private boolean mFinishAnimationAfterTransition = false; private final AnimationHandler mAnimationHandler; private static final int WAIT_FOR_DRAW_TIMEOUT_MS = 3000; BoundsAnimationController(Context context, AppTransition transition, Handler handler, AnimationHandler animationHandler) { mHandler = handler; mAppTransition = transition; mAppTransition.registerListenerLocked(mAppTransitionNotifier); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, com.android.internal.R.interpolator.fast_out_slow_in); mAnimationHandler = animationHandler; } @VisibleForTesting final class BoundsAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { private final BoundsAnimationTarget mTarget; private final Rect mFrom = new Rect(); private final Rect mTo = new Rect(); private final Rect mTmpRect = new Rect(); private final Rect mTmpTaskBounds = new Rect(); // True if this this animation was canceled and will be replaced the another animation from // the same {@link #BoundsAnimationTarget} target. private boolean mSkipFinalResize; // True if this animation was canceled by the user, not as a part of a replacing animation private boolean mSkipAnimationEnd; // True if the animation target is animating from the fullscreen. Only one of // {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be true at any time in the // animation. private boolean mMoveFromFullscreen; // True if the animation target should be moved to the fullscreen stack at the end of this // animation. Only one of {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be // true at any time in the animation. private boolean mMoveToFullscreen; // Whether to schedule PiP mode changes on animation start/end private @SchedulePipModeChangedState int mSchedulePipModeChangedState; private @SchedulePipModeChangedState int mPrevSchedulePipModeChangedState; // Depending on whether we are animating from // a smaller to a larger size private final int mFrozenTaskWidth; private final int mFrozenTaskHeight; // Timeout callback to ensure we continue the animation if waiting for resuming or app // windows drawn fails private final Runnable mResumeRunnable = () -> resume(); BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to, @SchedulePipModeChangedState int schedulePipModeChangedState, @SchedulePipModeChangedState int prevShedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen) { super(); mTarget = target; mFrom.set(from); mTo.set(to); mSchedulePipModeChangedState = schedulePipModeChangedState; mPrevSchedulePipModeChangedState = prevShedulePipModeChangedState; mMoveFromFullscreen = moveFromFullscreen; mMoveToFullscreen = moveToFullscreen; addUpdateListener(this); addListener(this); // If we are animating from smaller to larger, we want to change the task bounds // to their final size immediately so we can use scaling to make the window // larger. Likewise if we are going from bigger to smaller, we want to wait until // the end so we don't have to upscale from the smaller finished size. if (animatingToLargerSize()) { mFrozenTaskWidth = mTo.width(); mFrozenTaskHeight = mTo.height(); } else { mFrozenTaskWidth = mFrom.width(); mFrozenTaskHeight = mFrom.height(); } } @Override public void onAnimationStart(Animator animation) { if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget + " mPrevSchedulePipModeChangedState=" + mPrevSchedulePipModeChangedState + " mSchedulePipModeChangedState=" + mSchedulePipModeChangedState); mFinishAnimationAfterTransition = false; mTmpRect.set(mFrom.left, mFrom.top, mFrom.left + mFrozenTaskWidth, mFrom.top + mFrozenTaskHeight); // Boost the thread priority of the animation thread while the bounds animation is // running updateBooster(); // Ensure that we have prepared the target for animation before we trigger any size // changes, so it can swap surfaces in to appropriate modes, or do as it wishes // otherwise. if (mPrevSchedulePipModeChangedState == NO_PIP_MODE_CHANGED_CALLBACKS) { mTarget.onAnimationStart(mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START, false /* forceUpdate */); // When starting an animation from fullscreen, pause here and wait for the // windows-drawn signal before we start the rest of the transition down into PiP. if (mMoveFromFullscreen) { pause(); } } else if (mPrevSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END && mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) { // We are replacing a running animation into PiP, but since it hasn't completed, the // client will not currently receive any picture-in-picture mode change callbacks. // However, we still need to report to them that they are leaving PiP, so this will // force an update via a mode changed callback. mTarget.onAnimationStart(true /* schedulePipModeChangedCallback */, true /* forceUpdate */); } // Immediately update the task bounds if they have to become larger, but preserve // the starting position so we don't jump at the beginning of the animation. if (animatingToLargerSize()) { mTarget.setPinnedStackSize(mFrom, mTmpRect); // We pause the animation until the app has drawn at the new size. // The target will notify us via BoundsAnimationController#resume. // We do this here and pause the animation, rather than just defer starting it // so we can enter the animating state and have WindowStateAnimator apply the // correct logic to make this resize seamless. if (mMoveToFullscreen) { pause(); } } } @Override public void pause() { if (DEBUG) Slog.d(TAG, "pause: waiting for windows drawn"); super.pause(); mHandler.postDelayed(mResumeRunnable, WAIT_FOR_DRAW_TIMEOUT_MS); } @Override public void resume() { if (DEBUG) Slog.d(TAG, "resume:"); mHandler.removeCallbacks(mResumeRunnable); super.resume(); } @Override public void onAnimationUpdate(ValueAnimator animation) { final float value = (Float) animation.getAnimatedValue(); final float remains = 1 - value; mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f); mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f); mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f); mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f); if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds=" + mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value + " remains=" + remains); mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top, mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight); if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) { // Whoops, the target doesn't feel like animating anymore. Let's immediately finish // any further animation. if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled"); // If we have already scheduled a PiP mode changed at the start of the animation, // then we need to clean up and schedule one at the end, since we have canceled the // animation to the final state. if (mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) { mSchedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END; } // Since we are cancelling immediately without a replacement animation, send the // animation end to maintain callback parity, but also skip any further resizes cancelAndCallAnimationEnd(); } } @Override public void onAnimationEnd(Animator animation) { if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget + " mSkipFinalResize=" + mSkipFinalResize + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition + " mAppTransitionIsRunning=" + mAppTransition.isRunning() + " callers=" + Debug.getCallers(2)); // There could be another animation running. For example in the // move to fullscreen case, recents will also be closing while the // previous task will be taking its place in the fullscreen stack. // we have to ensure this is completed before we finish the animation // and take our place in the fullscreen stack. if (mAppTransition.isRunning() && !mFinishAnimationAfterTransition) { mFinishAnimationAfterTransition = true; return; } if (!mSkipAnimationEnd) { // If this animation has already scheduled the picture-in-picture mode on start, and // we are not skipping the final resize due to being canceled, then move the PiP to // fullscreen once the animation ends if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget + " moveToFullscreen=" + mMoveToFullscreen); mTarget.onAnimationEnd(mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END, !mSkipFinalResize ? mTo : null, mMoveToFullscreen); } // Clean up this animation removeListener(this); removeUpdateListener(this); mRunningAnimations.remove(mTarget); // Reset the thread priority of the animation thread after the bounds animation is done updateBooster(); } @Override public void onAnimationCancel(Animator animation) { // Always skip the final resize when the animation is canceled mSkipFinalResize = true; mMoveToFullscreen = false; } private void cancelAndCallAnimationEnd() { if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget); mSkipAnimationEnd = false; super.cancel(); } @Override public void cancel() { if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget); mSkipAnimationEnd = true; super.cancel(); } /** * @return true if the animation target is the same as the input bounds. */ boolean isAnimatingTo(Rect bounds) { return mTo.equals(bounds); } /** * @return true if we are animating to a larger surface size */ @VisibleForTesting boolean animatingToLargerSize() { // TODO: Fix this check for aspect ratio changes return (mFrom.width() * mFrom.height() <= mTo.width() * mTo.height()); } @Override public void onAnimationRepeat(Animator animation) { // Do nothing } @Override public AnimationHandler getAnimationHandler() { if (mAnimationHandler != null) { return mAnimationHandler; } return super.getAnimationHandler(); } } public void animateBounds(final BoundsAnimationTarget target, Rect from, Rect to, int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen) { animateBoundsImpl(target, from, to, animationDuration, schedulePipModeChangedState, moveFromFullscreen, moveToFullscreen); } @VisibleForTesting BoundsAnimator animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to, int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen) { final BoundsAnimator existing = mRunningAnimations.get(target); final boolean replacing = existing != null; @SchedulePipModeChangedState int prevSchedulePipModeChangedState = NO_PIP_MODE_CHANGED_CALLBACKS; if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to + " schedulePipModeChangedState=" + schedulePipModeChangedState + " replacing=" + replacing); if (replacing) { if (existing.isAnimatingTo(to)) { // Just let the current animation complete if it has the same destination as the // one we are trying to start. if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing + " ignoring..."); return existing; } // Save the previous state prevSchedulePipModeChangedState = existing.mSchedulePipModeChangedState; // Update the PiP callback states if we are replacing the animation if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) { if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) { if (DEBUG) Slog.d(TAG, "animateBounds: still animating to fullscreen, keep" + " existing deferred state"); } else { if (DEBUG) Slog.d(TAG, "animateBounds: fullscreen animation canceled, callback" + " on start already processed, schedule deferred update on end"); schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END; } } else if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END) { if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) { if (DEBUG) Slog.d(TAG, "animateBounds: non-fullscreen animation canceled," + " callback on start will be processed"); } else { if (DEBUG) Slog.d(TAG, "animateBounds: still animating from fullscreen, keep" + " existing deferred state"); schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END; } } // Since we are replacing, we skip both animation start and end callbacks existing.cancel(); } final BoundsAnimator animator = new BoundsAnimator(target, from, to, schedulePipModeChangedState, prevSchedulePipModeChangedState, moveFromFullscreen, moveToFullscreen); mRunningAnimations.put(target, animator); animator.setFloatValues(0f, 1f); animator.setDuration((animationDuration != -1 ? animationDuration : DEFAULT_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR); animator.setInterpolator(mFastOutSlowInInterpolator); animator.start(); return animator; } public Handler getHandler() { return mHandler; } public void onAllWindowsDrawn() { if (DEBUG) Slog.d(TAG, "onAllWindowsDrawn:"); mHandler.post(this::resume); } private void resume() { for (int i = 0; i < mRunningAnimations.size(); i++) { final BoundsAnimator b = mRunningAnimations.valueAt(i); b.resume(); } } private void updateBooster() { WindowManagerService.sThreadPriorityBooster.setBoundsAnimationRunning( !mRunningAnimations.isEmpty()); } }