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