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.AppTransition.DEFAULT_APP_TRANSITION_DURATION;
20import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
21import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
22import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
23
24import android.animation.Animator;
25import android.animation.ValueAnimator;
26import android.graphics.Rect;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Debug;
30import android.util.ArrayMap;
31import android.util.Slog;
32import android.view.animation.LinearInterpolator;
33import android.view.WindowManagerInternal;
34
35/**
36 * Enables animating bounds of objects.
37 *
38 * In multi-window world bounds of both stack and tasks can change. When we need these bounds to
39 * change smoothly and not require the app to relaunch (e.g. because it handles resizes and
40 * relaunching it would cause poorer experience), these class provides a way to directly animate
41 * the bounds of the resized object.
42 *
43 * The object that is resized needs to implement {@link AnimateBoundsUser} interface.
44 *
45 * NOTE: All calls to methods in this class should be done on the UI thread
46 */
47public class BoundsAnimationController {
48    private static final boolean DEBUG_LOCAL = false;
49    private static final boolean DEBUG = DEBUG_LOCAL || DEBUG_ANIM;
50    private static final String TAG = TAG_WITH_CLASS_NAME || DEBUG_LOCAL
51            ? "BoundsAnimationController" : TAG_WM;
52    private static final int DEBUG_ANIMATION_SLOW_DOWN_FACTOR = 1;
53
54    // Only accessed on UI thread.
55    private ArrayMap<AnimateBoundsUser, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
56
57    private final class AppTransitionNotifier
58            extends WindowManagerInternal.AppTransitionListener implements Runnable {
59
60        public void onAppTransitionCancelledLocked() {
61            animationFinished();
62        }
63        public void onAppTransitionFinishedLocked(IBinder token) {
64            animationFinished();
65        }
66        private void animationFinished() {
67            if (mFinishAnimationAfterTransition) {
68                mHandler.removeCallbacks(this);
69                // This might end up calling into activity manager which will be bad since we have the
70                // window manager lock held at this point. Post a message to take care of the processing
71                // so we don't deadlock.
72                mHandler.post(this);
73            }
74        }
75
76        @Override
77        public void run() {
78            for (int i = 0; i < mRunningAnimations.size(); i++) {
79                final BoundsAnimator b = mRunningAnimations.valueAt(i);
80                b.onAnimationEnd(null);
81            }
82        }
83    }
84
85    private final Handler mHandler;
86    private final AppTransition mAppTransition;
87    private final AppTransitionNotifier mAppTransitionNotifier = new AppTransitionNotifier();
88    private boolean mFinishAnimationAfterTransition = false;
89
90    BoundsAnimationController(AppTransition transition, Handler handler) {
91        mHandler = handler;
92        mAppTransition = transition;
93        mAppTransition.registerListenerLocked(mAppTransitionNotifier);
94    }
95
96    private final class BoundsAnimator extends ValueAnimator
97            implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
98        private final AnimateBoundsUser mTarget;
99        private final Rect mFrom;
100        private final Rect mTo;
101        private final Rect mTmpRect = new Rect();
102        private final Rect mTmpTaskBounds = new Rect();
103        private final boolean mMoveToFullScreen;
104        // True if this this animation was cancelled and will be replaced the another animation from
105        // the same {@link #AnimateBoundsUser} target.
106        private boolean mWillReplace;
107        // True to true if this animation replaced a previous animation of the same
108        // {@link #AnimateBoundsUser} target.
109        private final boolean mReplacement;
110
111        // Depending on whether we are animating from
112        // a smaller to a larger size
113        private final int mFrozenTaskWidth;
114        private final int mFrozenTaskHeight;
115
116        BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to,
117                boolean moveToFullScreen, boolean replacement) {
118            super();
119            mTarget = target;
120            mFrom = from;
121            mTo = to;
122            mMoveToFullScreen = moveToFullScreen;
123            mReplacement = replacement;
124            addUpdateListener(this);
125            addListener(this);
126
127            // If we are animating from smaller to larger, we want to change the task bounds
128            // to their final size immediately so we can use scaling to make the window
129            // larger. Likewise if we are going from bigger to smaller, we want to wait until
130            // the end so we don't have to upscale from the smaller finished size.
131            if (animatingToLargerSize()) {
132                mFrozenTaskWidth = mTo.width();
133                mFrozenTaskHeight = mTo.height();
134            } else {
135                mFrozenTaskWidth = mFrom.width();
136                mFrozenTaskHeight = mFrom.height();
137            }
138        }
139
140        boolean animatingToLargerSize() {
141            if (mFrom.width() * mFrom.height() > mTo.width() * mTo.height()) {
142                return false;
143            }
144            return true;
145        }
146
147        @Override
148        public void onAnimationUpdate(ValueAnimator animation) {
149            final float value = (Float) animation.getAnimatedValue();
150            final float remains = 1 - value;
151            mTmpRect.left = (int) (mFrom.left * remains + mTo.left * value + 0.5f);
152            mTmpRect.top = (int) (mFrom.top * remains + mTo.top * value + 0.5f);
153            mTmpRect.right = (int) (mFrom.right * remains + mTo.right * value + 0.5f);
154            mTmpRect.bottom = (int) (mFrom.bottom * remains + mTo.bottom * value + 0.5f);
155            if (DEBUG) Slog.d(TAG, "animateUpdate: mTarget=" + mTarget + " mBounds="
156                    + mTmpRect + " from=" + mFrom + " mTo=" + mTo + " value=" + value
157                    + " remains=" + remains);
158
159            mTmpTaskBounds.set(mTmpRect.left, mTmpRect.top,
160                    mTmpRect.left + mFrozenTaskWidth, mTmpRect.top + mFrozenTaskHeight);
161
162            if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
163                // Whoops, the target doesn't feel like animating anymore. Let's immediately finish
164                // any further animation.
165                animation.cancel();
166            }
167        }
168
169
170        @Override
171        public void onAnimationStart(Animator animation) {
172            if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
173                    + " mReplacement=" + mReplacement);
174            mFinishAnimationAfterTransition = false;
175            // Ensure that we have prepared the target for animation before
176            // we trigger any size changes, so it can swap surfaces
177            // in to appropriate modes, or do as it wishes otherwise.
178            if (!mReplacement) {
179                mTarget.onAnimationStart();
180            }
181
182            // Immediately update the task bounds if they have to become larger, but preserve
183            // the starting position so we don't jump at the beginning of the animation.
184            if (animatingToLargerSize()) {
185                mTmpRect.set(mFrom.left, mFrom.top,
186                        mFrom.left + mFrozenTaskWidth, mFrom.top + mFrozenTaskHeight);
187                mTarget.setPinnedStackSize(mFrom, mTmpRect);
188            }
189        }
190
191        @Override
192        public void onAnimationEnd(Animator animation) {
193            if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
194                    + " mMoveToFullScreen=" + mMoveToFullScreen + " mWillReplace=" + mWillReplace);
195
196            // There could be another animation running. For example in the
197            // move to fullscreen case, recents will also be closing while the
198            // previous task will be taking its place in the fullscreen stack.
199            // we have to ensure this is completed before we finish the animation
200            // and take our place in the fullscreen stack.
201            if (mAppTransition.isRunning() && !mFinishAnimationAfterTransition) {
202                mFinishAnimationAfterTransition = true;
203                return;
204            }
205
206            finishAnimation();
207
208            mTarget.setPinnedStackSize(mTo, null);
209            if (mMoveToFullScreen && !mWillReplace) {
210                mTarget.moveToFullscreen();
211            }
212        }
213
214        @Override
215        public void onAnimationCancel(Animator animation) {
216            finishAnimation();
217        }
218
219        @Override
220        public void cancel() {
221            mWillReplace = true;
222            if (DEBUG) Slog.d(TAG, "cancel: willReplace mTarget=" + mTarget);
223            super.cancel();
224        }
225
226        /** Returns true if the animation target is the same as the input bounds. */
227        public boolean isAnimatingTo(Rect bounds) {
228            return mTo.equals(bounds);
229        }
230
231        private void finishAnimation() {
232            if (DEBUG) Slog.d(TAG, "finishAnimation: mTarget=" + mTarget
233                    + " callers" + Debug.getCallers(2));
234            if (!mWillReplace) {
235                mTarget.onAnimationEnd();
236            }
237            removeListener(this);
238            removeUpdateListener(this);
239            mRunningAnimations.remove(mTarget);
240        }
241
242        @Override
243        public void onAnimationRepeat(Animator animation) {
244
245        }
246    }
247
248    public interface AnimateBoundsUser {
249        /**
250         * Asks the target to directly (without any intermediate steps, like scheduling animation)
251         * resize its bounds.
252         *
253         * @return Whether the target still wants to be animated and successfully finished the
254         * operation. If it returns false, the animation will immediately be cancelled. The target
255         * should return false when something abnormal happened, e.g. it was completely removed
256         * from the hierarchy and is not valid anymore.
257         */
258        boolean setSize(Rect bounds);
259        /**
260         * Behaves as setSize, but freezes the bounds of any tasks in the target at taskBounds,
261         * to allow for more flexibility during resizing. Only
262         * works for the pinned stack at the moment.
263         */
264        boolean setPinnedStackSize(Rect bounds, Rect taskBounds);
265
266        void onAnimationStart();
267
268        /**
269         * Callback for the target to inform it that the animation has ended, so it can do some
270         * necessary cleanup.
271         */
272        void onAnimationEnd();
273
274        void moveToFullscreen();
275
276        void getFullScreenBounds(Rect bounds);
277    }
278
279    void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration) {
280        boolean moveToFullscreen = false;
281        if (to == null) {
282            to = new Rect();
283            target.getFullScreenBounds(to);
284            moveToFullscreen = true;
285        }
286
287        final BoundsAnimator existing = mRunningAnimations.get(target);
288        final boolean replacing = existing != null;
289
290        if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
291                + " moveToFullscreen=" + moveToFullscreen + " replacing=" + replacing);
292
293        if (replacing) {
294            if (existing.isAnimatingTo(to)) {
295                // Just les the current animation complete if it has the same destination as the
296                // one we are trying to start.
297                if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing
298                        + " ignoring...");
299                return;
300            }
301            existing.cancel();
302        }
303        final BoundsAnimator animator =
304                new BoundsAnimator(target, from, to, moveToFullscreen, replacing);
305        mRunningAnimations.put(target, animator);
306        animator.setFloatValues(0f, 1f);
307        animator.setDuration((animationDuration != -1 ? animationDuration
308                : DEFAULT_APP_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
309        animator.setInterpolator(new LinearInterpolator());
310        animator.start();
311    }
312}
313