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