• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.pip.phone;
17 
18 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_USER_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.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
25 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
26 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
27 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
28 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
29 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
30 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
31 
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.graphics.Region;
38 import android.hardware.input.InputManager;
39 import android.os.Handler;
40 import android.os.Looper;
41 import android.provider.DeviceConfig;
42 import android.view.BatchedInputEventReceiver;
43 import android.view.Choreographer;
44 import android.view.InputChannel;
45 import android.view.InputEvent;
46 import android.view.InputEventReceiver;
47 import android.view.InputMonitor;
48 import android.view.MotionEvent;
49 import android.view.ViewConfiguration;
50 
51 import com.android.internal.policy.TaskResizingAlgorithm;
52 import com.android.systemui.R;
53 import com.android.systemui.model.SysUiState;
54 import com.android.systemui.pip.PipBoundsHandler;
55 import com.android.systemui.pip.PipTaskOrganizer;
56 import com.android.systemui.pip.PipUiEventLogger;
57 import com.android.systemui.util.DeviceConfigProxy;
58 
59 import java.io.PrintWriter;
60 import java.util.concurrent.Executor;
61 import java.util.function.Function;
62 
63 /**
64  * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
65  * trigger dynamic resize.
66  */
67 public class PipResizeGestureHandler {
68 
69     private static final String TAG = "PipResizeGestureHandler";
70 
71     private static final int INVALID_SYSUI_STATE_MASK =
72             SYSUI_STATE_GLOBAL_ACTIONS_SHOWING
73             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
74             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
75             | SYSUI_STATE_BOUNCER_SHOWING
76             | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
77             | SYSUI_STATE_BUBBLES_EXPANDED
78             | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
79 
80     private final Context mContext;
81     private final PipBoundsHandler mPipBoundsHandler;
82     private final PipMotionHelper mMotionHelper;
83     private final PipMenuActivityController mMenuController;
84     private final int mDisplayId;
85     private final Executor mMainExecutor;
86     private final SysUiState mSysUiState;
87     private final Region mTmpRegion = new Region();
88 
89     private final PointF mDownPoint = new PointF();
90     private final Point mMaxSize = new Point();
91     private final Point mMinSize = new Point();
92     private final Rect mLastResizeBounds = new Rect();
93     private final Rect mUserResizeBounds = new Rect();
94     private final Rect mLastDownBounds = new Rect();
95     private final Rect mDragCornerSize = new Rect();
96     private final Rect mTmpTopLeftCorner = new Rect();
97     private final Rect mTmpTopRightCorner = new Rect();
98     private final Rect mTmpBottomLeftCorner = new Rect();
99     private final Rect mTmpBottomRightCorner = new Rect();
100     private final Rect mDisplayBounds = new Rect();
101     private final Function<Rect, Rect> mMovementBoundsSupplier;
102     private final Runnable mUpdateMovementBoundsRunnable;
103 
104     private int mDelta;
105     private float mTouchSlop;
106     private boolean mAllowGesture;
107     private boolean mIsAttached;
108     private boolean mIsEnabled;
109     private boolean mEnableUserResize;
110     private boolean mThresholdCrossed;
111 
112     private InputMonitor mInputMonitor;
113     private InputEventReceiver mInputEventReceiver;
114     private PipTaskOrganizer mPipTaskOrganizer;
115     private PipUiEventLogger mPipUiEventLogger;
116 
117     private int mCtrlType;
118 
PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController pipMenuController, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, SysUiState sysUiState, PipUiEventLogger pipUiEventLogger)119     public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler,
120             PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig,
121             PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController pipMenuController,
122             Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable,
123             SysUiState sysUiState, PipUiEventLogger pipUiEventLogger) {
124         mContext = context;
125         mDisplayId = context.getDisplayId();
126         mMainExecutor = context.getMainExecutor();
127         mPipBoundsHandler = pipBoundsHandler;
128         mMenuController = pipMenuController;
129         mMotionHelper = motionHelper;
130         mPipTaskOrganizer = pipTaskOrganizer;
131         mMovementBoundsSupplier = movementBoundsSupplier;
132         mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
133         mSysUiState = sysUiState;
134         mPipUiEventLogger = pipUiEventLogger;
135 
136         context.getDisplay().getRealSize(mMaxSize);
137         reloadResources();
138 
139         mEnableUserResize = DeviceConfig.getBoolean(
140                 DeviceConfig.NAMESPACE_SYSTEMUI,
141                 PIP_USER_RESIZE,
142                 /* defaultValue = */ true);
143         deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor,
144                 new DeviceConfig.OnPropertiesChangedListener() {
145                     @Override
146                     public void onPropertiesChanged(DeviceConfig.Properties properties) {
147                         if (properties.getKeyset().contains(PIP_USER_RESIZE)) {
148                             mEnableUserResize = properties.getBoolean(
149                                     PIP_USER_RESIZE, /* defaultValue = */ true);
150                         }
151                     }
152                 });
153     }
154 
onConfigurationChanged()155     public void onConfigurationChanged() {
156         reloadResources();
157     }
158 
reloadResources()159     private void reloadResources() {
160         final Resources res = mContext.getResources();
161         mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size);
162         mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
163     }
164 
resetDragCorners()165     private void resetDragCorners() {
166         mDragCornerSize.set(0, 0, mDelta, mDelta);
167         mTmpTopLeftCorner.set(mDragCornerSize);
168         mTmpTopRightCorner.set(mDragCornerSize);
169         mTmpBottomLeftCorner.set(mDragCornerSize);
170         mTmpBottomRightCorner.set(mDragCornerSize);
171     }
172 
disposeInputChannel()173     private void disposeInputChannel() {
174         if (mInputEventReceiver != null) {
175             mInputEventReceiver.dispose();
176             mInputEventReceiver = null;
177         }
178         if (mInputMonitor != null) {
179             mInputMonitor.dispose();
180             mInputMonitor = null;
181         }
182     }
183 
onActivityPinned()184     void onActivityPinned() {
185         mIsAttached = true;
186         updateIsEnabled();
187     }
188 
onActivityUnpinned()189     void onActivityUnpinned() {
190         mIsAttached = false;
191         mUserResizeBounds.setEmpty();
192         updateIsEnabled();
193     }
194 
updateIsEnabled()195     private void updateIsEnabled() {
196         boolean isEnabled = mIsAttached && mEnableUserResize;
197         if (isEnabled == mIsEnabled) {
198             return;
199         }
200         mIsEnabled = isEnabled;
201         disposeInputChannel();
202 
203         if (mIsEnabled) {
204             // Register input event receiver
205             mInputMonitor = InputManager.getInstance().monitorGestureInput(
206                     "pip-resize", mDisplayId);
207             mInputEventReceiver = new SysUiInputEventReceiver(
208                     mInputMonitor.getInputChannel(), Looper.getMainLooper());
209         }
210     }
211 
onInputEvent(InputEvent ev)212     private void onInputEvent(InputEvent ev) {
213         if (ev instanceof MotionEvent) {
214             onMotionEvent((MotionEvent) ev);
215         }
216     }
217 
218     /**
219      * Check whether the current x,y coordinate is within the region in which drag-resize should
220      * start.
221      * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which
222      * overlaps with the PIP window while the rest goes outside of the PIP window.
223      *  _ _           _ _
224      * |_|_|_________|_|_|
225      * |_|_|         |_|_|
226      *   |     PIP     |
227      *   |   WINDOW    |
228      *  _|_           _|_
229      * |_|_|_________|_|_|
230      * |_|_|         |_|_|
231      */
isWithinTouchRegion(int x, int y)232     public boolean isWithinTouchRegion(int x, int y) {
233         final Rect currentPipBounds = mMotionHelper.getBounds();
234         if (currentPipBounds == null) {
235             return false;
236         }
237         resetDragCorners();
238         mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2,
239                 currentPipBounds.top - mDelta /  2);
240         mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2,
241                 currentPipBounds.top - mDelta /  2);
242         mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2,
243                 currentPipBounds.bottom - mDelta /  2);
244         mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2,
245                 currentPipBounds.bottom - mDelta /  2);
246 
247         mTmpRegion.setEmpty();
248         mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION);
249         mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION);
250         mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION);
251         mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION);
252 
253         return mTmpRegion.contains(x, y);
254     }
255 
willStartResizeGesture(MotionEvent ev)256     public boolean willStartResizeGesture(MotionEvent ev) {
257         return mEnableUserResize && isInValidSysUiState()
258                 && isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY());
259     }
260 
setCtrlType(int x, int y)261     private void setCtrlType(int x, int y) {
262         final Rect currentPipBounds = mMotionHelper.getBounds();
263 
264         Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds);
265         mDisplayBounds.set(movementBounds.left,
266                 movementBounds.top,
267                 movementBounds.right + currentPipBounds.width(),
268                 movementBounds.bottom + currentPipBounds.height());
269 
270         if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
271                 && currentPipBounds.left != mDisplayBounds.left) {
272             mCtrlType |= CTRL_LEFT;
273             mCtrlType |= CTRL_TOP;
274         }
275         if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
276                 && currentPipBounds.right != mDisplayBounds.right) {
277             mCtrlType |= CTRL_RIGHT;
278             mCtrlType |= CTRL_TOP;
279         }
280         if (mTmpBottomRightCorner.contains(x, y)
281                 && currentPipBounds.bottom != mDisplayBounds.bottom
282                 && currentPipBounds.right != mDisplayBounds.right) {
283             mCtrlType |= CTRL_RIGHT;
284             mCtrlType |= CTRL_BOTTOM;
285         }
286         if (mTmpBottomLeftCorner.contains(x, y)
287                 && currentPipBounds.bottom != mDisplayBounds.bottom
288                 && currentPipBounds.left != mDisplayBounds.left) {
289             mCtrlType |= CTRL_LEFT;
290             mCtrlType |= CTRL_BOTTOM;
291         }
292     }
293 
isInValidSysUiState()294     private boolean isInValidSysUiState() {
295         return (mSysUiState.getFlags() & INVALID_SYSUI_STATE_MASK) == 0;
296     }
297 
onMotionEvent(MotionEvent ev)298     private void onMotionEvent(MotionEvent ev) {
299         int action = ev.getActionMasked();
300         float x = ev.getX();
301         float y = ev.getY();
302         if (action == MotionEvent.ACTION_DOWN) {
303             mLastResizeBounds.setEmpty();
304             mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y);
305             if (mAllowGesture) {
306                 setCtrlType((int) x, (int) y);
307                 mDownPoint.set(x, y);
308                 mLastDownBounds.set(mMotionHelper.getBounds());
309             }
310 
311         } else if (mAllowGesture) {
312             switch (action) {
313                 case MotionEvent.ACTION_POINTER_DOWN:
314                     // We do not support multi touch for resizing via drag
315                     mAllowGesture = false;
316                     break;
317                 case MotionEvent.ACTION_MOVE:
318                     // Capture inputs
319                     if (!mThresholdCrossed
320                             && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) {
321                         mThresholdCrossed = true;
322                         // Reset the down to begin resizing from this point
323                         mDownPoint.set(x, y);
324                         mInputMonitor.pilferPointers();
325                     }
326                     if (mThresholdCrossed) {
327                         if (mMenuController.isMenuActivityVisible()) {
328                             mMenuController.hideMenuWithoutResize();
329                             mMenuController.hideMenu();
330                         }
331                         final Rect currentPipBounds = mMotionHelper.getBounds();
332                         mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y,
333                                 mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
334                                 mMinSize.y, mMaxSize, true,
335                                 mLastDownBounds.width() > mLastDownBounds.height()));
336                         mPipBoundsHandler.transformBoundsToAspectRatio(mLastResizeBounds);
337                         mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds,
338                                 null);
339                     }
340                     break;
341                 case MotionEvent.ACTION_UP:
342                 case MotionEvent.ACTION_CANCEL:
343                     if (!mLastResizeBounds.isEmpty()) {
344                         mUserResizeBounds.set(mLastResizeBounds);
345                         mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
346                                 (Rect bounds) -> {
347                                     new Handler(Looper.getMainLooper()).post(() -> {
348                                         mMotionHelper.synchronizePinnedStackBounds();
349                                         mUpdateMovementBoundsRunnable.run();
350                                         resetState();
351                                     });
352                                 });
353                         mPipUiEventLogger.log(
354                                 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE);
355                     } else {
356                         resetState();
357                     }
358                     break;
359             }
360         }
361     }
362 
resetState()363     private void resetState() {
364         mCtrlType = CTRL_NONE;
365         mAllowGesture = false;
366         mThresholdCrossed = false;
367     }
368 
setUserResizeBounds(Rect bounds)369     void setUserResizeBounds(Rect bounds) {
370         mUserResizeBounds.set(bounds);
371     }
372 
invalidateUserResizeBounds()373     void invalidateUserResizeBounds() {
374         mUserResizeBounds.setEmpty();
375     }
376 
getUserResizeBounds()377     Rect getUserResizeBounds() {
378         return mUserResizeBounds;
379     }
380 
updateMaxSize(int maxX, int maxY)381     void updateMaxSize(int maxX, int maxY) {
382         mMaxSize.set(maxX, maxY);
383     }
384 
updateMinSize(int minX, int minY)385     void updateMinSize(int minX, int minY) {
386         mMinSize.set(minX, minY);
387     }
388 
dump(PrintWriter pw, String prefix)389     public void dump(PrintWriter pw, String prefix) {
390         final String innerPrefix = prefix + "  ";
391         pw.println(prefix + TAG);
392         pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture);
393         pw.println(innerPrefix + "mIsAttached=" + mIsAttached);
394         pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled);
395         pw.println(innerPrefix + "mEnableUserResize=" + mEnableUserResize);
396         pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed);
397     }
398 
399     class SysUiInputEventReceiver extends BatchedInputEventReceiver {
SysUiInputEventReceiver(InputChannel channel, Looper looper)400         SysUiInputEventReceiver(InputChannel channel, Looper looper) {
401             super(channel, looper, Choreographer.getSfInstance());
402         }
403 
onInputEvent(InputEvent event)404         public void onInputEvent(InputEvent event) {
405             PipResizeGestureHandler.this.onInputEvent(event);
406             finishInputEvent(event, true);
407         }
408     }
409 }
410