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