Home | History | Annotate | Download | only in wm
      1 /*
      2  * Copyright (C) 2016 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.server.wm;
     18 
     19 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
     20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
     21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
     22 
     23 import android.animation.AnimationHandler;
     24 import android.animation.Animator;
     25 import android.animation.ValueAnimator;
     26 import android.annotation.IntDef;
     27 import android.content.Context;
     28 import android.graphics.Rect;
     29 import android.os.Handler;
     30 import android.os.IBinder;
     31 import android.os.Debug;
     32 import android.util.ArrayMap;
     33 import android.util.Slog;
     34 import android.view.animation.AnimationUtils;
     35 import android.view.animation.Interpolator;
     36 
     37 import com.android.internal.annotations.VisibleForTesting;
     38 
     39 import java.lang.annotation.Retention;
     40 import java.lang.annotation.RetentionPolicy;
     41 
     42 /**
     43  * Enables animating bounds of objects.
     44  *
     45  * In multi-window world bounds of both stack and tasks can change. When we need these bounds to
     46  * change smoothly and not require the app to relaunch (e.g. because it handles resizes and
     47  * relaunching it would cause poorer experience), these class provides a way to directly animate
     48  * the bounds of the resized object.
     49  *
     50  * The object that is resized needs to implement {@link BoundsAnimationTarget} interface.
     51  *
     52  * NOTE: All calls to methods in this class should be done on the Animation thread
     53  */
     54 public class BoundsAnimationController {
     55     private static final boolean DEBUG_LOCAL = false;
     56     private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM;
     57     private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL
     58             ? "BoundsAnimationController" : TAG_WM;
     59     private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1;
     60 
     61     private static final int DEFAULT_TRANSITION_DURATION = 425;
     62 
     63     @Retention(RetentionPolicy.SOURCE)
     64     @IntDef({NO_PIP_MODE_CHANGED_CALLBACKS, SCHEDULE_PIP_MODE_CHANGED_ON_START,
     65         SCHEDULE_PIP_MODE_CHANGED_ON_END})
     66     public @interface SchedulePipModeChangedState {}
     67     /** Do not schedule any PiP mode changed callbacks as a part of this animation. */
     68     public static final int NO_PIP_MODE_CHANGED_CALLBACKS = 0;
     69     /** Schedule a PiP mode changed callback when this animation starts. */
     70     public static final int SCHEDULE_PIP_MODE_CHANGED_ON_START = 1;
     71     /** Schedule a PiP mode changed callback when this animation ends. */
     72     public static final int SCHEDULE_PIP_MODE_CHANGED_ON_END = 2;
     73 
     74     // Only accessed on UI thread.
     75     private ArrayMap<BoundsAnimationTarget, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
     76 
     77     private final class AppTransitionNotifier
     78             extends WindowManagerInternal.AppTransitionListener implements Runnable {
     79 
     80         public void onAppTransitionCancelledLocked() {
     81             if (DEBUG) Slog.d(TAG, "onAppTransitionCancelledLocked:"
     82                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
     83             animationFinished();
     84         }
     85         public void onAppTransitionFinishedLocked(IBinder token) {
     86             if (DEBUG) Slog.d(TAG, "onAppTransitionFinishedLocked:"
     87                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
     88             animationFinished();
     89         }
     90         private void animationFinished() {
     91             if (mFinishAnimationAfterTransition) {
     92                 mHandler.removeCallbacks(this);
     93                 // This might end up calling into activity manager which will be bad since we have
     94                 // the window manager lock held at this point. Post a message to take care of the
     95                 // processing so we don't deadlock.
     96                 mHandler.post(this);
     97             }
     98         }
     99 
    100         @Override
    101         public void run() {
    102             for (int i = 0; i < mRunningAnimations.size(); i++) {
    103                 final BoundsAnimator b = mRunningAnimations.valueAt(i);
    104                 b.onAnimationEnd(null);
    105             }
    106         }
    107     }
    108 
    109     private final Handler mHandler;
    110     private final AppTransition mAppTransition;
    111     private final AppTransitionNotifier mAppTransitionNotifier = new AppTransitionNotifier();
    112     private final Interpolator mFastOutSlowInInterpolator;
    113     private boolean mFinishAnimationAfterTransition = false;
    114     private final AnimationHandler mAnimationHandler;
    115 
    116     private static final int WAIT_FOR_DRAW_TIMEOUT_MS = 3000;
    117 
    118     BoundsAnimationController(Context context, AppTransition transition, Handler handler,
    119             AnimationHandler animationHandler) {
    120         mHandler = handler;
    121         mAppTransition = transition;
    122         mAppTransition.registerListenerLocked(mAppTransitionNotifier);
    123         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
    124                 com.android.internal.R.interpolator.fast_out_slow_in);
    125         mAnimationHandler = animationHandler;
    126     }
    127 
    128     @VisibleForTesting
    129     final class BoundsAnimator extends ValueAnimator
    130             implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
    131 
    132         private final BoundsAnimationTarget mTarget;
    133         private final Rect mFrom = new Rect();
    134         private final Rect mTo = new Rect();
    135         private final Rect mTmpRect = new Rect();
    136         private final Rect mTmpTaskBounds = new Rect();
    137 
    138         // True if this this animation was canceled and will be replaced the another animation from
    139         // the same {@link #BoundsAnimationTarget} target.
    140         private boolean mSkipFinalResize;
    141         // True if this animation was canceled by the user, not as a part of a replacing animation
    142         private boolean mSkipAnimationEnd;
    143 
    144         // True if the animation target is animating from the fullscreen. Only one of
    145         // {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be true at any time in the
    146         // animation.
    147         private boolean mMoveFromFullscreen;
    148         // True if the animation target should be moved to the fullscreen stack at the end of this
    149         // animation. Only one of {@link mMoveToFullscreen} or {@link mMoveFromFullscreen} can be
    150         // true at any time in the animation.
    151         private boolean mMoveToFullscreen;
    152 
    153         // Whether to schedule PiP mode changes on animation start/end
    154         private @SchedulePipModeChangedState int mSchedulePipModeChangedState;
    155         private @SchedulePipModeChangedState int mPrevSchedulePipModeChangedState;
    156 
    157         // Depending on whether we are animating from
    158         // a smaller to a larger size
    159         private final int mFrozenTaskWidth;
    160         private final int mFrozenTaskHeight;
    161 
    162         // Timeout callback to ensure we continue the animation if waiting for resuming or app
    163         // windows drawn fails
    164         private final Runnable mResumeRunnable = () -> {
    165             if (DEBUG) Slog.d(TAG, "pause: timed out waiting for windows drawn");
    166             resume();
    167         };
    168 
    169         BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to,
    170                 @SchedulePipModeChangedState int schedulePipModeChangedState,
    171                 @SchedulePipModeChangedState int prevShedulePipModeChangedState,
    172                 boolean moveFromFullscreen, boolean moveToFullscreen) {
    173             super();
    174             mTarget = target;
    175             mFrom.set(from);
    176             mTo.set(to);
    177             mSchedulePipModeChangedState = schedulePipModeChangedState;
    178             mPrevSchedulePipModeChangedState = prevShedulePipModeChangedState;
    179             mMoveFromFullscreen = moveFromFullscreen;
    180             mMoveToFullscreen = moveToFullscreen;
    181             addUpdateListener(this);
    182             addListener(this);
    183 
    184             // If we are animating from smaller to larger, we want to change the task bounds
    185             // to their final size immediately so we can use scaling to make the window
    186             // larger. Likewise if we are going from bigger to smaller, we want to wait until
    187             // the end so we don't have to upscale from the smaller finished size.
    188             if (animatingToLargerSize()) {
    189                 mFrozenTaskWidth = mTo.width();
    190                 mFrozenTaskHeight = mTo.height();
    191             } else {
    192                 mFrozenTaskWidth = mFrom.width();
    193                 mFrozenTaskHeight = mFrom.height();
    194             }
    195         }
    196 
    197         @Override
    198         public void onAnimationStart(Animator animation) {
    199             if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
    200                     + " mPrevSchedulePipModeChangedState=" + mPrevSchedulePipModeChangedState
    201                     + " mSchedulePipModeChangedState=" + mSchedulePipModeChangedState);
    202             mFinishAnimationAfterTransition = false;
    203             mTmpRect.set(mFrom.left, mFrom.top, mFrom.left + mFrozenTaskWidth,
    204                     mFrom.top + mFrozenTaskHeight);
    205 
    206             // Boost the thread priority of the animation thread while the bounds animation is
    207             // running
    208             updateBooster();
    209 
    210             // Ensure that we have prepared the target for animation before we trigger any size
    211             // changes, so it can swap surfaces in to appropriate modes, or do as it wishes
    212             // otherwise.
    213             if (mPrevSchedulePipModeChangedState == NO_PIP_MODE_CHANGED_CALLBACKS) {
    214                 mTarget.onAnimationStart(mSchedulePipModeChangedState ==
    215                         SCHEDULE_PIP_MODE_CHANGED_ON_START, false /* forceUpdate */);
    216 
    217                 // When starting an animation from fullscreen, pause here and wait for the
    218                 // windows-drawn signal before we start the rest of the transition down into PiP.
    219                 if (mMoveFromFullscreen && mTarget.shouldDeferStartOnMoveToFullscreen()) {
    220                     pause();
    221                 }
    222             } else if (mPrevSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END &&
    223                     mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
    224                 // We are replacing a running animation into PiP, but since it hasn't completed, the
    225                 // client will not currently receive any picture-in-picture mode change callbacks.
    226                 // However, we still need to report to them that they are leaving PiP, so this will
    227                 // force an update via a mode changed callback.
    228                 mTarget.onAnimationStart(true /* schedulePipModeChangedCallback */,
    229                         true /* forceUpdate */);
    230             }
    231 
    232             // Immediately update the task bounds if they have to become larger, but preserve
    233             // the starting position so we don't jump at the beginning of the animation.
    234             if (animatingToLargerSize()) {
    235                 mTarget.setPinnedStackSize(mFrom, mTmpRect);
    236 
    237                 // We pause the animation until the app has drawn at the new size.
    238                 // The target will notify us via BoundsAnimationController#resume.
    239                 // We do this here and pause the animation, rather than just defer starting it
    240                 // so we can enter the animating state and have WindowStateAnimator apply the
    241                 // correct logic to make this resize seamless.
    242                 if (mMoveToFullscreen) {
    243                     pause();
    244                 }
    245             }
    246         }
    247 
    248         @Override
    249         public void pause() {
    250             if (DEBUG) Slog.d(TAG, "pause: waiting for windows drawn");
    251             super.pause();
    252             mHandler.postDelayed(mResumeRunnable, WAIT_FOR_DRAW_TIMEOUT_MS);
    253         }
    254 
    255         @Override
    256         public void resume() {
    257             if (DEBUG) Slog.d(TAG, "resume:");
    258             mHandler.removeCallbacks(mResumeRunnable);
    259             super.resume();
    260         }
    261 
    262         @Override
    263         public void onAnimationUpdate(ValueAnimator animation) {
    264             final float value = (Float) animation.getAnimatedValue();
    265             final float remains = 1 - value;
    266             mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f);
    267             mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f);
    268             mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f);
    269             mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f);
    270             if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds="
    271                     + mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value
    272                     + " remains=" + remains);
    273 
    274             mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top,
    275                     mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight);
    276 
    277             if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
    278                 // Whoops, the target doesn't feel like animating anymore. Let's immediately finish
    279                 // any further animation.
    280                 if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled");
    281 
    282                 // If we have already scheduled a PiP mode changed at the start of the animation,
    283                 // then we need to clean up and schedule one at the end, since we have canceled the
    284                 // animation to the final state.
    285                 if (mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
    286                     mSchedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
    287                 }
    288 
    289                 // Since we are cancelling immediately without a replacement animation, send the
    290                 // animation end to maintain callback parity, but also skip any further resizes
    291                 cancelAndCallAnimationEnd();
    292             }
    293         }
    294 
    295         @Override
    296         public void onAnimationEnd(Animator animation) {
    297             if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
    298                     + " mSkipFinalResize=" + mSkipFinalResize
    299                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition
    300                     + " mAppTransitionIsRunning=" + mAppTransition.isRunning()
    301                     + " callers=" + Debug.getCallers(2));
    302 
    303             // There could be another animation running. For example in the
    304             // move to fullscreen case, recents will also be closing while the
    305             // previous task will be taking its place in the fullscreen stack.
    306             // we have to ensure this is completed before we finish the animation
    307             // and take our place in the fullscreen stack.
    308             if (mAppTransition.isRunning() && !mFinishAnimationAfterTransition) {
    309                 mFinishAnimationAfterTransition = true;
    310                 return;
    311             }
    312 
    313             if (!mSkipAnimationEnd) {
    314                 // If this animation has already scheduled the picture-in-picture mode on start, and
    315                 // we are not skipping the final resize due to being canceled, then move the PiP to
    316                 // fullscreen once the animation ends
    317                 if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
    318                         + " moveToFullscreen=" + mMoveToFullscreen);
    319                 mTarget.onAnimationEnd(mSchedulePipModeChangedState ==
    320                         SCHEDULE_PIP_MODE_CHANGED_ON_END, !mSkipFinalResize ? mTo : null,
    321                                 mMoveToFullscreen);
    322             }
    323 
    324             // Clean up this animation
    325             removeListener(this);
    326             removeUpdateListener(this);
    327             mRunningAnimations.remove(mTarget);
    328 
    329             // Reset the thread priority of the animation thread after the bounds animation is done
    330             updateBooster();
    331         }
    332 
    333         @Override
    334         public void onAnimationCancel(Animator animation) {
    335             // Always skip the final resize when the animation is canceled
    336             mSkipFinalResize = true;
    337             mMoveToFullscreen = false;
    338         }
    339 
    340         private void cancelAndCallAnimationEnd() {
    341             if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget);
    342             mSkipAnimationEnd = false;
    343             super.cancel();
    344         }
    345 
    346         @Override
    347         public void cancel() {
    348             if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget);
    349             mSkipAnimationEnd = true;
    350             super.cancel();
    351         }
    352 
    353         /**
    354          * @return true if the animation target is the same as the input bounds.
    355          */
    356         boolean isAnimatingTo(Rect bounds) {
    357             return mTo.equals(bounds);
    358         }
    359 
    360         /**
    361          * @return true if we are animating to a larger surface size
    362          */
    363         @VisibleForTesting
    364         boolean animatingToLargerSize() {
    365             // TODO: Fix this check for aspect ratio changes
    366             return (mFrom.width() * mFrom.height() <= mTo.width() * mTo.height());
    367         }
    368 
    369         @Override
    370         public void onAnimationRepeat(Animator animation) {
    371             // Do nothing
    372         }
    373 
    374         @Override
    375         public AnimationHandler getAnimationHandler() {
    376             if (mAnimationHandler != null) {
    377                 return mAnimationHandler;
    378             }
    379             return super.getAnimationHandler();
    380         }
    381     }
    382 
    383     public void animateBounds(final BoundsAnimationTarget target, Rect from, Rect to,
    384             int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
    385             boolean moveFromFullscreen, boolean moveToFullscreen) {
    386         animateBoundsImpl(target, from, to, animationDuration, schedulePipModeChangedState,
    387                 moveFromFullscreen, moveToFullscreen);
    388     }
    389 
    390     @VisibleForTesting
    391     BoundsAnimator animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to,
    392             int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
    393             boolean moveFromFullscreen, boolean moveToFullscreen) {
    394         final BoundsAnimator existing = mRunningAnimations.get(target);
    395         final boolean replacing = existing != null;
    396         @SchedulePipModeChangedState int prevSchedulePipModeChangedState =
    397                 NO_PIP_MODE_CHANGED_CALLBACKS;
    398 
    399         if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
    400                 + " schedulePipModeChangedState=" + schedulePipModeChangedState
    401                 + " replacing=" + replacing);
    402 
    403         if (replacing) {
    404             if (existing.isAnimatingTo(to) && (!moveToFullscreen || existing.mMoveToFullscreen)
    405                     && (!moveFromFullscreen || existing.mMoveFromFullscreen)) {
    406                 // Just let the current animation complete if it has the same destination as the
    407                 // one we are trying to start, and, if moveTo/FromFullscreen was requested, already
    408                 // has that flag set.
    409                 if (DEBUG) Slog.d(TAG, "animateBounds: same destination and moveTo/From flags as "
    410                         + "existing=" + existing + ", ignoring...");
    411                 return existing;
    412             }
    413 
    414             // Save the previous state
    415             prevSchedulePipModeChangedState = existing.mSchedulePipModeChangedState;
    416 
    417             // Update the PiP callback states if we are replacing the animation
    418             if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
    419                 if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
    420                     if (DEBUG) Slog.d(TAG, "animateBounds: still animating to fullscreen, keep"
    421                             + " existing deferred state");
    422                 } else {
    423                     if (DEBUG) Slog.d(TAG, "animateBounds: fullscreen animation canceled, callback"
    424                             + " on start already processed, schedule deferred update on end");
    425                     schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
    426                 }
    427             } else if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END) {
    428                 if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
    429                     if (DEBUG) Slog.d(TAG, "animateBounds: non-fullscreen animation canceled,"
    430                             + " callback on start will be processed");
    431                 } else {
    432                     if (DEBUG) Slog.d(TAG, "animateBounds: still animating from fullscreen, keep"
    433                             + " existing deferred state");
    434                     schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
    435                 }
    436             }
    437 
    438             // We need to keep the previous moveTo/FromFullscreen flag, unless the new animation
    439             // specifies a direction.
    440             if (!moveFromFullscreen && !moveToFullscreen) {
    441                 moveToFullscreen = existing.mMoveToFullscreen;
    442                 moveFromFullscreen = existing.mMoveFromFullscreen;
    443             }
    444 
    445             // Since we are replacing, we skip both animation start and end callbacks
    446             existing.cancel();
    447         }
    448         final BoundsAnimator animator = new BoundsAnimator(target, from, to,
    449                 schedulePipModeChangedState, prevSchedulePipModeChangedState,
    450                 moveFromFullscreen, moveToFullscreen);
    451         mRunningAnimations.put(target, animator);
    452         animator.setFloatValues(0f, 1f);
    453         animator.setDuration((animationDuration != -1 ? animationDuration
    454                 : DEFAULT_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
    455         animator.setInterpolator(mFastOutSlowInInterpolator);
    456         animator.start();
    457         return animator;
    458     }
    459 
    460     public Handler getHandler() {
    461         return mHandler;
    462     }
    463 
    464     public void onAllWindowsDrawn() {
    465         if (DEBUG) Slog.d(TAG, "onAllWindowsDrawn:");
    466         mHandler.post(this::resume);
    467     }
    468 
    469     private void resume() {
    470         for (int i = 0; i < mRunningAnimations.size(); i++) {
    471             final BoundsAnimator b = mRunningAnimations.valueAt(i);
    472             b.resume();
    473         }
    474     }
    475 
    476     private void updateBooster() {
    477         WindowManagerService.sThreadPriorityBooster.setBoundsAnimationRunning(
    478                 !mRunningAnimations.isEmpty());
    479     }
    480 }
    481