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