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