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 package com.android.wm.shell.pip.phone; 17 18 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_PINCH_RESIZE; 19 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; 20 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; 21 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; 22 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; 23 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; 24 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; 25 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Point; 29 import android.graphics.PointF; 30 import android.graphics.Rect; 31 import android.graphics.Region; 32 import android.hardware.input.InputManager; 33 import android.os.Looper; 34 import android.provider.DeviceConfig; 35 import android.view.BatchedInputEventReceiver; 36 import android.view.Choreographer; 37 import android.view.InputChannel; 38 import android.view.InputEvent; 39 import android.view.InputEventReceiver; 40 import android.view.InputMonitor; 41 import android.view.MotionEvent; 42 import android.view.ViewConfiguration; 43 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.internal.policy.TaskResizingAlgorithm; 47 import com.android.wm.shell.R; 48 import com.android.wm.shell.common.ShellExecutor; 49 import com.android.wm.shell.common.pip.PipBoundsAlgorithm; 50 import com.android.wm.shell.common.pip.PipBoundsState; 51 import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; 52 import com.android.wm.shell.common.pip.PipUiEventLogger; 53 import com.android.wm.shell.pip.PipAnimationController; 54 import com.android.wm.shell.pip.PipTaskOrganizer; 55 56 import java.io.PrintWriter; 57 import java.util.function.Consumer; 58 import java.util.function.Function; 59 60 /** 61 * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to 62 * trigger dynamic resize. 63 */ 64 public class PipResizeGestureHandler { 65 66 private static final String TAG = "PipResizeGestureHandler"; 67 private static final int PINCH_RESIZE_SNAP_DURATION = 250; 68 private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f; 69 70 private final Context mContext; 71 private final PipBoundsAlgorithm mPipBoundsAlgorithm; 72 private final PipMotionHelper mMotionHelper; 73 private final PipBoundsState mPipBoundsState; 74 private final PipTouchState mPipTouchState; 75 private final PipTaskOrganizer mPipTaskOrganizer; 76 private final PhonePipMenuController mPhonePipMenuController; 77 private final PipDismissTargetHandler mPipDismissTargetHandler; 78 private final PipUiEventLogger mPipUiEventLogger; 79 private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; 80 private final int mDisplayId; 81 private final ShellExecutor mMainExecutor; 82 private final Region mTmpRegion = new Region(); 83 84 private final PointF mDownPoint = new PointF(); 85 private final PointF mDownSecondPoint = new PointF(); 86 private final PointF mLastPoint = new PointF(); 87 private final PointF mLastSecondPoint = new PointF(); 88 private final Point mMaxSize = new Point(); 89 private final Point mMinSize = new Point(); 90 private final Rect mLastResizeBounds = new Rect(); 91 private final Rect mUserResizeBounds = new Rect(); 92 private final Rect mDownBounds = new Rect(); 93 private final Rect mDragCornerSize = new Rect(); 94 private final Rect mTmpTopLeftCorner = new Rect(); 95 private final Rect mTmpTopRightCorner = new Rect(); 96 private final Rect mTmpBottomLeftCorner = new Rect(); 97 private final Rect mTmpBottomRightCorner = new Rect(); 98 private final Rect mDisplayBounds = new Rect(); 99 private final Function<Rect, Rect> mMovementBoundsSupplier; 100 private final Runnable mUpdateMovementBoundsRunnable; 101 private final Consumer<Rect> mUpdateResizeBoundsCallback; 102 103 private int mDelta; 104 private float mTouchSlop; 105 106 private boolean mAllowGesture; 107 private boolean mIsAttached; 108 private boolean mIsEnabled; 109 private boolean mEnablePinchResize; 110 private boolean mEnableDragCornerResize; 111 private boolean mIsSysUiStateValid; 112 private boolean mThresholdCrossed; 113 private boolean mOngoingPinchToResize = false; 114 private float mAngle = 0; 115 int mFirstIndex = -1; 116 int mSecondIndex = -1; 117 118 private InputMonitor mInputMonitor; 119 private InputEventReceiver mInputEventReceiver; 120 121 private int mCtrlType; 122 private int mOhmOffset; 123 PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, PipBoundsState pipBoundsState, PipMotionHelper motionHelper, PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer, PipDismissTargetHandler pipDismissTargetHandler, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, ShellExecutor mainExecutor)124 public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, 125 PipBoundsState pipBoundsState, PipMotionHelper motionHelper, 126 PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer, 127 PipDismissTargetHandler pipDismissTargetHandler, 128 Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, 129 PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, 130 ShellExecutor mainExecutor) { 131 mContext = context; 132 mDisplayId = context.getDisplayId(); 133 mMainExecutor = mainExecutor; 134 mPipBoundsAlgorithm = pipBoundsAlgorithm; 135 mPipBoundsState = pipBoundsState; 136 mMotionHelper = motionHelper; 137 mPipTouchState = pipTouchState; 138 mPipTaskOrganizer = pipTaskOrganizer; 139 mPipDismissTargetHandler = pipDismissTargetHandler; 140 mMovementBoundsSupplier = movementBoundsSupplier; 141 mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; 142 mPhonePipMenuController = menuActivityController; 143 mPipUiEventLogger = pipUiEventLogger; 144 mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); 145 146 mUpdateResizeBoundsCallback = (rect) -> { 147 mUserResizeBounds.set(rect); 148 mMotionHelper.synchronizePinnedStackBounds(); 149 mUpdateMovementBoundsRunnable.run(); 150 resetState(); 151 }; 152 } 153 init()154 public void init() { 155 mContext.getDisplay().getRealSize(mMaxSize); 156 reloadResources(); 157 158 mEnablePinchResize = DeviceConfig.getBoolean( 159 DeviceConfig.NAMESPACE_SYSTEMUI, 160 PIP_PINCH_RESIZE, 161 /* defaultValue = */ true); 162 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, 163 mMainExecutor, 164 new DeviceConfig.OnPropertiesChangedListener() { 165 @Override 166 public void onPropertiesChanged(DeviceConfig.Properties properties) { 167 if (properties.getKeyset().contains(PIP_PINCH_RESIZE)) { 168 mEnablePinchResize = properties.getBoolean( 169 PIP_PINCH_RESIZE, /* defaultValue = */ true); 170 } 171 } 172 }); 173 } 174 onConfigurationChanged()175 public void onConfigurationChanged() { 176 reloadResources(); 177 } 178 179 /** 180 * Called when SysUI state changed. 181 * 182 * @param isSysUiStateValid Is SysUI valid or not. 183 */ onSystemUiStateChanged(boolean isSysUiStateValid)184 public void onSystemUiStateChanged(boolean isSysUiStateValid) { 185 mIsSysUiStateValid = isSysUiStateValid; 186 } 187 reloadResources()188 private void reloadResources() { 189 final Resources res = mContext.getResources(); 190 mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); 191 mEnableDragCornerResize = res.getBoolean(R.bool.config_pipEnableDragCornerResize); 192 mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); 193 } 194 resetDragCorners()195 private void resetDragCorners() { 196 mDragCornerSize.set(0, 0, mDelta, mDelta); 197 mTmpTopLeftCorner.set(mDragCornerSize); 198 mTmpTopRightCorner.set(mDragCornerSize); 199 mTmpBottomLeftCorner.set(mDragCornerSize); 200 mTmpBottomRightCorner.set(mDragCornerSize); 201 } 202 disposeInputChannel()203 private void disposeInputChannel() { 204 if (mInputEventReceiver != null) { 205 mInputEventReceiver.dispose(); 206 mInputEventReceiver = null; 207 } 208 if (mInputMonitor != null) { 209 mInputMonitor.dispose(); 210 mInputMonitor = null; 211 } 212 } 213 onActivityPinned()214 void onActivityPinned() { 215 mIsAttached = true; 216 updateIsEnabled(); 217 } 218 onActivityUnpinned()219 void onActivityUnpinned() { 220 mIsAttached = false; 221 mUserResizeBounds.setEmpty(); 222 updateIsEnabled(); 223 } 224 updateIsEnabled()225 private void updateIsEnabled() { 226 boolean isEnabled = mIsAttached; 227 if (isEnabled == mIsEnabled) { 228 return; 229 } 230 mIsEnabled = isEnabled; 231 disposeInputChannel(); 232 233 if (mIsEnabled) { 234 // Register input event receiver 235 mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( 236 "pip-resize", mDisplayId); 237 try { 238 mMainExecutor.executeBlocking(() -> { 239 mInputEventReceiver = new PipResizeInputEventReceiver( 240 mInputMonitor.getInputChannel(), Looper.myLooper()); 241 }); 242 } catch (InterruptedException e) { 243 throw new RuntimeException("Failed to create input event receiver", e); 244 } 245 } 246 } 247 248 @VisibleForTesting onInputEvent(InputEvent ev)249 void onInputEvent(InputEvent ev) { 250 if (!mEnableDragCornerResize && !mEnablePinchResize) { 251 // No need to handle anything if neither form of resizing is enabled. 252 return; 253 } 254 255 if (!mPipTouchState.getAllowInputEvents()) { 256 // No need to handle anything if touches are not enabled 257 return; 258 } 259 260 // Don't allow resize when PiP is stashed. 261 if (mPipBoundsState.isStashed()) { 262 return; 263 } 264 265 if (ev instanceof MotionEvent) { 266 MotionEvent mv = (MotionEvent) ev; 267 int action = mv.getActionMasked(); 268 final Rect pipBounds = mPipBoundsState.getBounds(); 269 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 270 if (!pipBounds.contains((int) mv.getRawX(), (int) mv.getRawY()) 271 && mPhonePipMenuController.isMenuVisible()) { 272 mPhonePipMenuController.hideMenu(); 273 } 274 } 275 276 if (mEnablePinchResize && mOngoingPinchToResize) { 277 onPinchResize(mv); 278 } else if (mEnableDragCornerResize) { 279 onDragCornerResize(mv); 280 } 281 } 282 } 283 284 /** 285 * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize. 286 */ hasOngoingGesture()287 public boolean hasOngoingGesture() { 288 return mCtrlType != CTRL_NONE || mOngoingPinchToResize; 289 } 290 291 /** 292 * Check whether the current x,y coordinate is within the region in which drag-resize should 293 * start. 294 * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which 295 * overlaps with the PIP window while the rest goes outside of the PIP window. 296 * _ _ _ _ 297 * |_|_|_________|_|_| 298 * |_|_| |_|_| 299 * | PIP | 300 * | WINDOW | 301 * _|_ _|_ 302 * |_|_|_________|_|_| 303 * |_|_| |_|_| 304 */ isWithinDragResizeRegion(int x, int y)305 public boolean isWithinDragResizeRegion(int x, int y) { 306 if (!mEnableDragCornerResize) { 307 return false; 308 } 309 310 final Rect currentPipBounds = mPipBoundsState.getBounds(); 311 if (currentPipBounds == null) { 312 return false; 313 } 314 resetDragCorners(); 315 mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, 316 currentPipBounds.top - mDelta / 2); 317 mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, 318 currentPipBounds.top - mDelta / 2); 319 mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, 320 currentPipBounds.bottom - mDelta / 2); 321 mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, 322 currentPipBounds.bottom - mDelta / 2); 323 324 mTmpRegion.setEmpty(); 325 mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); 326 mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); 327 mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); 328 mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); 329 330 return mTmpRegion.contains(x, y); 331 } 332 isUsingPinchToZoom()333 public boolean isUsingPinchToZoom() { 334 return mEnablePinchResize; 335 } 336 isResizing()337 public boolean isResizing() { 338 return mAllowGesture; 339 } 340 willStartResizeGesture(MotionEvent ev)341 public boolean willStartResizeGesture(MotionEvent ev) { 342 if (isInValidSysUiState()) { 343 switch (ev.getActionMasked()) { 344 case MotionEvent.ACTION_DOWN: 345 if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) { 346 return true; 347 } 348 break; 349 350 case MotionEvent.ACTION_POINTER_DOWN: 351 if (mEnablePinchResize && ev.getPointerCount() == 2) { 352 onPinchResize(ev); 353 mOngoingPinchToResize = mAllowGesture; 354 return mAllowGesture; 355 } 356 break; 357 358 default: 359 break; 360 } 361 } 362 return false; 363 } 364 setCtrlType(int x, int y)365 private void setCtrlType(int x, int y) { 366 final Rect currentPipBounds = mPipBoundsState.getBounds(); 367 368 Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); 369 370 mDisplayBounds.set(movementBounds.left, 371 movementBounds.top, 372 movementBounds.right + currentPipBounds.width(), 373 movementBounds.bottom + currentPipBounds.height()); 374 375 if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top 376 && currentPipBounds.left != mDisplayBounds.left) { 377 mCtrlType |= CTRL_LEFT; 378 mCtrlType |= CTRL_TOP; 379 } 380 if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top 381 && currentPipBounds.right != mDisplayBounds.right) { 382 mCtrlType |= CTRL_RIGHT; 383 mCtrlType |= CTRL_TOP; 384 } 385 if (mTmpBottomRightCorner.contains(x, y) 386 && currentPipBounds.bottom != mDisplayBounds.bottom 387 && currentPipBounds.right != mDisplayBounds.right) { 388 mCtrlType |= CTRL_RIGHT; 389 mCtrlType |= CTRL_BOTTOM; 390 } 391 if (mTmpBottomLeftCorner.contains(x, y) 392 && currentPipBounds.bottom != mDisplayBounds.bottom 393 && currentPipBounds.left != mDisplayBounds.left) { 394 mCtrlType |= CTRL_LEFT; 395 mCtrlType |= CTRL_BOTTOM; 396 } 397 } 398 isInValidSysUiState()399 private boolean isInValidSysUiState() { 400 return mIsSysUiStateValid; 401 } 402 403 @VisibleForTesting onPinchResize(MotionEvent ev)404 void onPinchResize(MotionEvent ev) { 405 int action = ev.getActionMasked(); 406 407 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 408 mFirstIndex = -1; 409 mSecondIndex = -1; 410 mAllowGesture = false; 411 finishResize(); 412 } 413 414 if (ev.getPointerCount() != 2) { 415 return; 416 } 417 418 final Rect pipBounds = mPipBoundsState.getBounds(); 419 if (action == MotionEvent.ACTION_POINTER_DOWN) { 420 if (mFirstIndex == -1 && mSecondIndex == -1 421 && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) 422 && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { 423 mAllowGesture = true; 424 mFirstIndex = 0; 425 mSecondIndex = 1; 426 mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); 427 mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); 428 mDownBounds.set(pipBounds); 429 430 mLastPoint.set(mDownPoint); 431 mLastSecondPoint.set(mLastSecondPoint); 432 mLastResizeBounds.set(mDownBounds); 433 } 434 } 435 436 if (action == MotionEvent.ACTION_MOVE) { 437 if (mFirstIndex == -1 || mSecondIndex == -1) { 438 return; 439 } 440 441 float x0 = ev.getRawX(mFirstIndex); 442 float y0 = ev.getRawY(mFirstIndex); 443 float x1 = ev.getRawX(mSecondIndex); 444 float y1 = ev.getRawY(mSecondIndex); 445 mLastPoint.set(x0, y0); 446 mLastSecondPoint.set(x1, y1); 447 448 // Capture inputs 449 if (!mThresholdCrossed 450 && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop 451 || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) { 452 pilferPointers(); 453 mThresholdCrossed = true; 454 // Reset the down to begin resizing from this point 455 mDownPoint.set(mLastPoint); 456 mDownSecondPoint.set(mLastSecondPoint); 457 458 if (mPhonePipMenuController.isMenuVisible()) { 459 mPhonePipMenuController.hideMenu(); 460 } 461 } 462 463 if (mThresholdCrossed) { 464 mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint, 465 mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, 466 mDownBounds, mLastResizeBounds); 467 468 mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, 469 mAngle, null); 470 mPipBoundsState.setHasUserResizedPip(true); 471 } 472 } 473 } 474 onDragCornerResize(MotionEvent ev)475 private void onDragCornerResize(MotionEvent ev) { 476 int action = ev.getActionMasked(); 477 float x = ev.getX(); 478 float y = ev.getY() - mOhmOffset; 479 if (action == MotionEvent.ACTION_DOWN) { 480 mLastResizeBounds.setEmpty(); 481 mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y); 482 if (mAllowGesture) { 483 setCtrlType((int) x, (int) y); 484 mDownPoint.set(x, y); 485 mDownBounds.set(mPipBoundsState.getBounds()); 486 } 487 } else if (mAllowGesture) { 488 switch (action) { 489 case MotionEvent.ACTION_POINTER_DOWN: 490 // We do not support multi touch for resizing via drag 491 mAllowGesture = false; 492 break; 493 case MotionEvent.ACTION_MOVE: 494 // Capture inputs 495 if (!mThresholdCrossed 496 && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) { 497 mThresholdCrossed = true; 498 // Reset the down to begin resizing from this point 499 mDownPoint.set(x, y); 500 mInputMonitor.pilferPointers(); 501 } 502 if (mThresholdCrossed) { 503 if (mPhonePipMenuController.isMenuVisible()) { 504 mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE, 505 false /* resize */); 506 } 507 final Rect currentPipBounds = mPipBoundsState.getBounds(); 508 mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, 509 mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, 510 mMinSize.y, mMaxSize, true, 511 mDownBounds.width() > mDownBounds.height())); 512 mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds, 513 mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, 514 true /* useCurrentSize */); 515 mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, 516 null); 517 mPipBoundsState.setHasUserResizedPip(true); 518 } 519 break; 520 case MotionEvent.ACTION_UP: 521 case MotionEvent.ACTION_CANCEL: 522 finishResize(); 523 break; 524 } 525 } 526 } 527 snapToMovementBoundsEdge(Rect bounds, Rect movementBounds)528 private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { 529 final int leftEdge = bounds.left; 530 531 532 final int fromLeft = Math.abs(leftEdge - movementBounds.left); 533 final int fromRight = Math.abs(movementBounds.right - leftEdge); 534 535 // The PIP will be snapped to either the right or left edge, so calculate which one 536 // is closest to the current position. 537 final int newLeft = fromLeft < fromRight 538 ? movementBounds.left : movementBounds.right; 539 540 bounds.offsetTo(newLeft, mLastResizeBounds.top); 541 } 542 543 /** 544 * Resizes the pip window and updates user-resized bounds. 545 * 546 * @param bounds target bounds to resize to 547 * @param snapFraction snap fraction to apply after resizing 548 */ 549 void userResizeTo(Rect bounds, float snapFraction) { 550 Rect finalBounds = new Rect(bounds); 551 552 // get the current movement bounds 553 final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); 554 555 // snap the target bounds to the either left or right edge, by choosing the closer one 556 snapToMovementBoundsEdge(finalBounds, movementBounds); 557 558 // apply the requested snap fraction onto the target bounds 559 mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); 560 561 // resize from current bounds to target bounds without animation 562 mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); 563 // set the flag that pip has been resized 564 mPipBoundsState.setHasUserResizedPip(true); 565 566 // finish the resize operation and update the state of the bounds 567 mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); 568 } 569 570 private void finishResize() { 571 if (!mLastResizeBounds.isEmpty()) { 572 // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped 573 // position correctly. Drag-resize does not need to move, so just finalize resize. 574 if (mOngoingPinchToResize) { 575 final Rect startBounds = new Rect(mLastResizeBounds); 576 // If user resize is pretty close to max size, just auto resize to max. 577 if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x 578 || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { 579 resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); 580 } 581 582 // get the current movement bounds 583 final Rect movementBounds = mPipBoundsAlgorithm 584 .getMovementBounds(mLastResizeBounds); 585 586 // snap mLastResizeBounds to the correct edge based on movement bounds 587 snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); 588 589 final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( 590 mLastResizeBounds, movementBounds); 591 mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); 592 593 // disable any touch events beyond resizing too 594 mPipTouchState.setAllowInputEvents(false); 595 596 mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, 597 PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { 598 // enable touch events 599 mPipTouchState.setAllowInputEvents(true); 600 }); 601 } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, mUpdateResizeBoundsCallback)602 mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, 603 PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, 604 mUpdateResizeBoundsCallback); 605 } 606 final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; 607 mPipDismissTargetHandler setMagneticFieldRadiusPercent(magnetRadiusPercent)608 .setMagneticFieldRadiusPercent(magnetRadiusPercent); mPipUiEventLogger.log( PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE)609 mPipUiEventLogger.log( 610 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); 611 } else { resetState()612 resetState(); 613 } 614 } 615 616 private void resetState() { 617 mCtrlType = CTRL_NONE; 618 mAngle = 0; 619 mOngoingPinchToResize = false; 620 mAllowGesture = false; 621 mThresholdCrossed = false; 622 } 623 624 void setUserResizeBounds(Rect bounds) { 625 mUserResizeBounds.set(bounds); 626 } 627 628 void invalidateUserResizeBounds() { 629 mUserResizeBounds.setEmpty(); 630 } 631 632 Rect getUserResizeBounds() { 633 return mUserResizeBounds; 634 } 635 636 @VisibleForTesting 637 Rect getLastResizeBounds() { 638 return mLastResizeBounds; 639 } 640 641 @VisibleForTesting 642 void pilferPointers() { 643 mInputMonitor.pilferPointers(); 644 } 645 646 647 @VisibleForTesting public void updateMaxSize(int maxX, int maxY) { 648 mMaxSize.set(maxX, maxY); 649 } 650 651 @VisibleForTesting public void updateMinSize(int minX, int minY) { 652 mMinSize.set(minX, minY); 653 } 654 655 void setOhmOffset(int offset) { 656 mOhmOffset = offset; 657 } 658 659 private float distanceBetween(PointF p1, PointF p2) { 660 return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); 661 } 662 663 private void resizeRectAboutCenter(Rect rect, int w, int h) { 664 int cx = rect.centerX(); 665 int cy = rect.centerY(); 666 int l = cx - w / 2; 667 int r = l + w; 668 int t = cy - h / 2; 669 int b = t + h; 670 rect.set(l, t, r, b); 671 } 672 673 public void dump(PrintWriter pw, String prefix) { 674 final String innerPrefix = prefix + " "; 675 pw.println(prefix + TAG); 676 pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); 677 pw.println(innerPrefix + "mIsAttached=" + mIsAttached); 678 pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); 679 pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); 680 pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); 681 pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset); 682 } 683 684 class PipResizeInputEventReceiver extends BatchedInputEventReceiver { 685 PipResizeInputEventReceiver(InputChannel channel, Looper looper) { 686 super(channel, looper, Choreographer.getInstance()); 687 } 688 689 public void onInputEvent(InputEvent event) { 690 PipResizeGestureHandler.this.onInputEvent(event); 691 finishInputEvent(event, true); 692 } 693 } 694 } 695