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