1 /* 2 * Copyright (C) 2020 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.wm.shell.pip.phone; 18 19 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; 20 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD; 21 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; 22 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; 23 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; 24 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; 25 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; 26 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; 27 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; 28 29 import android.annotation.NonNull; 30 import android.annotation.SuppressLint; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.Point; 35 import android.graphics.PointF; 36 import android.graphics.Rect; 37 import android.provider.DeviceConfig; 38 import android.util.Size; 39 import android.view.DisplayCutout; 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.annotations.VisibleForTesting; 49 import com.android.internal.protolog.common.ProtoLog; 50 import com.android.wm.shell.R; 51 import com.android.wm.shell.common.FloatingContentCoordinator; 52 import com.android.wm.shell.common.ShellExecutor; 53 import com.android.wm.shell.common.pip.PipBoundsAlgorithm; 54 import com.android.wm.shell.common.pip.PipBoundsState; 55 import com.android.wm.shell.common.pip.PipUiEventLogger; 56 import com.android.wm.shell.common.pip.PipUtils; 57 import com.android.wm.shell.common.pip.SizeSpecSource; 58 import com.android.wm.shell.pip.PipAnimationController; 59 import com.android.wm.shell.pip.PipTaskOrganizer; 60 import com.android.wm.shell.pip.PipTransitionController; 61 import com.android.wm.shell.protolog.ShellProtoLogGroup; 62 import com.android.wm.shell.sysui.ShellInit; 63 64 import java.io.PrintWriter; 65 66 /** 67 * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding 68 * the PIP. 69 */ 70 public class PipTouchHandler { 71 72 private static final String TAG = "PipTouchHandler"; 73 private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; 74 75 // Allow PIP to resize to a slightly bigger state upon touch 76 private boolean mEnableResize; 77 private final Context mContext; 78 private final PipBoundsAlgorithm mPipBoundsAlgorithm; 79 @NonNull private final PipBoundsState mPipBoundsState; 80 @NonNull private final SizeSpecSource mSizeSpecSource; 81 private final PipUiEventLogger mPipUiEventLogger; 82 private final PipDismissTargetHandler mPipDismissTargetHandler; 83 private final PipTaskOrganizer mPipTaskOrganizer; 84 private final ShellExecutor mMainExecutor; 85 86 private PipResizeGestureHandler mPipResizeGestureHandler; 87 88 private final PhonePipMenuController mMenuController; 89 private final AccessibilityManager mAccessibilityManager; 90 91 /** 92 * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the 93 * screen, it will be shown in "stashed" mode, where PIP will only show partially. 94 */ 95 private boolean mEnableStash = true; 96 97 private float mStashVelocityThreshold; 98 99 // The reference inset bounds, used to determine the dismiss fraction 100 private final Rect mInsetBounds = new Rect(); 101 102 // Used to workaround an issue where the WM rotation happens before we are notified, allowing 103 // us to send stale bounds 104 private int mDeferResizeToNormalBoundsUntilRotation = -1; 105 private int mDisplayRotation; 106 107 private final PipAccessibilityInteractionConnection mConnection; 108 109 // Behaviour states 110 private int mMenuState = MENU_STATE_NONE; 111 private boolean mIsImeShowing; 112 private int mImeHeight; 113 private int mImeOffset; 114 private boolean mIsShelfShowing; 115 private int mShelfHeight; 116 private int mMovementBoundsExtraOffsets; 117 private int mBottomOffsetBufferPx; 118 private float mSavedSnapFraction = -1f; 119 private boolean mSendingHoverAccessibilityEvents; 120 private boolean mMovementWithinDismiss; 121 122 // Touch state 123 private final PipTouchState mTouchState; 124 private final FloatingContentCoordinator mFloatingContentCoordinator; 125 private PipMotionHelper mMotionHelper; 126 private PipTouchGesture mGesture; 127 128 // Temp vars 129 private final Rect mTmpBounds = new Rect(); 130 131 /** 132 * A listener for the PIP menu activity. 133 */ 134 private class PipMenuListener implements PhonePipMenuController.Listener { 135 @Override onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)136 public void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { 137 PipTouchHandler.this.onPipMenuStateChangeStart(menuState, resize, callback); 138 } 139 140 @Override onPipMenuStateChangeFinish(int menuState)141 public void onPipMenuStateChangeFinish(int menuState) { 142 setMenuState(menuState); 143 } 144 145 @Override onPipExpand()146 public void onPipExpand() { 147 mMotionHelper.expandLeavePip(false /* skipAnimation */); 148 } 149 150 @Override onEnterSplit()151 public void onEnterSplit() { 152 mMotionHelper.expandIntoSplit(); 153 } 154 155 @Override onPipDismiss()156 public void onPipDismiss() { 157 mTouchState.removeDoubleTapTimeoutCallback(); 158 mMotionHelper.dismissPip(); 159 } 160 161 @Override onPipShowMenu()162 public void onPipShowMenu() { 163 mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), 164 true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); 165 } 166 } 167 168 @SuppressLint("InflateParams") PipTouchHandler(Context context, ShellInit shellInit, PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @NonNull SizeSpecSource sizeSpecSource, PipTaskOrganizer pipTaskOrganizer, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor)169 public PipTouchHandler(Context context, 170 ShellInit shellInit, 171 PhonePipMenuController menuController, 172 PipBoundsAlgorithm pipBoundsAlgorithm, 173 @NonNull PipBoundsState pipBoundsState, 174 @NonNull SizeSpecSource sizeSpecSource, 175 PipTaskOrganizer pipTaskOrganizer, 176 PipMotionHelper pipMotionHelper, 177 FloatingContentCoordinator floatingContentCoordinator, 178 PipUiEventLogger pipUiEventLogger, 179 ShellExecutor mainExecutor) { 180 mContext = context; 181 mMainExecutor = mainExecutor; 182 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 183 mPipBoundsAlgorithm = pipBoundsAlgorithm; 184 mPipBoundsState = pipBoundsState; 185 mSizeSpecSource = sizeSpecSource; 186 mPipTaskOrganizer = pipTaskOrganizer; 187 mMenuController = menuController; 188 mPipUiEventLogger = pipUiEventLogger; 189 mFloatingContentCoordinator = floatingContentCoordinator; 190 mMenuController.addListener(new PipMenuListener()); 191 mGesture = new DefaultPipTouchGesture(); 192 mMotionHelper = pipMotionHelper; 193 mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, 194 mMotionHelper, mainExecutor); 195 mTouchState = new PipTouchState(ViewConfiguration.get(context), 196 () -> { 197 if (mPipBoundsState.isStashed()) { 198 animateToUnStashedState(); 199 mPipUiEventLogger.log( 200 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); 201 mPipBoundsState.setStashed(STASH_TYPE_NONE); 202 } else { 203 mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, 204 mPipBoundsState.getBounds(), true /* allowMenuTimeout */, 205 willResizeMenu(), 206 shouldShowResizeHandle()); 207 } 208 }, 209 menuController::hideMenu, 210 mainExecutor); 211 mPipResizeGestureHandler = 212 new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, 213 mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler, 214 this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger, 215 menuController, mainExecutor); 216 mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, 217 mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), 218 this::onAccessibilityShowMenu, this::updateMovementBounds, 219 this::animateToUnStashedState, mainExecutor); 220 221 // TODO(b/181599115): This should really be initializes as part of the pip controller, but 222 // until all PIP implementations derive from the controller, just initialize the touch handler 223 // if it is needed 224 if (!PipUtils.isPip2ExperimentEnabled()) { 225 shellInit.addInitCallback(this::onInit, this); 226 } 227 } 228 229 /** 230 * Called when the touch handler is initialized. 231 */ onInit()232 public void onInit() { 233 Resources res = mContext.getResources(); 234 mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); 235 reloadResources(); 236 237 mMotionHelper.init(); 238 mPipResizeGestureHandler.init(); 239 mPipDismissTargetHandler.init(); 240 241 mEnableStash = DeviceConfig.getBoolean( 242 DeviceConfig.NAMESPACE_SYSTEMUI, 243 PIP_STASHING, 244 /* defaultValue = */ true); 245 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, 246 mMainExecutor, 247 properties -> { 248 if (properties.getKeyset().contains(PIP_STASHING)) { 249 mEnableStash = properties.getBoolean( 250 PIP_STASHING, /* defaultValue = */ true); 251 } 252 }); 253 mStashVelocityThreshold = DeviceConfig.getFloat( 254 DeviceConfig.NAMESPACE_SYSTEMUI, 255 PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, 256 DEFAULT_STASH_VELOCITY_THRESHOLD); 257 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, 258 mMainExecutor, 259 properties -> { 260 if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) { 261 mStashVelocityThreshold = properties.getFloat( 262 PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, 263 DEFAULT_STASH_VELOCITY_THRESHOLD); 264 } 265 }); 266 } 267 getTransitionHandler()268 public PipTransitionController getTransitionHandler() { 269 return mPipTaskOrganizer.getTransitionController(); 270 } 271 reloadResources()272 private void reloadResources() { 273 final Resources res = mContext.getResources(); 274 mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); 275 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); 276 mPipDismissTargetHandler.updateMagneticTargetSize(); 277 } 278 onOverlayChanged()279 public void onOverlayChanged() { 280 // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly. 281 mPipDismissTargetHandler.init(); 282 } 283 shouldShowResizeHandle()284 private boolean shouldShowResizeHandle() { 285 return false; 286 } 287 setTouchGesture(PipTouchGesture gesture)288 public void setTouchGesture(PipTouchGesture gesture) { 289 mGesture = gesture; 290 } 291 setTouchEnabled(boolean enabled)292 public void setTouchEnabled(boolean enabled) { 293 mTouchState.setAllowTouches(enabled); 294 } 295 showPictureInPictureMenu()296 public void showPictureInPictureMenu() { 297 // Only show the menu if the user isn't currently interacting with the PiP 298 if (!mTouchState.isUserInteracting()) { 299 mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), 300 false /* allowMenuTimeout */, willResizeMenu(), 301 shouldShowResizeHandle()); 302 } 303 } 304 onActivityPinned()305 public void onActivityPinned() { 306 mPipDismissTargetHandler.createOrUpdateDismissTarget(); 307 308 mPipResizeGestureHandler.onActivityPinned(); 309 mFloatingContentCoordinator.onContentAdded(mMotionHelper); 310 } 311 onActivityUnpinned(ComponentName topPipActivity)312 public void onActivityUnpinned(ComponentName topPipActivity) { 313 if (topPipActivity == null) { 314 // Clean up state after the last PiP activity is removed 315 mPipDismissTargetHandler.cleanUpDismissTarget(); 316 317 mFloatingContentCoordinator.onContentRemoved(mMotionHelper); 318 } 319 mPipResizeGestureHandler.onActivityUnpinned(); 320 } 321 onPinnedStackAnimationEnded( @ipAnimationController.TransitionDirection int direction)322 public void onPinnedStackAnimationEnded( 323 @PipAnimationController.TransitionDirection int direction) { 324 // Always synchronize the motion helper bounds once PiP animations finish 325 mMotionHelper.synchronizePinnedStackBounds(); 326 updateMovementBounds(); 327 if (direction == TRANSITION_DIRECTION_TO_PIP) { 328 // Set the initial bounds as the user resize bounds. 329 mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); 330 } 331 } 332 onConfigurationChanged()333 public void onConfigurationChanged() { 334 mPipResizeGestureHandler.onConfigurationChanged(); 335 mMotionHelper.synchronizePinnedStackBounds(); 336 reloadResources(); 337 338 if (mPipTaskOrganizer.isInPip()) { 339 // Recreate the dismiss target for the new orientation. 340 mPipDismissTargetHandler.createOrUpdateDismissTarget(); 341 } 342 } 343 onImeVisibilityChanged(boolean imeVisible, int imeHeight)344 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 345 mIsImeShowing = imeVisible; 346 mImeHeight = imeHeight; 347 } 348 onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)349 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { 350 mIsShelfShowing = shelfVisible; 351 mShelfHeight = shelfHeight; 352 } 353 354 /** 355 * Called when SysUI state changed. 356 * 357 * @param isSysUiStateValid Is SysUI valid or not. 358 */ onSystemUiStateChanged(boolean isSysUiStateValid)359 public void onSystemUiStateChanged(boolean isSysUiStateValid) { 360 mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid); 361 } 362 adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds)363 public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { 364 final Rect toMovementBounds = new Rect(); 365 mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0); 366 final int prevBottom = mPipBoundsState.getMovementBounds().bottom 367 - mMovementBoundsExtraOffsets; 368 if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { 369 outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); 370 } 371 } 372 373 /** 374 * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. 375 */ onAspectRatioChanged()376 public void onAspectRatioChanged() { 377 mPipResizeGestureHandler.invalidateUserResizeBounds(); 378 } 379 onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)380 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, 381 boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { 382 // Set the user resized bounds equal to the new normal bounds in case they were 383 // invalidated (e.g. by an aspect ratio change). 384 if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { 385 mPipResizeGestureHandler.setUserResizeBounds(normalBounds); 386 } 387 388 final int bottomOffset = mIsImeShowing ? mImeHeight : 0; 389 final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); 390 if (fromDisplayRotationChanged) { 391 mTouchState.reset(); 392 } 393 394 // Re-calculate the expanded bounds 395 Rect normalMovementBounds = new Rect(); 396 mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds, 397 normalMovementBounds, bottomOffset); 398 399 if (mPipBoundsState.getMovementBounds().isEmpty()) { 400 // mMovementBounds is not initialized yet and a clean movement bounds without 401 // bottom offset shall be used later in this function. 402 mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, 403 mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */); 404 } 405 406 // Calculate the expanded size 407 float aspectRatio = (float) normalBounds.width() / normalBounds.height(); 408 Size expandedSize = mSizeSpecSource.getDefaultSize(aspectRatio); 409 mPipBoundsState.setExpandedBounds( 410 new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight())); 411 Rect expandedMovementBounds = new Rect(); 412 mPipBoundsAlgorithm.getMovementBounds( 413 mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds, 414 bottomOffset); 415 416 updatePipSizeConstraints(normalBounds, aspectRatio); 417 418 // The extra offset does not really affect the movement bounds, but are applied based on the 419 // current state (ime showing, or shelf offset) when we need to actually shift 420 int extraOffset = Math.max( 421 mIsImeShowing ? mImeOffset : 0, 422 !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); 423 424 // Update the movement bounds after doing the calculations based on the old movement bounds 425 // above 426 mPipBoundsState.setNormalMovementBounds(normalMovementBounds); 427 mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); 428 mDisplayRotation = displayRotation; 429 mInsetBounds.set(insetBounds); 430 updateMovementBounds(); 431 mMovementBoundsExtraOffsets = extraOffset; 432 mConnection.onMovementBoundsChanged(normalBounds, mPipBoundsState.getExpandedBounds(), 433 mPipBoundsState.getNormalMovementBounds(), 434 mPipBoundsState.getExpandedMovementBounds()); 435 436 // If we have a deferred resize, apply it now 437 if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { 438 mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, 439 mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(), 440 true /* immediate */); 441 mSavedSnapFraction = -1f; 442 mDeferResizeToNormalBoundsUntilRotation = -1; 443 } 444 } 445 446 /** 447 * Update the values for min/max allowed size of picture in picture window based on the aspect 448 * ratio. 449 * @param aspectRatio aspect ratio to use for the calculation of min/max size 450 */ updateMinMaxSize(float aspectRatio)451 public void updateMinMaxSize(float aspectRatio) { 452 updatePipSizeConstraints(mPipBoundsState.getNormalBounds(), 453 aspectRatio); 454 } 455 updatePipSizeConstraints(Rect normalBounds, float aspectRatio)456 private void updatePipSizeConstraints(Rect normalBounds, 457 float aspectRatio) { 458 if (mPipResizeGestureHandler.isUsingPinchToZoom()) { 459 updatePinchResizeSizeConstraints(aspectRatio); 460 } else { 461 mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); 462 mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), 463 mPipBoundsState.getExpandedBounds().height()); 464 } 465 } 466 updatePinchResizeSizeConstraints(float aspectRatio)467 private void updatePinchResizeSizeConstraints(float aspectRatio) { 468 final int minWidth, minHeight, maxWidth, maxHeight; 469 470 minWidth = mSizeSpecSource.getMinSize(aspectRatio).getWidth(); 471 minHeight = mSizeSpecSource.getMinSize(aspectRatio).getHeight(); 472 maxWidth = mSizeSpecSource.getMaxSize(aspectRatio).getWidth(); 473 maxHeight = mSizeSpecSource.getMaxSize(aspectRatio).getHeight(); 474 475 mPipResizeGestureHandler.updateMinSize(minWidth, minHeight); 476 mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight); 477 mPipBoundsState.setMaxSize(maxWidth, maxHeight); 478 mPipBoundsState.setMinSize(minWidth, minHeight); 479 } 480 481 /** 482 * TODO Add appropriate description 483 */ onRegistrationChanged(boolean isRegistered)484 public void onRegistrationChanged(boolean isRegistered) { 485 if (isRegistered) { 486 mConnection.register(mAccessibilityManager); 487 } else { 488 mAccessibilityManager.setPictureInPictureActionReplacingConnection(null); 489 } 490 if (!isRegistered && mTouchState.isUserInteracting()) { 491 // If the input consumer is unregistered while the user is interacting, then we may not 492 // get the final TOUCH_UP event, so clean up the dismiss target as well 493 mPipDismissTargetHandler.cleanUpDismissTarget(); 494 } 495 } 496 onAccessibilityShowMenu()497 private void onAccessibilityShowMenu() { 498 mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), 499 true /* allowMenuTimeout */, willResizeMenu(), 500 shouldShowResizeHandle()); 501 } 502 503 /** 504 * TODO Add appropriate description 505 */ handleTouchEvent(InputEvent inputEvent)506 public boolean handleTouchEvent(InputEvent inputEvent) { 507 // Skip any non motion events 508 if (!(inputEvent instanceof MotionEvent)) { 509 return true; 510 } 511 512 // do not process input event if not allowed 513 if (!mTouchState.getAllowInputEvents()) { 514 return true; 515 } 516 517 MotionEvent ev = (MotionEvent) inputEvent; 518 if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) { 519 // Initialize the touch state for the gesture, but immediately reset to invalidate the 520 // gesture 521 mTouchState.onTouchEvent(ev); 522 mTouchState.reset(); 523 return true; 524 } 525 526 if (mPipResizeGestureHandler.hasOngoingGesture()) { 527 mPipDismissTargetHandler.hideDismissTargetMaybe(); 528 return true; 529 } 530 531 if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) 532 && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) { 533 // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event 534 // to the touch state. Touch state needs a DOWN event in order to later process MOVE 535 // events it'll receive if the object is dragged out of the magnetic field. 536 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 537 mTouchState.onTouchEvent(ev); 538 } 539 540 // Continue tracking velocity when the object is in the magnetic field, since we want to 541 // respect touch input velocity if the object is dragged out and then flung. 542 mTouchState.addMovementToVelocityTracker(ev); 543 544 return true; 545 } 546 547 // Update the touch state 548 mTouchState.onTouchEvent(ev); 549 550 boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; 551 552 switch (ev.getAction()) { 553 case MotionEvent.ACTION_DOWN: { 554 mGesture.onDown(mTouchState); 555 break; 556 } 557 case MotionEvent.ACTION_MOVE: { 558 if (mGesture.onMove(mTouchState)) { 559 break; 560 } 561 562 shouldDeliverToMenu = !mTouchState.isDragging(); 563 break; 564 } 565 case MotionEvent.ACTION_UP: { 566 // Update the movement bounds again if the state has changed since the user started 567 // dragging (ie. when the IME shows) 568 updateMovementBounds(); 569 570 if (mGesture.onUp(mTouchState)) { 571 break; 572 } 573 574 // Fall through to clean up 575 } 576 case MotionEvent.ACTION_CANCEL: { 577 shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); 578 mTouchState.reset(); 579 break; 580 } 581 case MotionEvent.ACTION_HOVER_ENTER: 582 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably 583 // on and changing MotionEvents into HoverEvents. 584 // Let's not enable menu show/hide for a11y services. 585 if (!mAccessibilityManager.isTouchExplorationEnabled()) { 586 mTouchState.removeHoverExitTimeoutCallback(); 587 mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), 588 false /* allowMenuTimeout */, false /* willResizeMenu */, 589 shouldShowResizeHandle()); 590 } 591 case MotionEvent.ACTION_HOVER_MOVE: { 592 if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { 593 sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 594 mSendingHoverAccessibilityEvents = true; 595 } 596 break; 597 } 598 case MotionEvent.ACTION_HOVER_EXIT: { 599 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably 600 // on and changing MotionEvents into HoverEvents. 601 // Let's not enable menu show/hide for a11y services. 602 if (!mAccessibilityManager.isTouchExplorationEnabled()) { 603 mTouchState.scheduleHoverExitTimeoutCallback(); 604 } 605 if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { 606 sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 607 mSendingHoverAccessibilityEvents = false; 608 } 609 break; 610 } 611 } 612 613 shouldDeliverToMenu &= !mPipBoundsState.isStashed(); 614 615 // Deliver the event to PipMenuActivity to handle button click if the menu has shown. 616 if (shouldDeliverToMenu) { 617 final MotionEvent cloneEvent = MotionEvent.obtain(ev); 618 // Send the cancel event and cancel menu timeout if it starts to drag. 619 if (mTouchState.startedDragging()) { 620 cloneEvent.setAction(MotionEvent.ACTION_CANCEL); 621 mMenuController.pokeMenu(); 622 } 623 624 mMenuController.handlePointerEvent(cloneEvent); 625 cloneEvent.recycle(); 626 } 627 628 return true; 629 } 630 sendAccessibilityHoverEvent(int type)631 private void sendAccessibilityHoverEvent(int type) { 632 if (!mAccessibilityManager.isEnabled()) { 633 return; 634 } 635 636 AccessibilityEvent event = AccessibilityEvent.obtain(type); 637 event.setImportantForAccessibility(true); 638 event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); 639 event.setWindowId( 640 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 641 mAccessibilityManager.sendAccessibilityEvent(event); 642 } 643 644 /** 645 * Called when the PiP menu state is in the process of animating/changing from one to another. 646 */ onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)647 private void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { 648 if (mMenuState == menuState && !resize) { 649 return; 650 } 651 652 if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { 653 // Save the current snap fraction and if we do not drag or move the PiP, then 654 // we store back to this snap fraction. Otherwise, we'll reset the snap 655 // fraction and snap to the closest edge. 656 if (resize) { 657 // PIP is too small to show the menu actions and thus needs to be resized to a 658 // size that can fit them all. Resize to the default size. 659 animateToNormalSize(callback); 660 } 661 } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { 662 // Try and restore the PiP to the closest edge, using the saved snap fraction 663 // if possible 664 if (resize && !mPipResizeGestureHandler.isResizing()) { 665 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 666 // This is a very special case: when the menu is expanded and visible, 667 // navigating to another activity can trigger auto-enter PiP, and if the 668 // revealed activity has a forced rotation set, then the controller will get 669 // updated with the new rotation of the display. However, at the same time, 670 // SystemUI will try to hide the menu by creating an animation to the normal 671 // bounds which are now stale. In such a case we defer the animation to the 672 // normal bounds until after the next onMovementBoundsChanged() call to get the 673 // bounds in the new orientation 674 int displayRotation = mContext.getDisplay().getRotation(); 675 if (mDisplayRotation != displayRotation) { 676 mDeferResizeToNormalBoundsUntilRotation = displayRotation; 677 } 678 } 679 680 if (mDeferResizeToNormalBoundsUntilRotation == -1) { 681 animateToUnexpandedState(getUserResizeBounds()); 682 } 683 } else { 684 mSavedSnapFraction = -1f; 685 } 686 } 687 } 688 setMenuState(int menuState)689 private void setMenuState(int menuState) { 690 mMenuState = menuState; 691 updateMovementBounds(); 692 // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip 693 // as well, or it can't handle a11y focus and pip menu can't perform any action. 694 onRegistrationChanged(menuState == MENU_STATE_NONE); 695 if (menuState == MENU_STATE_NONE) { 696 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); 697 } else if (menuState == MENU_STATE_FULL) { 698 mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); 699 } 700 } 701 animateToMaximizedState(Runnable callback)702 private void animateToMaximizedState(Runnable callback) { 703 Rect maxMovementBounds = new Rect(); 704 Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, 705 mPipBoundsState.getMaxSize().y); 706 mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds, 707 mIsImeShowing ? mImeHeight : 0); 708 mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds, 709 mPipBoundsState.getMovementBounds(), maxMovementBounds, 710 callback); 711 } 712 animateToNormalSize(Runnable callback)713 private void animateToNormalSize(Runnable callback) { 714 // Save the current bounds as the user-resize bounds. 715 mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); 716 717 final Size minMenuSize = mMenuController.getEstimatedMinMenuSize(); 718 final Rect normalBounds = mPipBoundsState.getNormalBounds(); 719 final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, 720 minMenuSize); 721 Rect restoredMovementBounds = new Rect(); 722 mPipBoundsAlgorithm.getMovementBounds(destBounds, 723 mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); 724 mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds, 725 mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback); 726 } 727 animateToUnexpandedState(Rect restoreBounds)728 private void animateToUnexpandedState(Rect restoreBounds) { 729 Rect restoredMovementBounds = new Rect(); 730 mPipBoundsAlgorithm.getMovementBounds(restoreBounds, 731 mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); 732 mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, 733 restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */); 734 mSavedSnapFraction = -1f; 735 } 736 animateToUnStashedState()737 private void animateToUnStashedState() { 738 final Rect pipBounds = mPipBoundsState.getBounds(); 739 final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; 740 final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); 741 unStashedBounds.left = onLeftEdge ? mInsetBounds.left 742 : mInsetBounds.right - pipBounds.width(); 743 unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() 744 : mInsetBounds.right; 745 mMotionHelper.animateToUnStashedBounds(unStashedBounds); 746 } 747 748 /** 749 * @return the motion helper. 750 */ 751 public PipMotionHelper getMotionHelper() { 752 return mMotionHelper; 753 } 754 755 @VisibleForTesting 756 public PipResizeGestureHandler getPipResizeGestureHandler() { 757 return mPipResizeGestureHandler; 758 } 759 760 @VisibleForTesting 761 public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { 762 mPipResizeGestureHandler = pipResizeGestureHandler; 763 } 764 765 @VisibleForTesting 766 public void setPipMotionHelper(PipMotionHelper pipMotionHelper) { 767 mMotionHelper = pipMotionHelper; 768 } 769 770 Rect getUserResizeBounds() { 771 return mPipResizeGestureHandler.getUserResizeBounds(); 772 } 773 774 /** 775 * Resizes the pip window and updates user resized bounds 776 * 777 * @param bounds target bounds to resize to 778 * @param snapFraction snap fraction to apply after resizing 779 */ 780 void userResizeTo(Rect bounds, float snapFraction) { 781 mPipResizeGestureHandler.userResizeTo(bounds, snapFraction); 782 } 783 784 /** 785 * Gesture controlling normal movement of the PIP. 786 */ 787 private class DefaultPipTouchGesture extends PipTouchGesture { 788 private final Point mStartPosition = new Point(); 789 private final PointF mDelta = new PointF(); 790 private boolean mShouldHideMenuAfterFling; 791 792 @Override 793 public void onDown(PipTouchState touchState) { 794 if (!touchState.isUserInteracting()) { 795 return; 796 } 797 798 Rect bounds = getPossiblyMotionBounds(); 799 mDelta.set(0f, 0f); 800 mStartPosition.set(bounds.left, bounds.top); 801 mMovementWithinDismiss = touchState.getDownTouchPosition().y 802 >= mPipBoundsState.getMovementBounds().bottom; 803 mMotionHelper.setSpringingToTouch(false); 804 mPipDismissTargetHandler.setTaskLeash(mPipTaskOrganizer.getSurfaceControl()); 805 806 // If the menu is still visible then just poke the menu 807 // so that it will timeout after the user stops touching it 808 if (mMenuState != MENU_STATE_NONE && !mPipBoundsState.isStashed()) { 809 mMenuController.pokeMenu(); 810 } 811 } 812 813 @Override onMove(PipTouchState touchState)814 public boolean onMove(PipTouchState touchState) { 815 if (!touchState.isUserInteracting()) { 816 return false; 817 } 818 819 if (touchState.startedDragging()) { 820 mSavedSnapFraction = -1f; 821 mPipDismissTargetHandler.showDismissTargetMaybe(); 822 } 823 824 if (touchState.isDragging()) { 825 mPipBoundsState.setHasUserMovedPip(true); 826 827 // Move the pinned stack freely 828 final PointF lastDelta = touchState.getLastTouchDelta(); 829 float lastX = mStartPosition.x + mDelta.x; 830 float lastY = mStartPosition.y + mDelta.y; 831 float left = lastX + lastDelta.x; 832 float top = lastY + lastDelta.y; 833 834 // Add to the cumulative delta after bounding the position 835 mDelta.x += left - lastX; 836 mDelta.y += top - lastY; 837 838 mTmpBounds.set(getPossiblyMotionBounds()); 839 mTmpBounds.offsetTo((int) left, (int) top); 840 mMotionHelper.movePip(mTmpBounds, true /* isDragging */); 841 842 final PointF curPos = touchState.getLastTouchPosition(); 843 if (mMovementWithinDismiss) { 844 // Track if movement remains near the bottom edge to identify swipe to dismiss 845 mMovementWithinDismiss = curPos.y >= mPipBoundsState.getMovementBounds().bottom; 846 } 847 return true; 848 } 849 return false; 850 } 851 852 @Override onUp(PipTouchState touchState)853 public boolean onUp(PipTouchState touchState) { 854 mPipDismissTargetHandler.hideDismissTargetMaybe(); 855 mPipDismissTargetHandler.setTaskLeash(null); 856 857 if (!touchState.isUserInteracting()) { 858 return false; 859 } 860 861 final PointF vel = touchState.getVelocity(); 862 863 if (touchState.isDragging()) { 864 if (mMenuState != MENU_STATE_NONE) { 865 // If the menu is still visible, then just poke the menu so that 866 // it will timeout after the user stops touching it 867 mMenuController.showMenu(mMenuState, mPipBoundsState.getBounds(), 868 true /* allowMenuTimeout */, willResizeMenu(), 869 shouldShowResizeHandle()); 870 } 871 mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; 872 873 // Reset the touch state on up before the fling settles 874 mTouchState.reset(); 875 if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) { 876 mMotionHelper.stashToEdge(vel.x, vel.y, this::stashEndAction /* endAction */); 877 } else { 878 if (mPipBoundsState.isStashed()) { 879 // Reset stashed state if previously stashed 880 mPipUiEventLogger.log( 881 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); 882 mPipBoundsState.setStashed(STASH_TYPE_NONE); 883 } 884 mMotionHelper.flingToSnapTarget(vel.x, vel.y, 885 this::flingEndAction /* endAction */); 886 } 887 } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed() 888 && mMenuState != MENU_STATE_FULL) { 889 // If using pinch to zoom, double-tap functions as resizing between max/min size 890 if (mPipResizeGestureHandler.isUsingPinchToZoom()) { 891 final boolean toExpand = mPipBoundsState.getBounds().width() 892 < mPipBoundsState.getMaxSize().x 893 && mPipBoundsState.getBounds().height() 894 < mPipBoundsState.getMaxSize().y; 895 if (mMenuController.isMenuVisible()) { 896 mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); 897 } 898 899 // the size to toggle to after a double tap 900 int nextSize = PipDoubleTapHelper 901 .nextSizeSpec(mPipBoundsState, getUserResizeBounds()); 902 903 // actually toggle to the size chosen 904 if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) { 905 mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); 906 animateToMaximizedState(null); 907 } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) { 908 mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); 909 animateToNormalSize(null); 910 } else { 911 animateToUnexpandedState(getUserResizeBounds()); 912 } 913 } else { 914 // Expand to fullscreen if this is a double tap 915 // the PiP should be frozen until the transition ends 916 setTouchEnabled(false); 917 mMotionHelper.expandLeavePip(false /* skipAnimation */); 918 } 919 } else if (mMenuState != MENU_STATE_FULL) { 920 if (mPipBoundsState.isStashed()) { 921 // Unstash immediately if stashed, and don't wait for the double tap timeout 922 animateToUnStashedState(); 923 mPipUiEventLogger.log( 924 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); 925 mPipBoundsState.setStashed(STASH_TYPE_NONE); 926 mTouchState.removeDoubleTapTimeoutCallback(); 927 } else if (!mTouchState.isWaitingForDoubleTap()) { 928 // User has stalled long enough for this not to be a drag or a double tap, 929 // just expand the menu 930 mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), 931 true /* allowMenuTimeout */, willResizeMenu(), 932 shouldShowResizeHandle()); 933 } else { 934 // Next touch event _may_ be the second tap for the double-tap, schedule a 935 // fallback runnable to trigger the menu if no touch event occurs before the 936 // next tap 937 mTouchState.scheduleDoubleTapTimeoutCallback(); 938 } 939 } 940 return true; 941 } 942 943 private void stashEndAction() { 944 if (mPipBoundsState.getBounds().left < 0 945 && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { 946 mPipUiEventLogger.log( 947 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT); 948 mPipBoundsState.setStashed(STASH_TYPE_LEFT); 949 } else if (mPipBoundsState.getBounds().left >= 0 950 && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { 951 mPipUiEventLogger.log( 952 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT); 953 mPipBoundsState.setStashed(STASH_TYPE_RIGHT); 954 } 955 mMenuController.hideMenu(); 956 } 957 958 private void flingEndAction() { 959 if (mShouldHideMenuAfterFling) { 960 // If the menu is not visible, then we can still be showing the activity for the 961 // dismiss overlay, so just finish it after the animation completes 962 mMenuController.hideMenu(); 963 } 964 } 965 966 private boolean shouldStash(PointF vel, Rect motionBounds) { 967 final boolean flingToLeft = vel.x < -mStashVelocityThreshold; 968 final boolean flingToRight = vel.x > mStashVelocityThreshold; 969 final int offset = motionBounds.width() / 2; 970 final boolean droppingOnLeft = 971 motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset; 972 final boolean droppingOnRight = 973 motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset; 974 975 // Do not allow stash if the destination edge contains display cutout. We only 976 // compare the left and right edges since we do not allow stash on top / bottom. 977 final DisplayCutout displayCutout = 978 mPipBoundsState.getDisplayLayout().getDisplayCutout(); 979 if (displayCutout != null) { 980 if ((flingToLeft || droppingOnLeft) 981 && !displayCutout.getBoundingRectLeft().isEmpty()) { 982 return false; 983 } else if ((flingToRight || droppingOnRight) 984 && !displayCutout.getBoundingRectRight().isEmpty()) { 985 return false; 986 } 987 } 988 989 // If user flings the PIP window above the minimum velocity, stash PIP. 990 // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite 991 // edge. 992 final boolean stashFromFlingToEdge = 993 (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) 994 || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT); 995 996 // If User releases the PIP window while it's out of the display bounds, put 997 // PIP into stashed mode. 998 final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight; 999 1000 return stashFromFlingToEdge || stashFromDroppingOnEdge; 1001 } 1002 } 1003 1004 /** 1005 * Updates the current movement bounds based on whether the menu is currently visible and 1006 * resized. 1007 */ 1008 private void updateMovementBounds() { 1009 mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), 1010 mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); 1011 mMotionHelper.onMovementBoundsChanged(); 1012 } 1013 1014 private Rect getMovementBounds(Rect curBounds) { 1015 Rect movementBounds = new Rect(); 1016 mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds, 1017 movementBounds, mIsImeShowing ? mImeHeight : 0); 1018 return movementBounds; 1019 } 1020 1021 /** 1022 * @return {@code true} if the menu should be resized on tap because app explicitly specifies 1023 * PiP window size that is too small to hold all the actions. 1024 */ 1025 private boolean willResizeMenu() { 1026 if (!mEnableResize) { 1027 return false; 1028 } 1029 final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); 1030 if (estimatedMinMenuSize == null) { 1031 ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1032 "%s: Failed to get estimated menu size", TAG); 1033 return false; 1034 } 1035 final Rect currentBounds = mPipBoundsState.getBounds(); 1036 return currentBounds.width() < estimatedMinMenuSize.getWidth() 1037 || currentBounds.height() < estimatedMinMenuSize.getHeight(); 1038 } 1039 1040 /** 1041 * Returns the PIP bounds if we're not in the middle of a motion operation, or the current, 1042 * temporary motion bounds otherwise. 1043 */ 1044 Rect getPossiblyMotionBounds() { 1045 return mPipBoundsState.getMotionBoundsState().isInMotion() 1046 ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion() 1047 : mPipBoundsState.getBounds(); 1048 } 1049 1050 void setOhmOffset(int offset) { 1051 mPipResizeGestureHandler.setOhmOffset(offset); 1052 } 1053 1054 public void dump(PrintWriter pw, String prefix) { 1055 final String innerPrefix = prefix + " "; 1056 pw.println(prefix + TAG); 1057 pw.println(innerPrefix + "mMenuState=" + mMenuState); 1058 pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 1059 pw.println(innerPrefix + "mImeHeight=" + mImeHeight); 1060 pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); 1061 pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); 1062 pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); 1063 pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); 1064 mPipBoundsAlgorithm.dump(pw, innerPrefix); 1065 mTouchState.dump(pw, innerPrefix); 1066 if (mPipResizeGestureHandler != null) { 1067 mPipResizeGestureHandler.dump(pw, innerPrefix); 1068 } 1069 } 1070 1071 } 1072