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