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