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 
onAppTransitionCancelledLocked()80         public void onAppTransitionCancelledLocked() {
81             if (DEBUG) Slog.d(TAG, "onAppTransitionCancelledLocked:"
82                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
83             animationFinished();
84         }
onAppTransitionFinishedLocked(IBinder token)85         public void onAppTransitionFinishedLocked(IBinder token) {
86             if (DEBUG) Slog.d(TAG, "onAppTransitionFinishedLocked:"
87                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition);
88             animationFinished();
89         }
animationFinished()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
run()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 
BoundsAnimationController(Context context, AppTransition transition, Handler handler, AnimationHandler animationHandler)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 
BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to, @SchedulePipModeChangedState int schedulePipModeChangedState, @SchedulePipModeChangedState int prevShedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen)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
onAnimationStart(Animator animation)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
pause()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
resume()256         public void resume() {
257             if (DEBUG) Slog.d(TAG, "resume:");
258             mHandler.removeCallbacks(mResumeRunnable);
259             super.resume();
260         }
261 
262         @Override
onAnimationUpdate(ValueAnimator animation)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
onAnimationEnd(Animator animation)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
onAnimationCancel(Animator animation)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 
cancelAndCallAnimationEnd()340         private void cancelAndCallAnimationEnd() {
341             if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget);
342             mSkipAnimationEnd = false;
343             super.cancel();
344         }
345 
346         @Override
cancel()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          */
isAnimatingTo(Rect bounds)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
animatingToLargerSize()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
onAnimationRepeat(Animator animation)370         public void onAnimationRepeat(Animator animation) {
371             // Do nothing
372         }
373 
374         @Override
getAnimationHandler()375         public AnimationHandler getAnimationHandler() {
376             if (mAnimationHandler != null) {
377                 return mAnimationHandler;
378             }
379             return super.getAnimationHandler();
380         }
381     }
382 
animateBounds(final BoundsAnimationTarget target, Rect from, Rect to, int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen)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
animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to, int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState, boolean moveFromFullscreen, boolean moveToFullscreen)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 
getHandler()460     public Handler getHandler() {
461         return mHandler;
462     }
463 
onAllWindowsDrawn()464     public void onAllWindowsDrawn() {
465         if (DEBUG) Slog.d(TAG, "onAllWindowsDrawn:");
466         mHandler.post(this::resume);
467     }
468 
resume()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 
updateBooster()476     private void updateBooster() {
477         WindowManagerService.sThreadPriorityBooster.setBoundsAnimationRunning(
478                 !mRunningAnimations.isEmpty());
479     }
480 }
481