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 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 = 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 */ 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 */ 146 void expandPip() { 147 expandPip(false /* skipAnimation */); 148 } 149 150 /** 151 * Resizes the pinned stack back to fullscreen. 152 */ 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 */ 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 */ 190 Rect getBounds() { 191 return mBounds; 192 } 193 194 /** 195 * @return the closest minimized PiP bounds. 196 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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