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 com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; 20 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; 21 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ValueAnimator; 26 import android.animation.ValueAnimator.AnimatorUpdateListener; 27 import android.app.IActivityManager; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.graphics.Point; 32 import android.graphics.PointF; 33 import android.graphics.Rect; 34 import android.os.Handler; 35 import android.os.RemoteException; 36 import android.util.Log; 37 import android.util.Size; 38 import android.view.IPinnedStackController; 39 import android.view.MotionEvent; 40 import android.view.ViewConfiguration; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.view.accessibility.AccessibilityManager; 43 import android.view.accessibility.AccessibilityNodeInfo; 44 import android.view.accessibility.AccessibilityWindowInfo; 45 46 import com.android.internal.logging.MetricsLogger; 47 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 48 import com.android.internal.policy.PipSnapAlgorithm; 49 import com.android.systemui.R; 50 import com.android.systemui.statusbar.FlingAnimationUtils; 51 52 import java.io.PrintWriter; 53 54 /** 55 * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding 56 * the PIP. 57 */ 58 public class PipTouchHandler { 59 private static final String TAG = "PipTouchHandler"; 60 61 // Allow the PIP to be dragged to the edge of the screen to be minimized. 62 private static final boolean ENABLE_MINIMIZE = false; 63 // Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed. 64 private static final boolean ENABLE_FLING_DISMISS = false; 65 66 // These values are used for metrics and should never change 67 private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0; 68 private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1; 69 70 private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225; 71 72 // Allow dragging the PIP to a location to close it 73 private static final boolean ENABLE_DISMISS_DRAG_TO_EDGE = true; 74 75 private final Context mContext; 76 private final IActivityManager mActivityManager; 77 private final ViewConfiguration mViewConfig; 78 private final PipMenuListener mMenuListener = new PipMenuListener(); 79 private IPinnedStackController mPinnedStackController; 80 81 private final PipMenuActivityController mMenuController; 82 private final PipDismissViewController mDismissViewController; 83 private final PipSnapAlgorithm mSnapAlgorithm; 84 private final AccessibilityManager mAccessibilityManager; 85 private boolean mShowPipMenuOnAnimationEnd = false; 86 87 // The current movement bounds 88 private Rect mMovementBounds = new Rect(); 89 90 // The reference inset bounds, used to determine the dismiss fraction 91 private Rect mInsetBounds = new Rect(); 92 // The reference bounds used to calculate the normal/expanded target bounds 93 private Rect mNormalBounds = new Rect(); 94 private Rect mNormalMovementBounds = new Rect(); 95 private Rect mExpandedBounds = new Rect(); 96 private Rect mExpandedMovementBounds = new Rect(); 97 private int mExpandedShortestEdgeSize; 98 99 // Used to workaround an issue where the WM rotation happens before we are notified, allowing 100 // us to send stale bounds 101 private int mDeferResizeToNormalBoundsUntilRotation = -1; 102 private int mDisplayRotation; 103 104 private Handler mHandler = new Handler(); 105 private Runnable mShowDismissAffordance = new Runnable() { 106 @Override 107 public void run() { 108 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 109 mDismissViewController.showDismissTarget(); 110 } 111 } 112 }; 113 private ValueAnimator.AnimatorUpdateListener mUpdateScrimListener = 114 new AnimatorUpdateListener() { 115 @Override 116 public void onAnimationUpdate(ValueAnimator animation) { 117 updateDismissFraction(); 118 } 119 }; 120 121 // Behaviour states 122 private int mMenuState = MENU_STATE_NONE; 123 private boolean mIsMinimized; 124 private boolean mIsImeShowing; 125 private int mImeHeight; 126 private int mImeOffset; 127 private float mSavedSnapFraction = -1f; 128 private boolean mSendingHoverAccessibilityEvents; 129 private boolean mMovementWithinMinimize; 130 private boolean mMovementWithinDismiss; 131 132 // Touch state 133 private final PipTouchState mTouchState; 134 private final FlingAnimationUtils mFlingAnimationUtils; 135 private final PipTouchGesture[] mGestures; 136 private final PipMotionHelper mMotionHelper; 137 138 // Temp vars 139 private final Rect mTmpBounds = new Rect(); 140 141 /** 142 * A listener for the PIP menu activity. 143 */ 144 private class PipMenuListener implements PipMenuActivityController.Listener { 145 @Override 146 public void onPipMenuStateChanged(int menuState, boolean resize) { 147 setMenuState(menuState, resize); 148 } 149 150 @Override 151 public void onPipExpand() { 152 if (!mIsMinimized) { 153 mMotionHelper.expandPip(); 154 } 155 } 156 157 @Override 158 public void onPipMinimize() { 159 setMinimizedStateInternal(true); 160 mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateListener */); 161 } 162 163 @Override 164 public void onPipDismiss() { 165 mMotionHelper.dismissPip(); 166 MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED, 167 METRIC_VALUE_DISMISSED_BY_TAP); 168 } 169 170 @Override 171 public void onPipShowMenu() { 172 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 173 mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()); 174 } 175 } 176 177 public PipTouchHandler(Context context, IActivityManager activityManager, 178 PipMenuActivityController menuController, 179 InputConsumerController inputConsumerController) { 180 181 // Initialize the Pip input consumer 182 mContext = context; 183 mActivityManager = activityManager; 184 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 185 mViewConfig = ViewConfiguration.get(context); 186 mMenuController = menuController; 187 mMenuController.addListener(mMenuListener); 188 mDismissViewController = new PipDismissViewController(context); 189 mSnapAlgorithm = new PipSnapAlgorithm(mContext); 190 mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f); 191 mGestures = new PipTouchGesture[] { 192 mDefaultMovementGesture 193 }; 194 mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController, 195 mSnapAlgorithm, mFlingAnimationUtils); 196 mTouchState = new PipTouchState(mViewConfig, mHandler, 197 () -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 198 mMovementBounds, true /* allowMenuTimeout */, willResizeMenu())); 199 200 Resources res = context.getResources(); 201 mExpandedShortestEdgeSize = res.getDimensionPixelSize( 202 R.dimen.pip_expanded_shortest_edge_size); 203 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); 204 205 // Register the listener for input consumer touch events 206 inputConsumerController.setTouchListener(this::handleTouchEvent); 207 inputConsumerController.setRegistrationListener(this::onRegistrationChanged); 208 onRegistrationChanged(inputConsumerController.isRegistered()); 209 } 210 211 public void setTouchEnabled(boolean enabled) { 212 mTouchState.setAllowTouches(enabled); 213 } 214 215 public void showPictureInPictureMenu() { 216 // Only show the menu if the user isn't currently interacting with the PiP 217 if (!mTouchState.isUserInteracting()) { 218 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 219 mMovementBounds, false /* allowMenuTimeout */, willResizeMenu()); 220 } 221 } 222 223 public void onActivityPinned() { 224 cleanUp(); 225 mShowPipMenuOnAnimationEnd = true; 226 } 227 228 public void onActivityUnpinned(ComponentName topPipActivity) { 229 if (topPipActivity == null) { 230 // Clean up state after the last PiP activity is removed 231 cleanUp(); 232 } 233 } 234 235 public void onPinnedStackAnimationEnded() { 236 // Always synchronize the motion helper bounds once PiP animations finish 237 mMotionHelper.synchronizePinnedStackBounds(); 238 239 if (mShowPipMenuOnAnimationEnd) { 240 mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), 241 mMovementBounds, true /* allowMenuTimeout */, false /* willResizeMenu */); 242 mShowPipMenuOnAnimationEnd = false; 243 } 244 } 245 246 public void onConfigurationChanged() { 247 mMotionHelper.onConfigurationChanged(); 248 mMotionHelper.synchronizePinnedStackBounds(); 249 } 250 251 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 252 mIsImeShowing = imeVisible; 253 mImeHeight = imeHeight; 254 } 255 256 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, 257 boolean fromImeAdjustement, int displayRotation) { 258 // Re-calculate the expanded bounds 259 mNormalBounds = normalBounds; 260 Rect normalMovementBounds = new Rect(); 261 mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds, 262 mIsImeShowing ? mImeHeight : 0); 263 264 // Calculate the expanded size 265 float aspectRatio = (float) normalBounds.width() / normalBounds.height(); 266 Point displaySize = new Point(); 267 mContext.getDisplay().getRealSize(displaySize); 268 Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, 269 mExpandedShortestEdgeSize, displaySize.x, displaySize.y); 270 mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight()); 271 Rect expandedMovementBounds = new Rect(); 272 mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds, 273 mIsImeShowing ? mImeHeight : 0); 274 275 // If this is from an IME adjustment, then we should move the PiP so that it is not occluded 276 // by the IME 277 if (fromImeAdjustement) { 278 if (mTouchState.isUserInteracting()) { 279 // Defer the update of the current movement bounds until after the user finishes 280 // touching the screen 281 } else { 282 final Rect bounds = new Rect(animatingBounds); 283 final Rect toMovementBounds = mMenuState == MENU_STATE_FULL 284 ? expandedMovementBounds 285 : normalMovementBounds; 286 if (mIsImeShowing) { 287 // IME visible, apply the IME offset if the space allows for it 288 final int imeOffset = toMovementBounds.bottom - Math.max(toMovementBounds.top, 289 toMovementBounds.bottom - mImeOffset); 290 if (bounds.top == mMovementBounds.bottom) { 291 // If the PIP is currently resting on top of the IME, then adjust it with 292 // the showing IME 293 bounds.offsetTo(bounds.left, toMovementBounds.bottom - imeOffset); 294 } else { 295 bounds.offset(0, Math.min(0, toMovementBounds.bottom - imeOffset 296 - bounds.top)); 297 } 298 } else { 299 // IME hidden 300 if (bounds.top >= (mMovementBounds.bottom - mImeOffset)) { 301 // If the PIP is resting on top of the IME, then adjust it with the hiding 302 // IME 303 bounds.offsetTo(bounds.left, toMovementBounds.bottom); 304 } 305 } 306 mMotionHelper.animateToIMEOffset(bounds); 307 } 308 } 309 310 // Update the movement bounds after doing the calculations based on the old movement bounds 311 // above 312 mNormalMovementBounds = normalMovementBounds; 313 mExpandedMovementBounds = expandedMovementBounds; 314 mDisplayRotation = displayRotation; 315 mInsetBounds.set(insetBounds); 316 updateMovementBounds(mMenuState); 317 318 // If we have a deferred resize, apply it now 319 if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { 320 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 321 mNormalMovementBounds, mMovementBounds, mIsMinimized, 322 true /* immediate */); 323 mSavedSnapFraction = -1f; 324 mDeferResizeToNormalBoundsUntilRotation = -1; 325 } 326 } 327 328 private void onRegistrationChanged(boolean isRegistered) { 329 mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered 330 ? new PipAccessibilityInteractionConnection(mMotionHelper, 331 this::onAccessibilityShowMenu, mHandler) : null); 332 333 if (!isRegistered && mTouchState.isUserInteracting()) { 334 // If the input consumer is unregistered while the user is interacting, then we may not 335 // get the final TOUCH_UP event, so clean up the dismiss target as well 336 cleanUpDismissTarget(); 337 } 338 } 339 340 private void onAccessibilityShowMenu() { 341 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 342 mMovementBounds, false /* allowMenuTimeout */, willResizeMenu()); 343 } 344 345 private boolean handleTouchEvent(MotionEvent ev) { 346 // Skip touch handling until we are bound to the controller 347 if (mPinnedStackController == null) { 348 return true; 349 } 350 351 // Update the touch state 352 mTouchState.onTouchEvent(ev); 353 354 switch (ev.getAction()) { 355 case MotionEvent.ACTION_DOWN: { 356 mMotionHelper.synchronizePinnedStackBounds(); 357 358 for (PipTouchGesture gesture : mGestures) { 359 gesture.onDown(mTouchState); 360 } 361 break; 362 } 363 case MotionEvent.ACTION_MOVE: { 364 for (PipTouchGesture gesture : mGestures) { 365 if (gesture.onMove(mTouchState)) { 366 break; 367 } 368 } 369 break; 370 } 371 case MotionEvent.ACTION_UP: { 372 // Update the movement bounds again if the state has changed since the user started 373 // dragging (ie. when the IME shows) 374 updateMovementBounds(mMenuState); 375 376 for (PipTouchGesture gesture : mGestures) { 377 if (gesture.onUp(mTouchState)) { 378 break; 379 } 380 } 381 382 // Fall through to clean up 383 } 384 case MotionEvent.ACTION_CANCEL: { 385 mTouchState.reset(); 386 break; 387 } 388 case MotionEvent.ACTION_HOVER_ENTER: 389 case MotionEvent.ACTION_HOVER_MOVE: { 390 if (mAccessibilityManager.isEnabled() && !mSendingHoverAccessibilityEvents) { 391 AccessibilityEvent event = AccessibilityEvent.obtain( 392 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 393 event.setImportantForAccessibility(true); 394 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 395 event.setWindowId( 396 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 397 mAccessibilityManager.sendAccessibilityEvent(event); 398 mSendingHoverAccessibilityEvents = true; 399 } 400 break; 401 } 402 case MotionEvent.ACTION_HOVER_EXIT: { 403 if (mAccessibilityManager.isEnabled() && mSendingHoverAccessibilityEvents) { 404 AccessibilityEvent event = AccessibilityEvent.obtain( 405 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 406 event.setImportantForAccessibility(true); 407 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 408 event.setWindowId( 409 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 410 mAccessibilityManager.sendAccessibilityEvent(event); 411 mSendingHoverAccessibilityEvents = false; 412 } 413 break; 414 } 415 } 416 return mMenuState == MENU_STATE_NONE; 417 } 418 419 /** 420 * Updates the appearance of the menu and scrim on top of the PiP while dismissing. 421 */ 422 private void updateDismissFraction() { 423 // Skip updating the dismiss fraction when the IME is showing. This is to work around an 424 // issue where starting the menu activity for the dismiss overlay will steal the window 425 // focus, which closes the IME. 426 if (mMenuController != null && !mIsImeShowing) { 427 Rect bounds = mMotionHelper.getBounds(); 428 final float target = mInsetBounds.bottom; 429 float fraction = 0f; 430 if (bounds.bottom > target) { 431 final float distance = bounds.bottom - target; 432 fraction = Math.min(distance / bounds.height(), 1f); 433 } 434 if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) { 435 // Update if the fraction > 0, or if fraction == 0 and the menu was already visible 436 mMenuController.setDismissFraction(fraction); 437 } 438 } 439 } 440 441 /** 442 * Sets the controller to update the system of changes from user interaction. 443 */ 444 void setPinnedStackController(IPinnedStackController controller) { 445 mPinnedStackController = controller; 446 } 447 448 /** 449 * Sets the minimized state. 450 */ 451 private void setMinimizedStateInternal(boolean isMinimized) { 452 if (!ENABLE_MINIMIZE) { 453 return; 454 } 455 setMinimizedState(isMinimized, false /* fromController */); 456 } 457 458 /** 459 * Sets the minimized state. 460 */ 461 void setMinimizedState(boolean isMinimized, boolean fromController) { 462 if (!ENABLE_MINIMIZE) { 463 return; 464 } 465 if (mIsMinimized != isMinimized) { 466 MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED, 467 isMinimized); 468 } 469 mIsMinimized = isMinimized; 470 mSnapAlgorithm.setMinimized(isMinimized); 471 472 if (fromController) { 473 if (isMinimized) { 474 // Move the PiP to the new bounds immediately if minimized 475 mMotionHelper.movePip(mMotionHelper.getClosestMinimizedBounds(mNormalBounds, 476 mMovementBounds)); 477 } 478 } else if (mPinnedStackController != null) { 479 try { 480 mPinnedStackController.setIsMinimized(isMinimized); 481 } catch (RemoteException e) { 482 Log.e(TAG, "Could not set minimized state", e); 483 } 484 } 485 } 486 487 /** 488 * Sets the menu visibility. 489 */ 490 private void setMenuState(int menuState, boolean resize) { 491 if (menuState == MENU_STATE_FULL) { 492 // Save the current snap fraction and if we do not drag or move the PiP, then 493 // we store back to this snap fraction. Otherwise, we'll reset the snap 494 // fraction and snap to the closest edge 495 Rect expandedBounds = new Rect(mExpandedBounds); 496 if (resize) { 497 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, 498 mMovementBounds, mExpandedMovementBounds); 499 } 500 } else if (menuState == MENU_STATE_NONE) { 501 // Try and restore the PiP to the closest edge, using the saved snap fraction 502 // if possible 503 if (resize) { 504 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 505 // This is a very special case: when the menu is expanded and visible, 506 // navigating to another activity can trigger auto-enter PiP, and if the 507 // revealed activity has a forced rotation set, then the controller will get 508 // updated with the new rotation of the display. However, at the same time, 509 // SystemUI will try to hide the menu by creating an animation to the normal 510 // bounds which are now stale. In such a case we defer the animation to the 511 // normal bounds until after the next onMovementBoundsChanged() call to get the 512 // bounds in the new orientation 513 try { 514 int displayRotation = mPinnedStackController.getDisplayRotation(); 515 if (mDisplayRotation != displayRotation) { 516 mDeferResizeToNormalBoundsUntilRotation = displayRotation; 517 } 518 } catch (RemoteException e) { 519 Log.e(TAG, "Could not get display rotation from controller"); 520 } 521 } 522 523 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 524 Rect normalBounds = new Rect(mNormalBounds); 525 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 526 mNormalMovementBounds, mMovementBounds, mIsMinimized, 527 false /* immediate */); 528 mSavedSnapFraction = -1f; 529 } 530 } else { 531 // If resizing is not allowed, then the PiP should be frozen until the transition 532 // ends as well 533 setTouchEnabled(false); 534 mSavedSnapFraction = -1f; 535 } 536 } 537 mMenuState = menuState; 538 updateMovementBounds(menuState); 539 if (menuState != MENU_STATE_CLOSE) { 540 MetricsLogger.visibility(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MENU, 541 menuState == MENU_STATE_FULL); 542 } 543 } 544 545 /** 546 * @return the motion helper. 547 */ 548 public PipMotionHelper getMotionHelper() { 549 return mMotionHelper; 550 } 551 552 /** 553 * Gesture controlling normal movement of the PIP. 554 */ 555 private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() { 556 // Whether the PiP was on the left side of the screen at the start of the gesture 557 private boolean mStartedOnLeft; 558 private final Point mStartPosition = new Point(); 559 private final PointF mDelta = new PointF(); 560 561 @Override 562 public void onDown(PipTouchState touchState) { 563 if (!touchState.isUserInteracting()) { 564 return; 565 } 566 567 Rect bounds = mMotionHelper.getBounds(); 568 mDelta.set(0f, 0f); 569 mStartPosition.set(bounds.left, bounds.top); 570 mStartedOnLeft = bounds.left < mMovementBounds.centerX(); 571 mMovementWithinMinimize = true; 572 mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; 573 574 // If the menu is still visible, and we aren't minimized, then just poke the menu 575 // so that it will timeout after the user stops touching it 576 if (mMenuState != MENU_STATE_NONE && !mIsMinimized) { 577 mMenuController.pokeMenu(); 578 } 579 580 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 581 mDismissViewController.createDismissTarget(); 582 mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY); 583 } 584 } 585 586 @Override 587 boolean onMove(PipTouchState touchState) { 588 if (!touchState.isUserInteracting()) { 589 return false; 590 } 591 592 if (touchState.startedDragging()) { 593 mSavedSnapFraction = -1f; 594 595 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 596 mHandler.removeCallbacks(mShowDismissAffordance); 597 mDismissViewController.showDismissTarget(); 598 } 599 } 600 601 if (touchState.isDragging()) { 602 // Move the pinned stack freely 603 final PointF lastDelta = touchState.getLastTouchDelta(); 604 float lastX = mStartPosition.x + mDelta.x; 605 float lastY = mStartPosition.y + mDelta.y; 606 float left = lastX + lastDelta.x; 607 float top = lastY + lastDelta.y; 608 if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) { 609 left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left)); 610 } 611 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 612 // Allow pip to move past bottom bounds 613 top = Math.max(mMovementBounds.top, top); 614 } else { 615 top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top)); 616 } 617 618 // Add to the cumulative delta after bounding the position 619 mDelta.x += left - lastX; 620 mDelta.y += top - lastY; 621 622 mTmpBounds.set(mMotionHelper.getBounds()); 623 mTmpBounds.offsetTo((int) left, (int) top); 624 mMotionHelper.movePip(mTmpBounds); 625 626 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 627 updateDismissFraction(); 628 } 629 630 final PointF curPos = touchState.getLastTouchPosition(); 631 if (mMovementWithinMinimize) { 632 // Track if movement remains near starting edge to identify swipes to minimize 633 mMovementWithinMinimize = mStartedOnLeft 634 ? curPos.x <= mMovementBounds.left + mTmpBounds.width() 635 : curPos.x >= mMovementBounds.right; 636 } 637 if (mMovementWithinDismiss) { 638 // Track if movement remains near the bottom edge to identify swipe to dismiss 639 mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; 640 } 641 return true; 642 } 643 return false; 644 } 645 646 @Override 647 public boolean onUp(PipTouchState touchState) { 648 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 649 // Clean up the dismiss target regardless of the touch state in case the touch 650 // enabled state changes while the user is interacting 651 cleanUpDismissTarget(); 652 } 653 654 if (!touchState.isUserInteracting()) { 655 return false; 656 } 657 658 final PointF vel = touchState.getVelocity(); 659 final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y); 660 final float velocity = PointF.length(vel.x, vel.y); 661 final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond(); 662 final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS 663 && touchState.getLastTouchPosition().y >= mMovementBounds.bottom 664 && mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x, 665 vel.y, isFling); 666 final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal 667 && (mMovementWithinDismiss || isUpWithinDimiss); 668 if (ENABLE_DISMISS_DRAG_TO_EDGE) { 669 // Check if the user dragged or flung the PiP offscreen to dismiss it 670 if (mMotionHelper.shouldDismissPip() || isFlingToBot) { 671 mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x, 672 vel.y, mUpdateScrimListener); 673 MetricsLogger.action(mContext, 674 MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED, 675 METRIC_VALUE_DISMISSED_BY_DRAG); 676 return true; 677 } 678 } 679 680 if (touchState.isDragging()) { 681 final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize 682 && (mStartedOnLeft ? vel.x < 0 : vel.x > 0); 683 if (ENABLE_MINIMIZE && 684 !mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) { 685 // Pip should be minimized 686 setMinimizedStateInternal(true); 687 if (mMenuState == MENU_STATE_FULL) { 688 // If the user dragged the expanded PiP to the edge, then hiding the menu 689 // will trigger the PiP to be scaled back to the normal size with the 690 // minimize offset adjusted 691 mMenuController.hideMenu(); 692 } else { 693 mMotionHelper.animateToClosestMinimizedState(mMovementBounds, 694 mUpdateScrimListener); 695 } 696 return true; 697 } 698 if (mIsMinimized) { 699 // If we're dragging and it wasn't a minimize gesture then we shouldn't be 700 // minimized. 701 setMinimizedStateInternal(false); 702 } 703 704 AnimatorListenerAdapter postAnimationCallback = null; 705 if (mMenuState != MENU_STATE_NONE) { 706 // If the menu is still visible, and we aren't minimized, then just poke the 707 // menu so that it will timeout after the user stops touching it 708 mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(), 709 mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()); 710 } else { 711 // If the menu is not visible, then we can still be showing the activity for the 712 // dismiss overlay, so just finish it after the animation completes 713 postAnimationCallback = new AnimatorListenerAdapter() { 714 @Override 715 public void onAnimationEnd(Animator animation) { 716 mMenuController.hideMenu(); 717 } 718 }; 719 } 720 721 if (isFling) { 722 mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds, 723 mUpdateScrimListener, postAnimationCallback, 724 mStartPosition); 725 } else { 726 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener, 727 postAnimationCallback); 728 } 729 } else if (mIsMinimized) { 730 // This was a tap, so no longer minimized 731 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */, 732 null /* animatorListener */); 733 setMinimizedStateInternal(false); 734 } else if (mMenuState != MENU_STATE_FULL) { 735 if (mTouchState.isDoubleTap()) { 736 // Expand to fullscreen if this is a double tap 737 mMotionHelper.expandPip(); 738 } else if (!mTouchState.isWaitingForDoubleTap()) { 739 // User has stalled long enough for this not to be a drag or a double tap, just 740 // expand the menu 741 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 742 mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()); 743 } else { 744 // Next touch event _may_ be the second tap for the double-tap, schedule a 745 // fallback runnable to trigger the menu if no touch event occurs before the 746 // next tap 747 mTouchState.scheduleDoubleTapTimeoutCallback(); 748 } 749 } else { 750 mMenuController.hideMenu(); 751 mMotionHelper.expandPip(); 752 } 753 return true; 754 } 755 }; 756 757 /** 758 * Updates the current movement bounds based on whether the menu is currently visible. 759 */ 760 private void updateMovementBounds(int menuState) { 761 boolean isMenuExpanded = menuState == MENU_STATE_FULL; 762 mMovementBounds = isMenuExpanded 763 ? mExpandedMovementBounds 764 : mNormalMovementBounds; 765 try { 766 mPinnedStackController.setMinEdgeSize(isMenuExpanded ? mExpandedShortestEdgeSize : 0); 767 } catch (RemoteException e) { 768 Log.e(TAG, "Could not set minimized state", e); 769 } 770 } 771 772 /** 773 * Removes the dismiss target and cancels any pending callbacks to show it. 774 */ 775 private void cleanUpDismissTarget() { 776 mHandler.removeCallbacks(mShowDismissAffordance); 777 mDismissViewController.destroyDismissTarget(); 778 } 779 780 /** 781 * Resets some states related to the touch handling. 782 */ 783 private void cleanUp() { 784 if (mIsMinimized) { 785 setMinimizedStateInternal(false); 786 } 787 cleanUpDismissTarget(); 788 } 789 790 /** 791 * @return whether the menu will resize as a part of showing the full menu. 792 */ 793 private boolean willResizeMenu() { 794 return mExpandedBounds.width() != mNormalBounds.width() || 795 mExpandedBounds.height() != mNormalBounds.height(); 796 } 797 798 public void dump(PrintWriter pw, String prefix) { 799 final String innerPrefix = prefix + " "; 800 pw.println(prefix + TAG); 801 pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); 802 pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); 803 pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); 804 pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); 805 pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); 806 pw.println(innerPrefix + "mMenuState=" + mMenuState); 807 pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized); 808 pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 809 pw.println(innerPrefix + "mImeHeight=" + mImeHeight); 810 pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); 811 pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + ENABLE_DISMISS_DRAG_TO_EDGE); 812 pw.println(innerPrefix + "mEnableMinimize=" + ENABLE_MINIMIZE); 813 mSnapAlgorithm.dump(pw, innerPrefix); 814 mTouchState.dump(pw, innerPrefix); 815 mMotionHelper.dump(pw, innerPrefix); 816 } 817 818 } 819