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.systemui.pip.phone; 18 19 import static android.app.ActivityManager.StackId.PINNED_STACK_ID; 20 21 import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN; 22 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; 23 import static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN; 24 25 import android.animation.AnimationHandler; 26 import android.animation.Animator; 27 import android.animation.Animator.AnimatorListener; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.RectEvaluator; 30 import android.animation.ValueAnimator; 31 import android.animation.ValueAnimator.AnimatorUpdateListener; 32 import android.app.ActivityManager.StackInfo; 33 import android.app.IActivityManager; 34 import android.content.Context; 35 import android.graphics.Point; 36 import android.graphics.PointF; 37 import android.graphics.Rect; 38 import android.os.Debug; 39 import android.os.Handler; 40 import android.os.Message; 41 import android.os.RemoteException; 42 import android.util.Log; 43 import android.view.animation.Interpolator; 44 45 import com.android.internal.graphics.SfVsyncFrameCallbackProvider; 46 import com.android.internal.os.SomeArgs; 47 import com.android.internal.policy.PipSnapAlgorithm; 48 import com.android.systemui.recents.misc.ForegroundThread; 49 import com.android.systemui.recents.misc.SystemServicesProxy; 50 import com.android.systemui.statusbar.FlingAnimationUtils; 51 52 import java.io.PrintWriter; 53 54 /** 55 * A helper to animate and manipulate the PiP. 56 */ 57 public class PipMotionHelper implements Handler.Callback { 58 59 private static final String TAG = "PipMotionHelper"; 60 private static final boolean DEBUG = false; 61 62 private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect()); 63 64 private static final int DEFAULT_MOVE_STACK_DURATION = 225; 65 private static final int SNAP_STACK_DURATION = 225; 66 private static final int DRAG_TO_TARGET_DISMISS_STACK_DURATION = 375; 67 private static final int DRAG_TO_DISMISS_STACK_DURATION = 175; 68 private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; 69 private static final int EXPAND_STACK_TO_MENU_DURATION = 250; 70 private static final int EXPAND_STACK_TO_FULLSCREEN_DURATION = 300; 71 private static final int MINIMIZE_STACK_MAX_DURATION = 200; 72 private static final int IME_SHIFT_DURATION = 300; 73 74 // The fraction of the stack width that the user has to drag offscreen to minimize the PiP 75 private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.3f; 76 // The fraction of the stack height that the user has to drag offscreen to dismiss the PiP 77 private static final float DISMISS_OFFSCREEN_FRACTION = 0.3f; 78 79 private static final int MSG_RESIZE_IMMEDIATE = 1; 80 private static final int MSG_RESIZE_ANIMATE = 2; 81 82 private Context mContext; 83 private IActivityManager mActivityManager; 84 private Handler mHandler; 85 86 private PipMenuActivityController mMenuController; 87 private PipSnapAlgorithm mSnapAlgorithm; 88 private FlingAnimationUtils mFlingAnimationUtils; 89 private AnimationHandler mAnimationHandler; 90 91 private final Rect mBounds = new Rect(); 92 private final Rect mStableInsets = new Rect(); 93 94 private ValueAnimator mBoundsAnimator = null; 95 96 public PipMotionHelper(Context context, IActivityManager activityManager, 97 PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm, 98 FlingAnimationUtils flingAnimationUtils) { 99 mContext = context; 100 mHandler = new Handler(ForegroundThread.get().getLooper(), this); 101 mActivityManager = activityManager; 102 mMenuController = menuController; 103 mSnapAlgorithm = snapAlgorithm; 104 mFlingAnimationUtils = flingAnimationUtils; 105 mAnimationHandler = new AnimationHandler(); 106 mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider()); 107 onConfigurationChanged(); 108 } 109 110 /** 111 * Updates whenever the configuration changes. 112 */ 113 void onConfigurationChanged() { 114 mSnapAlgorithm.onConfigurationChanged(); 115 SystemServicesProxy.getInstance(mContext).getStableInsets(mStableInsets); 116 } 117 118 /** 119 * Synchronizes the current bounds with the pinned stack. 120 */ 121 void synchronizePinnedStackBounds() { 122 cancelAnimations(); 123 try { 124 StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); 125 if (stackInfo != null) { 126 mBounds.set(stackInfo.bounds); 127 } 128 } catch (RemoteException e) { 129 Log.w(TAG, "Failed to get pinned stack bounds"); 130 } 131 } 132 133 /** 134 * Tries to the move the pinned stack to the given {@param bounds}. 135 */ 136 void movePip(Rect toBounds) { 137 cancelAnimations(); 138 resizePipUnchecked(toBounds); 139 mBounds.set(toBounds); 140 } 141 142 /** 143 * Resizes the pinned stack back to fullscreen. 144 */ 145 void expandPip() { 146 expandPip(false /* skipAnimation */); 147 } 148 149 /** 150 * Resizes the pinned stack back to fullscreen. 151 */ 152 void expandPip(boolean skipAnimation) { 153 if (DEBUG) { 154 Log.d(TAG, "expandPip: skipAnimation=" + skipAnimation 155 + " callers=\n" + Debug.getCallers(5, " ")); 156 } 157 cancelAnimations(); 158 mMenuController.hideMenuWithoutResize(); 159 mHandler.post(() -> { 160 try { 161 if (skipAnimation) { 162 mActivityManager.moveTasksToFullscreenStack(PINNED_STACK_ID, true /* onTop */); 163 } else { 164 mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */, 165 true /* allowResizeInDockedMode */, true /* preserveWindows */, 166 true /* animate */, EXPAND_STACK_TO_FULLSCREEN_DURATION); 167 } 168 } catch (RemoteException e) { 169 Log.e(TAG, "Error expanding PiP activity", e); 170 } 171 }); 172 } 173 174 /** 175 * Dismisses the pinned stack. 176 */ 177 void dismissPip() { 178 if (DEBUG) { 179 Log.d(TAG, "dismissPip: callers=\n" + Debug.getCallers(5, " ")); 180 } 181 cancelAnimations(); 182 mMenuController.hideMenuWithoutResize(); 183 mHandler.post(() -> { 184 try { 185 mActivityManager.removeStack(PINNED_STACK_ID); 186 } catch (RemoteException e) { 187 Log.e(TAG, "Failed to remove PiP", e); 188 } 189 }); 190 } 191 192 /** 193 * @return the PiP bounds. 194 */ 195 Rect getBounds() { 196 return mBounds; 197 } 198 199 /** 200 * @return the closest minimized PiP bounds. 201 */ 202 Rect getClosestMinimizedBounds(Rect stackBounds, Rect movementBounds) { 203 Point displaySize = new Point(); 204 mContext.getDisplay().getRealSize(displaySize); 205 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, stackBounds); 206 mSnapAlgorithm.applyMinimizedOffset(toBounds, movementBounds, displaySize, mStableInsets); 207 return toBounds; 208 } 209 210 /** 211 * @return whether the PiP at the current bounds should be minimized. 212 */ 213 boolean shouldMinimizePip() { 214 Point displaySize = new Point(); 215 mContext.getDisplay().getRealSize(displaySize); 216 if (mBounds.left < 0) { 217 float offscreenFraction = (float) -mBounds.left / mBounds.width(); 218 return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION; 219 } else if (mBounds.right > displaySize.x) { 220 float offscreenFraction = (float) (mBounds.right - displaySize.x) / 221 mBounds.width(); 222 return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION; 223 } else { 224 return false; 225 } 226 } 227 228 /** 229 * @return whether the PiP at the current bounds should be dismissed. 230 */ 231 boolean shouldDismissPip() { 232 Point displaySize = new Point(); 233 mContext.getDisplay().getSize(displaySize); 234 if (mBounds.bottom > displaySize.y) { 235 float offscreenFraction = (float) (mBounds.bottom - displaySize.y) / mBounds.height(); 236 return offscreenFraction >= DISMISS_OFFSCREEN_FRACTION; 237 } 238 return false; 239 } 240 241 /** 242 * Flings the minimized PiP to the closest minimized snap target. 243 */ 244 Rect flingToMinimizedState(float velocityY, Rect movementBounds, Point dragStartPosition) { 245 cancelAnimations(); 246 // We currently only allow flinging the minimized stack up and down, so just lock the 247 // movement bounds to the current stack bounds horizontally 248 movementBounds = new Rect(mBounds.left, movementBounds.top, mBounds.left, 249 movementBounds.bottom); 250 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds, 251 0 /* velocityX */, velocityY, dragStartPosition); 252 if (!mBounds.equals(toBounds)) { 253 mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN); 254 mFlingAnimationUtils.apply(mBoundsAnimator, 0, 255 distanceBetweenRectOffsets(mBounds, toBounds), 256 velocityY); 257 mBoundsAnimator.start(); 258 } 259 return toBounds; 260 } 261 262 /** 263 * Animates the PiP to the minimized state, slightly offscreen. 264 */ 265 Rect animateToClosestMinimizedState(Rect movementBounds, 266 AnimatorUpdateListener updateListener) { 267 cancelAnimations(); 268 Rect toBounds = getClosestMinimizedBounds(mBounds, movementBounds); 269 if (!mBounds.equals(toBounds)) { 270 mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 271 MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN); 272 if (updateListener != null) { 273 mBoundsAnimator.addUpdateListener(updateListener); 274 } 275 mBoundsAnimator.start(); 276 } 277 return toBounds; 278 } 279 280 /** 281 * Flings the PiP to the closest snap target. 282 */ 283 Rect flingToSnapTarget(float velocity, float velocityX, float velocityY, Rect movementBounds, 284 AnimatorUpdateListener updateListener, AnimatorListener listener, 285 Point startPosition) { 286 cancelAnimations(); 287 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds, 288 velocityX, velocityY, startPosition); 289 if (!mBounds.equals(toBounds)) { 290 mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN); 291 mFlingAnimationUtils.apply(mBoundsAnimator, 0, 292 distanceBetweenRectOffsets(mBounds, toBounds), 293 velocity); 294 if (updateListener != null) { 295 mBoundsAnimator.addUpdateListener(updateListener); 296 } 297 if (listener != null){ 298 mBoundsAnimator.addListener(listener); 299 } 300 mBoundsAnimator.start(); 301 } 302 return toBounds; 303 } 304 305 /** 306 * Animates the PiP to the closest snap target. 307 */ 308 Rect animateToClosestSnapTarget(Rect movementBounds, AnimatorUpdateListener updateListener, 309 AnimatorListener listener) { 310 cancelAnimations(); 311 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds); 312 if (!mBounds.equals(toBounds)) { 313 mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, SNAP_STACK_DURATION, 314 FAST_OUT_SLOW_IN); 315 if (updateListener != null) { 316 mBoundsAnimator.addUpdateListener(updateListener); 317 } 318 if (listener != null){ 319 mBoundsAnimator.addListener(listener); 320 } 321 mBoundsAnimator.start(); 322 } 323 return toBounds; 324 } 325 326 /** 327 * Animates the PiP to the expanded state to show the menu. 328 */ 329 float animateToExpandedState(Rect expandedBounds, Rect movementBounds, 330 Rect expandedMovementBounds) { 331 float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds); 332 mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); 333 resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); 334 return savedSnapFraction; 335 } 336 337 /** 338 * Animates the PiP from the expanded state to the normal state after the menu is hidden. 339 */ 340 void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, 341 Rect normalMovementBounds, Rect currentMovementBounds, boolean minimized, 342 boolean immediate) { 343 if (savedSnapFraction < 0f) { 344 // If there are no saved snap fractions, then just use the current bounds 345 savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), 346 currentMovementBounds); 347 } 348 mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction); 349 if (minimized) { 350 normalBounds = getClosestMinimizedBounds(normalBounds, normalMovementBounds); 351 } 352 if (immediate) { 353 movePip(normalBounds); 354 } else { 355 resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); 356 } 357 } 358 359 /** 360 * Animates the PiP to offset it from the IME. 361 */ 362 void animateToIMEOffset(Rect toBounds) { 363 cancelAnimations(); 364 resizeAndAnimatePipUnchecked(toBounds, IME_SHIFT_DURATION); 365 } 366 367 /** 368 * Animates the dismissal of the PiP off the edge of the screen. 369 */ 370 Rect animateDismiss(Rect pipBounds, float velocityX, float velocityY, 371 AnimatorUpdateListener listener) { 372 cancelAnimations(); 373 final float velocity = PointF.length(velocityX, velocityY); 374 final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond(); 375 Point p = getDismissEndPoint(pipBounds, velocityX, velocityY, isFling); 376 Rect toBounds = new Rect(pipBounds); 377 toBounds.offsetTo(p.x, p.y); 378 mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, DRAG_TO_DISMISS_STACK_DURATION, 379 FAST_OUT_LINEAR_IN); 380 mBoundsAnimator.addListener(new AnimatorListenerAdapter() { 381 @Override 382 public void onAnimationEnd(Animator animation) { 383 dismissPip(); 384 } 385 }); 386 if (isFling) { 387 mFlingAnimationUtils.apply(mBoundsAnimator, 0, 388 distanceBetweenRectOffsets(mBounds, toBounds), velocity); 389 } 390 if (listener != null) { 391 mBoundsAnimator.addUpdateListener(listener); 392 } 393 mBoundsAnimator.start(); 394 return toBounds; 395 } 396 397 /** 398 * Cancels all existing animations. 399 */ 400 void cancelAnimations() { 401 if (mBoundsAnimator != null) { 402 mBoundsAnimator.cancel(); 403 mBoundsAnimator = null; 404 } 405 } 406 407 /** 408 * Creates an animation to move the PiP to give given {@param toBounds}. 409 */ 410 private ValueAnimator createAnimationToBounds(Rect fromBounds, Rect toBounds, int duration, 411 Interpolator interpolator) { 412 ValueAnimator anim = new ValueAnimator() { 413 @Override 414 public AnimationHandler getAnimationHandler() { 415 return mAnimationHandler; 416 } 417 }; 418 anim.setObjectValues(fromBounds, toBounds); 419 anim.setEvaluator(RECT_EVALUATOR); 420 anim.setDuration(duration); 421 anim.setInterpolator(interpolator); 422 anim.addUpdateListener((ValueAnimator animation) -> { 423 resizePipUnchecked((Rect) animation.getAnimatedValue()); 424 }); 425 return anim; 426 } 427 428 /** 429 * Directly resizes the PiP to the given {@param bounds}. 430 */ 431 private void resizePipUnchecked(Rect toBounds) { 432 if (DEBUG) { 433 Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds 434 + " callers=\n" + Debug.getCallers(5, " ")); 435 } 436 if (!toBounds.equals(mBounds)) { 437 SomeArgs args = SomeArgs.obtain(); 438 args.arg1 = toBounds; 439 mHandler.sendMessage(mHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args)); 440 } 441 } 442 443 /** 444 * Directly resizes the PiP to the given {@param bounds}. 445 */ 446 private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { 447 if (DEBUG) { 448 Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds 449 + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " ")); 450 } 451 if (!toBounds.equals(mBounds)) { 452 SomeArgs args = SomeArgs.obtain(); 453 args.arg1 = toBounds; 454 args.argi1 = duration; 455 mHandler.sendMessage(mHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); 456 } 457 } 458 459 /** 460 * @return the coordinates the PIP should animate to based on the direction of velocity when 461 * dismissing. 462 */ 463 private Point getDismissEndPoint(Rect pipBounds, float velX, float velY, boolean isFling) { 464 Point displaySize = new Point(); 465 mContext.getDisplay().getRealSize(displaySize); 466 final float bottomBound = displaySize.y + pipBounds.height() * .1f; 467 if (isFling && velX != 0 && velY != 0) { 468 // Line is defined by: y = mx + b, m = slope, b = y-intercept 469 // Find the slope 470 final float slope = velY / velX; 471 // Sub in slope and PiP position to solve for y-intercept: b = y - mx 472 final float yIntercept = pipBounds.top - slope * pipBounds.left; 473 // Now find the point on this line when y = bottom bound: x = (y - b) / m 474 final float x = (bottomBound - yIntercept) / slope; 475 return new Point((int) x, (int) bottomBound); 476 } else { 477 // If it wasn't a fling the velocity on 'up' is not reliable for direction of movement, 478 // just animate downwards. 479 return new Point(pipBounds.left, (int) bottomBound); 480 } 481 } 482 483 /** 484 * @return whether the gesture it towards the dismiss area based on the velocity when 485 * dismissing. 486 */ 487 public boolean isGestureToDismissArea(Rect pipBounds, float velX, float velY, 488 boolean isFling) { 489 Point endpoint = getDismissEndPoint(pipBounds, velX, velY, isFling); 490 // Center the point 491 endpoint.x += pipBounds.width() / 2; 492 endpoint.y += pipBounds.height() / 2; 493 494 // The dismiss area is the middle third of the screen, half the PIP's height from the bottom 495 Point size = new Point(); 496 mContext.getDisplay().getRealSize(size); 497 final int left = size.x / 3; 498 Rect dismissArea = new Rect(left, size.y - (pipBounds.height() / 2), left * 2, 499 size.y + pipBounds.height()); 500 return dismissArea.contains(endpoint.x, endpoint.y); 501 } 502 503 /** 504 * @return the distance between points {@param p1} and {@param p2}. 505 */ 506 private float distanceBetweenRectOffsets(Rect r1, Rect r2) { 507 return PointF.length(r1.left - r2.left, r1.top - r2.top); 508 } 509 510 /** 511 * Handles messages to be processed on the background thread. 512 */ 513 public boolean handleMessage(Message msg) { 514 switch (msg.what) { 515 case MSG_RESIZE_IMMEDIATE: { 516 SomeArgs args = (SomeArgs) msg.obj; 517 Rect toBounds = (Rect) args.arg1; 518 try { 519 mActivityManager.resizePinnedStack(toBounds, null /* tempPinnedTaskBounds */); 520 mBounds.set(toBounds); 521 } catch (RemoteException e) { 522 Log.e(TAG, "Could not resize pinned stack to bounds: " + toBounds, e); 523 } 524 return true; 525 } 526 527 case MSG_RESIZE_ANIMATE: { 528 SomeArgs args = (SomeArgs) msg.obj; 529 Rect toBounds = (Rect) args.arg1; 530 int duration = args.argi1; 531 try { 532 StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); 533 if (stackInfo == null) { 534 // In the case where we've already re-expanded or dismissed the PiP, then 535 // just skip the resize 536 return true; 537 } 538 539 mActivityManager.resizeStack(PINNED_STACK_ID, toBounds, 540 false /* allowResizeInDockedMode */, true /* preserveWindows */, 541 true /* animate */, duration); 542 mBounds.set(toBounds); 543 } catch (RemoteException e) { 544 Log.e(TAG, "Could not animate resize pinned stack to bounds: " + toBounds, e); 545 } 546 return true; 547 } 548 549 default: 550 return false; 551 } 552 } 553 554 public void dump(PrintWriter pw, String prefix) { 555 final String innerPrefix = prefix + " "; 556 pw.println(prefix + TAG); 557 pw.println(innerPrefix + "mBounds=" + mBounds); 558 pw.println(innerPrefix + "mStableInsets=" + mStableInsets); 559 } 560 } 561