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