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.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; 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 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; 23 24 import android.annotation.SuppressLint; 25 import android.app.IActivityManager; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.PixelFormat; 30 import android.graphics.Point; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.graphics.drawable.TransitionDrawable; 34 import android.os.Handler; 35 import android.os.RemoteException; 36 import android.util.Log; 37 import android.util.Size; 38 import android.view.Gravity; 39 import android.view.IPinnedStackController; 40 import android.view.InputEvent; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.ViewGroup; 45 import android.view.WindowManager; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.view.accessibility.AccessibilityManager; 48 import android.view.accessibility.AccessibilityNodeInfo; 49 import android.view.accessibility.AccessibilityWindowInfo; 50 import android.widget.FrameLayout; 51 52 import androidx.annotation.NonNull; 53 import androidx.dynamicanimation.animation.DynamicAnimation; 54 import androidx.dynamicanimation.animation.SpringForce; 55 56 import com.android.internal.annotations.VisibleForTesting; 57 import com.android.systemui.R; 58 import com.android.systemui.model.SysUiState; 59 import com.android.systemui.pip.PipAnimationController; 60 import com.android.systemui.pip.PipBoundsHandler; 61 import com.android.systemui.pip.PipSnapAlgorithm; 62 import com.android.systemui.pip.PipTaskOrganizer; 63 import com.android.systemui.pip.PipUiEventLogger; 64 import com.android.systemui.shared.system.InputConsumerController; 65 import com.android.systemui.util.DeviceConfigProxy; 66 import com.android.systemui.util.DismissCircleView; 67 import com.android.systemui.util.FloatingContentCoordinator; 68 import com.android.systemui.util.animation.PhysicsAnimator; 69 import com.android.systemui.util.magnetictarget.MagnetizedObject; 70 71 import java.io.PrintWriter; 72 73 import kotlin.Unit; 74 75 /** 76 * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding 77 * the PIP. 78 */ 79 public class PipTouchHandler { 80 private static final String TAG = "PipTouchHandler"; 81 82 /** Duration of the dismiss scrim fading in/out. */ 83 private static final int DISMISS_TRANSITION_DURATION_MS = 200; 84 85 /* The multiplier to apply scale the target size by when applying the magnetic field radius */ 86 private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; 87 88 // Allow dragging the PIP to a location to close it 89 private final boolean mEnableDismissDragToEdge; 90 // Allow PIP to resize to a slightly bigger state upon touch 91 private final boolean mEnableResize; 92 private final Context mContext; 93 private final WindowManager mWindowManager; 94 private final IActivityManager mActivityManager; 95 private final PipBoundsHandler mPipBoundsHandler; 96 private final PipUiEventLogger mPipUiEventLogger; 97 98 private PipResizeGestureHandler mPipResizeGestureHandler; 99 private IPinnedStackController mPinnedStackController; 100 101 private final PipMenuActivityController mMenuController; 102 private final PipSnapAlgorithm mSnapAlgorithm; 103 private final AccessibilityManager mAccessibilityManager; 104 private boolean mShowPipMenuOnAnimationEnd = false; 105 106 /** 107 * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move 108 * PIP. 109 */ 110 private MagnetizedObject<Rect> mMagnetizedPip; 111 112 /** 113 * Container for the dismiss circle, so that it can be animated within the container via 114 * translation rather than within the WindowManager via slow layout animations. 115 */ 116 private ViewGroup mTargetViewContainer; 117 118 /** Circle view used to render the dismiss target. */ 119 private DismissCircleView mTargetView; 120 121 /** 122 * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. 123 */ 124 private MagnetizedObject.MagneticTarget mMagneticTarget; 125 126 /** PhysicsAnimator instance for animating the dismiss target in/out. */ 127 private PhysicsAnimator<View> mMagneticTargetAnimator; 128 129 /** Default configuration to use for springing the dismiss target in/out. */ 130 private final PhysicsAnimator.SpringConfig mTargetSpringConfig = 131 new PhysicsAnimator.SpringConfig( 132 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 133 134 // The current movement bounds 135 private Rect mMovementBounds = new Rect(); 136 137 // The reference inset bounds, used to determine the dismiss fraction 138 private Rect mInsetBounds = new Rect(); 139 // The reference bounds used to calculate the normal/expanded target bounds 140 private Rect mNormalBounds = new Rect(); 141 @VisibleForTesting Rect mNormalMovementBounds = new Rect(); 142 private Rect mExpandedBounds = new Rect(); 143 @VisibleForTesting Rect mExpandedMovementBounds = new Rect(); 144 private int mExpandedShortestEdgeSize; 145 146 // Used to workaround an issue where the WM rotation happens before we are notified, allowing 147 // us to send stale bounds 148 private int mDeferResizeToNormalBoundsUntilRotation = -1; 149 private int mDisplayRotation; 150 151 /** 152 * Runnable that can be posted delayed to show the target. This needs to be saved as a member 153 * variable so we can pass it to removeCallbacks. 154 */ 155 private Runnable mShowTargetAction = this::showDismissTargetMaybe; 156 157 private Handler mHandler = new Handler(); 158 159 // Behaviour states 160 private int mMenuState = MENU_STATE_NONE; 161 private boolean mIsImeShowing; 162 private int mImeHeight; 163 private int mImeOffset; 164 private int mDismissAreaHeight; 165 private boolean mIsShelfShowing; 166 private int mShelfHeight; 167 private int mMovementBoundsExtraOffsets; 168 private int mBottomOffsetBufferPx; 169 private float mSavedSnapFraction = -1f; 170 private boolean mSendingHoverAccessibilityEvents; 171 private boolean mMovementWithinDismiss; 172 private PipAccessibilityInteractionConnection mConnection; 173 174 // Touch state 175 private final PipTouchState mTouchState; 176 private final FloatingContentCoordinator mFloatingContentCoordinator; 177 private PipMotionHelper mMotionHelper; 178 private PipTouchGesture mGesture; 179 180 // Temp vars 181 private final Rect mTmpBounds = new Rect(); 182 183 /** 184 * A listener for the PIP menu activity. 185 */ 186 private class PipMenuListener implements PipMenuActivityController.Listener { 187 @Override onPipMenuStateChanged(int menuState, boolean resize, Runnable callback)188 public void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback) { 189 setMenuState(menuState, resize, callback); 190 } 191 192 @Override onPipExpand()193 public void onPipExpand() { 194 mMotionHelper.expandPipToFullscreen(); 195 } 196 197 @Override onPipDismiss()198 public void onPipDismiss() { 199 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); 200 mTouchState.removeDoubleTapTimeoutCallback(); 201 mMotionHelper.dismissPip(); 202 } 203 204 @Override onPipShowMenu()205 public void onPipShowMenu() { 206 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 207 true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); 208 } 209 } 210 211 @SuppressLint("InflateParams") PipTouchHandler(Context context, IActivityManager activityManager, PipMenuActivityController menuController, InputConsumerController inputConsumerController, PipBoundsHandler pipBoundsHandler, PipTaskOrganizer pipTaskOrganizer, FloatingContentCoordinator floatingContentCoordinator, DeviceConfigProxy deviceConfig, PipSnapAlgorithm pipSnapAlgorithm, SysUiState sysUiState, PipUiEventLogger pipUiEventLogger)212 public PipTouchHandler(Context context, IActivityManager activityManager, 213 PipMenuActivityController menuController, 214 InputConsumerController inputConsumerController, 215 PipBoundsHandler pipBoundsHandler, 216 PipTaskOrganizer pipTaskOrganizer, 217 FloatingContentCoordinator floatingContentCoordinator, 218 DeviceConfigProxy deviceConfig, 219 PipSnapAlgorithm pipSnapAlgorithm, 220 SysUiState sysUiState, 221 PipUiEventLogger pipUiEventLogger) { 222 // Initialize the Pip input consumer 223 mContext = context; 224 mActivityManager = activityManager; 225 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 226 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 227 mMenuController = menuController; 228 mMenuController.addListener(new PipMenuListener()); 229 mSnapAlgorithm = pipSnapAlgorithm; 230 mGesture = new DefaultPipTouchGesture(); 231 mMotionHelper = new PipMotionHelper(mContext, pipTaskOrganizer, mMenuController, 232 mSnapAlgorithm, floatingContentCoordinator); 233 mPipResizeGestureHandler = 234 new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper, 235 deviceConfig, pipTaskOrganizer, menuController, this::getMovementBounds, 236 this::updateMovementBounds, sysUiState, pipUiEventLogger); 237 mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler, 238 () -> mMenuController.showMenuWithDelay(MENU_STATE_FULL, mMotionHelper.getBounds(), 239 true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()), 240 menuController::hideMenu); 241 242 Resources res = context.getResources(); 243 mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); 244 mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); 245 reloadResources(); 246 247 // Register the listener for input consumer touch events 248 inputConsumerController.setInputListener(this::handleTouchEvent); 249 inputConsumerController.setRegistrationListener(this::onRegistrationChanged); 250 251 mPipBoundsHandler = pipBoundsHandler; 252 mFloatingContentCoordinator = floatingContentCoordinator; 253 mConnection = new PipAccessibilityInteractionConnection(mContext, mMotionHelper, 254 pipTaskOrganizer, pipSnapAlgorithm, this::onAccessibilityShowMenu, 255 this::updateMovementBounds, mHandler); 256 257 mPipUiEventLogger = pipUiEventLogger; 258 259 mTargetView = new DismissCircleView(context); 260 mTargetViewContainer = new FrameLayout(context); 261 mTargetViewContainer.setBackgroundDrawable( 262 context.getDrawable(R.drawable.floating_dismiss_gradient_transition)); 263 mTargetViewContainer.setClipChildren(false); 264 mTargetViewContainer.addView(mTargetView); 265 266 mMagnetizedPip = mMotionHelper.getMagnetizedPip(); 267 mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); 268 updateMagneticTargetSize(); 269 270 mMagnetizedPip.setAnimateStuckToTarget( 271 (target, velX, velY, flung, after) -> { 272 mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); 273 return Unit.INSTANCE; 274 }); 275 mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { 276 @Override 277 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { 278 // Show the dismiss target, in case the initial touch event occurred within the 279 // magnetic field radius. 280 showDismissTargetMaybe(); 281 } 282 283 @Override 284 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 285 float velX, float velY, boolean wasFlungOut) { 286 if (wasFlungOut) { 287 mMotionHelper.flingToSnapTarget(velX, velY, null, null); 288 hideDismissTarget(); 289 } else { 290 mMotionHelper.setSpringingToTouch(true); 291 } 292 } 293 294 @Override 295 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 296 mMotionHelper.notifyDismissalPending(); 297 298 mHandler.post(() -> { 299 mMotionHelper.animateDismiss(); 300 hideDismissTarget(); 301 }); 302 303 mPipUiEventLogger.log( 304 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); 305 } 306 }); 307 308 mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView); 309 } 310 reloadResources()311 private void reloadResources() { 312 final Resources res = mContext.getResources(); 313 mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); 314 mExpandedShortestEdgeSize = res.getDimensionPixelSize( 315 R.dimen.pip_expanded_shortest_edge_size); 316 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); 317 mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); 318 updateMagneticTargetSize(); 319 } 320 updateMagneticTargetSize()321 private void updateMagneticTargetSize() { 322 if (mTargetView == null) { 323 return; 324 } 325 326 final Resources res = mContext.getResources(); 327 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); 328 final FrameLayout.LayoutParams newParams = 329 new FrameLayout.LayoutParams(targetSize, targetSize); 330 newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 331 newParams.bottomMargin = mContext.getResources().getDimensionPixelSize( 332 R.dimen.floating_dismiss_bottom_margin); 333 mTargetView.setLayoutParams(newParams); 334 335 // Set the magnetic field radius equal to the target size from the center of the target 336 mMagneticTarget.setMagneticFieldRadiusPx( 337 (int) (targetSize * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); 338 } 339 shouldShowResizeHandle()340 private boolean shouldShowResizeHandle() { 341 return !mPipBoundsHandler.hasSaveReentryBounds(); 342 } 343 setTouchGesture(PipTouchGesture gesture)344 public void setTouchGesture(PipTouchGesture gesture) { 345 mGesture = gesture; 346 } 347 setTouchEnabled(boolean enabled)348 public void setTouchEnabled(boolean enabled) { 349 mTouchState.setAllowTouches(enabled); 350 } 351 showPictureInPictureMenu()352 public void showPictureInPictureMenu() { 353 // Only show the menu if the user isn't currently interacting with the PiP 354 if (!mTouchState.isUserInteracting()) { 355 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 356 false /* allowMenuTimeout */, willResizeMenu(), 357 shouldShowResizeHandle()); 358 } 359 } 360 onActivityPinned()361 public void onActivityPinned() { 362 createOrUpdateDismissTarget(); 363 364 mShowPipMenuOnAnimationEnd = true; 365 mPipResizeGestureHandler.onActivityPinned(); 366 mFloatingContentCoordinator.onContentAdded(mMotionHelper); 367 } 368 onActivityUnpinned(ComponentName topPipActivity)369 public void onActivityUnpinned(ComponentName topPipActivity) { 370 if (topPipActivity == null) { 371 // Clean up state after the last PiP activity is removed 372 cleanUpDismissTarget(); 373 374 mFloatingContentCoordinator.onContentRemoved(mMotionHelper); 375 } 376 mPipResizeGestureHandler.onActivityUnpinned(); 377 } 378 onPinnedStackAnimationEnded( @ipAnimationController.TransitionDirection int direction)379 public void onPinnedStackAnimationEnded( 380 @PipAnimationController.TransitionDirection int direction) { 381 // Always synchronize the motion helper bounds once PiP animations finish 382 mMotionHelper.synchronizePinnedStackBounds(); 383 updateMovementBounds(); 384 if (direction == TRANSITION_DIRECTION_TO_PIP) { 385 // Set the initial bounds as the user resize bounds. 386 mPipResizeGestureHandler.setUserResizeBounds(mMotionHelper.getBounds()); 387 } 388 389 if (mShowPipMenuOnAnimationEnd) { 390 mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), 391 true /* allowMenuTimeout */, false /* willResizeMenu */, 392 shouldShowResizeHandle()); 393 mShowPipMenuOnAnimationEnd = false; 394 } 395 } 396 onConfigurationChanged()397 public void onConfigurationChanged() { 398 mPipResizeGestureHandler.onConfigurationChanged(); 399 mMotionHelper.synchronizePinnedStackBounds(); 400 reloadResources(); 401 402 // Recreate the dismiss target for the new orientation. 403 createOrUpdateDismissTarget(); 404 } 405 onImeVisibilityChanged(boolean imeVisible, int imeHeight)406 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 407 mIsImeShowing = imeVisible; 408 mImeHeight = imeHeight; 409 } 410 onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)411 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { 412 mIsShelfShowing = shelfVisible; 413 mShelfHeight = shelfHeight; 414 } 415 adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds)416 public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { 417 final Rect toMovementBounds = new Rect(); 418 mSnapAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0); 419 final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; 420 if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { 421 outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); 422 } 423 } 424 425 /** 426 * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. 427 */ onAspectRatioChanged()428 public void onAspectRatioChanged() { 429 mPipResizeGestureHandler.invalidateUserResizeBounds(); 430 } 431 onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)432 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, 433 boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { 434 // Set the user resized bounds equal to the new normal bounds in case they were 435 // invalidated (e.g. by an aspect ratio change). 436 if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { 437 mPipResizeGestureHandler.setUserResizeBounds(normalBounds); 438 } 439 440 final int bottomOffset = mIsImeShowing ? mImeHeight : 0; 441 final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); 442 if (fromDisplayRotationChanged) { 443 mTouchState.reset(); 444 } 445 446 // Re-calculate the expanded bounds 447 mNormalBounds.set(normalBounds); 448 Rect normalMovementBounds = new Rect(); 449 mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds, 450 bottomOffset); 451 452 if (mMovementBounds.isEmpty()) { 453 // mMovementBounds is not initialized yet and a clean movement bounds without 454 // bottom offset shall be used later in this function. 455 mSnapAlgorithm.getMovementBounds(curBounds, insetBounds, mMovementBounds, 456 0 /* bottomOffset */); 457 } 458 459 // Calculate the expanded size 460 float aspectRatio = (float) normalBounds.width() / normalBounds.height(); 461 Point displaySize = new Point(); 462 mContext.getDisplay().getRealSize(displaySize); 463 Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, 464 mExpandedShortestEdgeSize, displaySize.x, displaySize.y); 465 mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight()); 466 Rect expandedMovementBounds = new Rect(); 467 mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds, 468 bottomOffset); 469 470 mPipResizeGestureHandler.updateMinSize(mNormalBounds.width(), mNormalBounds.height()); 471 mPipResizeGestureHandler.updateMaxSize(mExpandedBounds.width(), mExpandedBounds.height()); 472 473 // The extra offset does not really affect the movement bounds, but are applied based on the 474 // current state (ime showing, or shelf offset) when we need to actually shift 475 int extraOffset = Math.max( 476 mIsImeShowing ? mImeOffset : 0, 477 !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); 478 479 // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not 480 // occluded by the IME or shelf. 481 if (fromImeAdjustment || fromShelfAdjustment) { 482 if (mTouchState.isUserInteracting()) { 483 // Defer the update of the current movement bounds until after the user finishes 484 // touching the screen 485 } else { 486 final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu(); 487 final Rect toMovementBounds = new Rect(); 488 mSnapAlgorithm.getMovementBounds(curBounds, insetBounds, 489 toMovementBounds, mIsImeShowing ? mImeHeight : 0); 490 final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; 491 // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this 492 // case 493 final int toBottom = toMovementBounds.bottom < toMovementBounds.top 494 ? toMovementBounds.bottom 495 : toMovementBounds.bottom - extraOffset; 496 497 if (isExpanded) { 498 curBounds.set(mExpandedBounds); 499 mSnapAlgorithm.applySnapFraction(curBounds, toMovementBounds, 500 mSavedSnapFraction); 501 } 502 503 if (prevBottom < toBottom) { 504 // The movement bounds are expanding 505 if (curBounds.top > prevBottom - mBottomOffsetBufferPx) { 506 mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); 507 } 508 } else if (prevBottom > toBottom) { 509 // The movement bounds are shrinking 510 if (curBounds.top > toBottom - mBottomOffsetBufferPx) { 511 mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); 512 } 513 } 514 } 515 } 516 517 // Update the movement bounds after doing the calculations based on the old movement bounds 518 // above 519 mNormalMovementBounds.set(normalMovementBounds); 520 mExpandedMovementBounds.set(expandedMovementBounds); 521 mDisplayRotation = displayRotation; 522 mInsetBounds.set(insetBounds); 523 updateMovementBounds(); 524 mMovementBoundsExtraOffsets = extraOffset; 525 mConnection.onMovementBoundsChanged(mNormalBounds, mExpandedBounds, mNormalMovementBounds, 526 mExpandedMovementBounds); 527 528 // If we have a deferred resize, apply it now 529 if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { 530 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 531 mNormalMovementBounds, mMovementBounds, true /* immediate */); 532 mSavedSnapFraction = -1f; 533 mDeferResizeToNormalBoundsUntilRotation = -1; 534 } 535 } 536 537 /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ createOrUpdateDismissTarget()538 private void createOrUpdateDismissTarget() { 539 if (!mTargetViewContainer.isAttachedToWindow()) { 540 mHandler.removeCallbacks(mShowTargetAction); 541 mMagneticTargetAnimator.cancel(); 542 543 mTargetViewContainer.setVisibility(View.INVISIBLE); 544 545 try { 546 mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); 547 } catch (IllegalStateException e) { 548 // This shouldn't happen, but if the target is already added, just update its layout 549 // params. 550 mWindowManager.updateViewLayout( 551 mTargetViewContainer, getDismissTargetLayoutParams()); 552 } 553 } else { 554 mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); 555 } 556 } 557 558 /** Returns layout params for the dismiss target, using the latest display metrics. */ getDismissTargetLayoutParams()559 private WindowManager.LayoutParams getDismissTargetLayoutParams() { 560 final Point windowSize = new Point(); 561 mWindowManager.getDefaultDisplay().getRealSize(windowSize); 562 563 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 564 WindowManager.LayoutParams.MATCH_PARENT, 565 mDismissAreaHeight, 566 0, windowSize.y - mDismissAreaHeight, 567 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 568 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 569 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 570 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 571 PixelFormat.TRANSLUCENT); 572 573 lp.setTitle("pip-dismiss-overlay"); 574 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 575 lp.setFitInsetsTypes(0 /* types */); 576 577 return lp; 578 } 579 580 /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ showDismissTargetMaybe()581 private void showDismissTargetMaybe() { 582 createOrUpdateDismissTarget(); 583 584 if (mTargetViewContainer.getVisibility() != View.VISIBLE) { 585 586 mTargetView.setTranslationY(mTargetViewContainer.getHeight()); 587 mTargetViewContainer.setVisibility(View.VISIBLE); 588 589 // Cancel in case we were in the middle of animating it out. 590 mMagneticTargetAnimator.cancel(); 591 mMagneticTargetAnimator 592 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig) 593 .start(); 594 595 ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition( 596 DISMISS_TRANSITION_DURATION_MS); 597 } 598 } 599 600 /** Animates the magnetic dismiss target out and then sets it to GONE. */ hideDismissTarget()601 private void hideDismissTarget() { 602 mHandler.removeCallbacks(mShowTargetAction); 603 mMagneticTargetAnimator 604 .spring(DynamicAnimation.TRANSLATION_Y, 605 mTargetViewContainer.getHeight(), 606 mTargetSpringConfig) 607 .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE)) 608 .start(); 609 610 ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition( 611 DISMISS_TRANSITION_DURATION_MS); 612 } 613 614 /** 615 * Removes the dismiss target and cancels any pending callbacks to show it. 616 */ cleanUpDismissTarget()617 private void cleanUpDismissTarget() { 618 mHandler.removeCallbacks(mShowTargetAction); 619 620 if (mTargetViewContainer.isAttachedToWindow()) { 621 mWindowManager.removeViewImmediate(mTargetViewContainer); 622 } 623 } 624 onRegistrationChanged(boolean isRegistered)625 private void onRegistrationChanged(boolean isRegistered) { 626 mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered 627 ? mConnection : null); 628 if (!isRegistered && mTouchState.isUserInteracting()) { 629 // If the input consumer is unregistered while the user is interacting, then we may not 630 // get the final TOUCH_UP event, so clean up the dismiss target as well 631 cleanUpDismissTarget(); 632 } 633 } 634 onAccessibilityShowMenu()635 private void onAccessibilityShowMenu() { 636 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 637 true /* allowMenuTimeout */, willResizeMenu(), 638 shouldShowResizeHandle()); 639 } 640 handleTouchEvent(InputEvent inputEvent)641 private boolean handleTouchEvent(InputEvent inputEvent) { 642 // Skip any non motion events 643 if (!(inputEvent instanceof MotionEvent)) { 644 return true; 645 } 646 // Skip touch handling until we are bound to the controller 647 if (mPinnedStackController == null) { 648 return true; 649 } 650 651 MotionEvent ev = (MotionEvent) inputEvent; 652 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN 653 && mPipResizeGestureHandler.willStartResizeGesture(ev)) { 654 // Initialize the touch state for the gesture, but immediately reset to invalidate the 655 // gesture 656 mTouchState.onTouchEvent(ev); 657 mTouchState.reset(); 658 return true; 659 } 660 661 if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) 662 && mMagnetizedPip.maybeConsumeMotionEvent(ev)) { 663 // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event 664 // to the touch state. Touch state needs a DOWN event in order to later process MOVE 665 // events it'll receive if the object is dragged out of the magnetic field. 666 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 667 mTouchState.onTouchEvent(ev); 668 } 669 670 // Continue tracking velocity when the object is in the magnetic field, since we want to 671 // respect touch input velocity if the object is dragged out and then flung. 672 mTouchState.addMovementToVelocityTracker(ev); 673 674 return true; 675 } 676 677 // Update the touch state 678 mTouchState.onTouchEvent(ev); 679 680 boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; 681 682 switch (ev.getAction()) { 683 case MotionEvent.ACTION_DOWN: { 684 mGesture.onDown(mTouchState); 685 break; 686 } 687 case MotionEvent.ACTION_MOVE: { 688 if (mGesture.onMove(mTouchState)) { 689 break; 690 } 691 692 shouldDeliverToMenu = !mTouchState.isDragging(); 693 break; 694 } 695 case MotionEvent.ACTION_UP: { 696 // Update the movement bounds again if the state has changed since the user started 697 // dragging (ie. when the IME shows) 698 updateMovementBounds(); 699 700 if (mGesture.onUp(mTouchState)) { 701 break; 702 } 703 704 // Fall through to clean up 705 } 706 case MotionEvent.ACTION_CANCEL: { 707 shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); 708 mTouchState.reset(); 709 break; 710 } 711 case MotionEvent.ACTION_HOVER_ENTER: 712 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably 713 // on and changing MotionEvents into HoverEvents. 714 // Let's not enable menu show/hide for a11y services. 715 if (!mAccessibilityManager.isTouchExplorationEnabled()) { 716 mTouchState.removeHoverExitTimeoutCallback(); 717 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 718 false /* allowMenuTimeout */, false /* willResizeMenu */, 719 shouldShowResizeHandle()); 720 } 721 case MotionEvent.ACTION_HOVER_MOVE: { 722 if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { 723 sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 724 mSendingHoverAccessibilityEvents = true; 725 } 726 break; 727 } 728 case MotionEvent.ACTION_HOVER_EXIT: { 729 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably 730 // on and changing MotionEvents into HoverEvents. 731 // Let's not enable menu show/hide for a11y services. 732 if (!mAccessibilityManager.isTouchExplorationEnabled()) { 733 mTouchState.scheduleHoverExitTimeoutCallback(); 734 } 735 if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { 736 sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 737 mSendingHoverAccessibilityEvents = false; 738 } 739 break; 740 } 741 } 742 743 // Deliver the event to PipMenuActivity to handle button click if the menu has shown. 744 if (shouldDeliverToMenu) { 745 final MotionEvent cloneEvent = MotionEvent.obtain(ev); 746 // Send the cancel event and cancel menu timeout if it starts to drag. 747 if (mTouchState.startedDragging()) { 748 cloneEvent.setAction(MotionEvent.ACTION_CANCEL); 749 mMenuController.pokeMenu(); 750 } 751 752 mMenuController.handlePointerEvent(cloneEvent); 753 } 754 755 return true; 756 } 757 sendAccessibilityHoverEvent(int type)758 private void sendAccessibilityHoverEvent(int type) { 759 if (!mAccessibilityManager.isEnabled()) { 760 return; 761 } 762 763 AccessibilityEvent event = AccessibilityEvent.obtain(type); 764 event.setImportantForAccessibility(true); 765 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 766 event.setWindowId( 767 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 768 mAccessibilityManager.sendAccessibilityEvent(event); 769 } 770 771 /** 772 * Updates the appearance of the menu and scrim on top of the PiP while dismissing. 773 */ updateDismissFraction()774 private void updateDismissFraction() { 775 // Skip updating the dismiss fraction when the IME is showing. This is to work around an 776 // issue where starting the menu activity for the dismiss overlay will steal the window 777 // focus, which closes the IME. 778 if (mMenuController != null && !mIsImeShowing) { 779 Rect bounds = mMotionHelper.getBounds(); 780 final float target = mInsetBounds.bottom; 781 float fraction = 0f; 782 if (bounds.bottom > target) { 783 final float distance = bounds.bottom - target; 784 fraction = Math.min(distance / bounds.height(), 1f); 785 } 786 if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) { 787 // Update if the fraction > 0, or if fraction == 0 and the menu was already visible 788 mMenuController.setDismissFraction(fraction); 789 } 790 } 791 } 792 793 /** 794 * Sets the controller to update the system of changes from user interaction. 795 */ setPinnedStackController(IPinnedStackController controller)796 void setPinnedStackController(IPinnedStackController controller) { 797 mPinnedStackController = controller; 798 } 799 800 /** 801 * Sets the menu visibility. 802 */ setMenuState(int menuState, boolean resize, Runnable callback)803 private void setMenuState(int menuState, boolean resize, Runnable callback) { 804 if (mMenuState == menuState && !resize) { 805 return; 806 } 807 808 if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { 809 // Save the current snap fraction and if we do not drag or move the PiP, then 810 // we store back to this snap fraction. Otherwise, we'll reset the snap 811 // fraction and snap to the closest edge. 812 if (resize) { 813 Rect expandedBounds = new Rect(mExpandedBounds); 814 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, 815 mMovementBounds, mExpandedMovementBounds, callback); 816 } 817 } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { 818 // Try and restore the PiP to the closest edge, using the saved snap fraction 819 // if possible 820 if (resize) { 821 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 822 // This is a very special case: when the menu is expanded and visible, 823 // navigating to another activity can trigger auto-enter PiP, and if the 824 // revealed activity has a forced rotation set, then the controller will get 825 // updated with the new rotation of the display. However, at the same time, 826 // SystemUI will try to hide the menu by creating an animation to the normal 827 // bounds which are now stale. In such a case we defer the animation to the 828 // normal bounds until after the next onMovementBoundsChanged() call to get the 829 // bounds in the new orientation 830 try { 831 int displayRotation = mPinnedStackController.getDisplayRotation(); 832 if (mDisplayRotation != displayRotation) { 833 mDeferResizeToNormalBoundsUntilRotation = displayRotation; 834 } 835 } catch (RemoteException e) { 836 Log.e(TAG, "Could not get display rotation from controller"); 837 } 838 } 839 840 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 841 Rect restoreBounds = new Rect(getUserResizeBounds()); 842 Rect restoredMovementBounds = new Rect(); 843 mSnapAlgorithm.getMovementBounds(restoreBounds, mInsetBounds, 844 restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); 845 mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, 846 restoredMovementBounds, mMovementBounds, false /* immediate */); 847 mSavedSnapFraction = -1f; 848 } 849 } else { 850 mSavedSnapFraction = -1f; 851 } 852 } 853 mMenuState = menuState; 854 updateMovementBounds(); 855 // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip 856 // as well, or it can't handle a11y focus and pip menu can't perform any action. 857 onRegistrationChanged(menuState == MENU_STATE_NONE); 858 if (menuState == MENU_STATE_NONE) { 859 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); 860 } else if (menuState == MENU_STATE_FULL) { 861 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); 862 } 863 } 864 865 /** 866 * @return the motion helper. 867 */ getMotionHelper()868 public PipMotionHelper getMotionHelper() { 869 return mMotionHelper; 870 } 871 872 @VisibleForTesting getPipResizeGestureHandler()873 PipResizeGestureHandler getPipResizeGestureHandler() { 874 return mPipResizeGestureHandler; 875 } 876 877 @VisibleForTesting setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler)878 void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { 879 mPipResizeGestureHandler = pipResizeGestureHandler; 880 } 881 882 @VisibleForTesting setPipMotionHelper(PipMotionHelper pipMotionHelper)883 void setPipMotionHelper(PipMotionHelper pipMotionHelper) { 884 mMotionHelper = pipMotionHelper; 885 } 886 887 /** 888 * @return the unexpanded bounds. 889 */ getNormalBounds()890 public Rect getNormalBounds() { 891 return mNormalBounds; 892 } 893 getUserResizeBounds()894 Rect getUserResizeBounds() { 895 return mPipResizeGestureHandler.getUserResizeBounds(); 896 } 897 898 /** 899 * Gesture controlling normal movement of the PIP. 900 */ 901 private class DefaultPipTouchGesture extends PipTouchGesture { 902 private final Point mStartPosition = new Point(); 903 private final PointF mDelta = new PointF(); 904 private boolean mShouldHideMenuAfterFling; 905 906 @Override onDown(PipTouchState touchState)907 public void onDown(PipTouchState touchState) { 908 if (!touchState.isUserInteracting()) { 909 return; 910 } 911 912 Rect bounds = mMotionHelper.getPossiblyAnimatingBounds(); 913 mDelta.set(0f, 0f); 914 mStartPosition.set(bounds.left, bounds.top); 915 mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; 916 mMotionHelper.setSpringingToTouch(false); 917 918 // If the menu is still visible then just poke the menu 919 // so that it will timeout after the user stops touching it 920 if (mMenuState != MENU_STATE_NONE) { 921 mMenuController.pokeMenu(); 922 } 923 } 924 925 @Override onMove(PipTouchState touchState)926 public boolean onMove(PipTouchState touchState) { 927 if (!touchState.isUserInteracting()) { 928 return false; 929 } 930 931 if (touchState.startedDragging()) { 932 mSavedSnapFraction = -1f; 933 934 if (mEnableDismissDragToEdge) { 935 if (mTargetViewContainer.getVisibility() != View.VISIBLE) { 936 mHandler.removeCallbacks(mShowTargetAction); 937 showDismissTargetMaybe(); 938 } 939 } 940 } 941 942 if (touchState.isDragging()) { 943 // Move the pinned stack freely 944 final PointF lastDelta = touchState.getLastTouchDelta(); 945 float lastX = mStartPosition.x + mDelta.x; 946 float lastY = mStartPosition.y + mDelta.y; 947 float left = lastX + lastDelta.x; 948 float top = lastY + lastDelta.y; 949 950 // Add to the cumulative delta after bounding the position 951 mDelta.x += left - lastX; 952 mDelta.y += top - lastY; 953 954 mTmpBounds.set(mMotionHelper.getPossiblyAnimatingBounds()); 955 mTmpBounds.offsetTo((int) left, (int) top); 956 mMotionHelper.movePip(mTmpBounds, true /* isDragging */); 957 958 final PointF curPos = touchState.getLastTouchPosition(); 959 if (mMovementWithinDismiss) { 960 // Track if movement remains near the bottom edge to identify swipe to dismiss 961 mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; 962 } 963 return true; 964 } 965 return false; 966 } 967 968 @Override onUp(PipTouchState touchState)969 public boolean onUp(PipTouchState touchState) { 970 if (mEnableDismissDragToEdge) { 971 hideDismissTarget(); 972 } 973 974 if (!touchState.isUserInteracting()) { 975 return false; 976 } 977 978 final PointF vel = touchState.getVelocity(); 979 980 if (touchState.isDragging()) { 981 if (mMenuState != MENU_STATE_NONE) { 982 // If the menu is still visible, then just poke the menu so that 983 // it will timeout after the user stops touching it 984 mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(), 985 true /* allowMenuTimeout */, willResizeMenu(), 986 shouldShowResizeHandle()); 987 } 988 mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; 989 990 // Reset the touch state on up before the fling settles 991 mTouchState.reset(); 992 mMotionHelper.flingToSnapTarget(vel.x, vel.y, 993 PipTouchHandler.this::updateDismissFraction /* updateAction */, 994 this::flingEndAction /* endAction */); 995 } else if (mTouchState.isDoubleTap()) { 996 // Expand to fullscreen if this is a double tap 997 // the PiP should be frozen until the transition ends 998 setTouchEnabled(false); 999 mMotionHelper.expandPipToFullscreen(); 1000 } else if (mMenuState != MENU_STATE_FULL) { 1001 if (!mTouchState.isWaitingForDoubleTap()) { 1002 // User has stalled long enough for this not to be a drag or a double tap, just 1003 // expand the menu 1004 mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), 1005 true /* allowMenuTimeout */, willResizeMenu(), 1006 shouldShowResizeHandle()); 1007 } else { 1008 // Next touch event _may_ be the second tap for the double-tap, schedule a 1009 // fallback runnable to trigger the menu if no touch event occurs before the 1010 // next tap 1011 mTouchState.scheduleDoubleTapTimeoutCallback(); 1012 } 1013 } 1014 return true; 1015 } 1016 flingEndAction()1017 private void flingEndAction() { 1018 if (mShouldHideMenuAfterFling) { 1019 // If the menu is not visible, then we can still be showing the activity for the 1020 // dismiss overlay, so just finish it after the animation completes 1021 mMenuController.hideMenu(); 1022 } 1023 } 1024 }; 1025 1026 /** 1027 * Updates the current movement bounds based on whether the menu is currently visible and 1028 * resized. 1029 */ updateMovementBounds()1030 private void updateMovementBounds() { 1031 mSnapAlgorithm.getMovementBounds(mMotionHelper.getBounds(), mInsetBounds, 1032 mMovementBounds, mIsImeShowing ? mImeHeight : 0); 1033 mMotionHelper.setCurrentMovementBounds(mMovementBounds); 1034 1035 boolean isMenuExpanded = mMenuState == MENU_STATE_FULL; 1036 mPipBoundsHandler.setMinEdgeSize( 1037 isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0); 1038 } 1039 getMovementBounds(Rect curBounds)1040 private Rect getMovementBounds(Rect curBounds) { 1041 Rect movementBounds = new Rect(); 1042 mSnapAlgorithm.getMovementBounds(curBounds, mInsetBounds, 1043 movementBounds, mIsImeShowing ? mImeHeight : 0); 1044 return movementBounds; 1045 } 1046 1047 /** 1048 * @return whether the menu will resize as a part of showing the full menu. 1049 */ willResizeMenu()1050 private boolean willResizeMenu() { 1051 if (!mEnableResize) { 1052 return false; 1053 } 1054 return mExpandedBounds.width() != mNormalBounds.width() 1055 || mExpandedBounds.height() != mNormalBounds.height(); 1056 } 1057 dump(PrintWriter pw, String prefix)1058 public void dump(PrintWriter pw, String prefix) { 1059 final String innerPrefix = prefix + " "; 1060 pw.println(prefix + TAG); 1061 pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); 1062 pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); 1063 pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); 1064 pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); 1065 pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); 1066 pw.println(innerPrefix + "mMenuState=" + mMenuState); 1067 pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 1068 pw.println(innerPrefix + "mImeHeight=" + mImeHeight); 1069 pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); 1070 pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); 1071 pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); 1072 pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + mEnableDismissDragToEdge); 1073 pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); 1074 mTouchState.dump(pw, innerPrefix); 1075 mMotionHelper.dump(pw, innerPrefix); 1076 if (mPipResizeGestureHandler != null) { 1077 mPipResizeGestureHandler.dump(pw, innerPrefix); 1078 } 1079 } 1080 1081 } 1082