1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.pip.phone;
18 
19 import android.graphics.PointF;
20 import android.view.Display;
21 import android.view.MotionEvent;
22 import android.view.VelocityTracker;
23 import android.view.ViewConfiguration;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.internal.protolog.common.ProtoLog;
27 import com.android.wm.shell.common.ShellExecutor;
28 import com.android.wm.shell.protolog.ShellProtoLogGroup;
29 
30 import java.io.PrintWriter;
31 
32 /**
33  * This keeps track of the touch state throughout the current touch gesture.
34  */
35 public class PipTouchState {
36     private static final String TAG = "PipTouchState";
37     private static final boolean DEBUG = false;
38 
39     @VisibleForTesting
40     public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
41     static final long HOVER_EXIT_TIMEOUT = 50;
42 
43     private final ShellExecutor mMainExecutor;
44     private final ViewConfiguration mViewConfig;
45     private final Runnable mDoubleTapTimeoutCallback;
46     private final Runnable mHoverExitTimeoutCallback;
47 
48     private VelocityTracker mVelocityTracker;
49     private long mDownTouchTime = 0;
50     private long mLastDownTouchTime = 0;
51     private long mUpTouchTime = 0;
52     private final PointF mDownTouch = new PointF();
53     private final PointF mDownDelta = new PointF();
54     private final PointF mLastTouch = new PointF();
55     private final PointF mLastDelta = new PointF();
56     private final PointF mVelocity = new PointF();
57     private boolean mAllowTouches = true;
58 
59     // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing
60     private boolean mAllowInputEvents = true;
61     private boolean mIsUserInteracting = false;
62     // Set to true only if the multiple taps occur within the double tap timeout
63     private boolean mIsDoubleTap = false;
64     // Set to true only if a gesture
65     private boolean mIsWaitingForDoubleTap = false;
66     private boolean mIsDragging = false;
67     // The previous gesture was a drag
68     private boolean mPreviouslyDragging = false;
69     private boolean mStartedDragging = false;
70     private boolean mAllowDraggingOffscreen = false;
71     private int mActivePointerId;
72     private int mLastTouchDisplayId = Display.INVALID_DISPLAY;
73 
PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback, Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor)74     public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback,
75             Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) {
76         mViewConfig = viewConfig;
77         mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
78         mHoverExitTimeoutCallback = hoverExitTimeoutCallback;
79         mMainExecutor = mainExecutor;
80     }
81 
82     /**
83      * @return true if input processing is enabled for PiP in general.
84      */
getAllowInputEvents()85     public boolean getAllowInputEvents() {
86         return mAllowInputEvents;
87     }
88 
89     /**
90      * @param allowInputEvents true to enable input processing for PiP in general.
91      */
setAllowInputEvents(boolean allowInputEvents)92     public void setAllowInputEvents(boolean allowInputEvents) {
93         mAllowInputEvents = allowInputEvents;
94     }
95 
96     /**
97      * Resets this state.
98      */
reset()99     public void reset() {
100         mAllowDraggingOffscreen = false;
101         mIsDragging = false;
102         mStartedDragging = false;
103         mIsUserInteracting = false;
104         mLastTouchDisplayId = Display.INVALID_DISPLAY;
105     }
106 
107     /**
108      * Processes a given touch event and updates the state.
109      */
onTouchEvent(MotionEvent ev)110     public void onTouchEvent(MotionEvent ev) {
111         mLastTouchDisplayId = ev.getDisplayId();
112         switch (ev.getActionMasked()) {
113             case MotionEvent.ACTION_DOWN: {
114                 if (!mAllowTouches) {
115                     return;
116                 }
117 
118                 // Initialize the velocity tracker
119                 initOrResetVelocityTracker();
120                 addMovementToVelocityTracker(ev);
121 
122                 mActivePointerId = ev.getPointerId(0);
123                 if (DEBUG) {
124                     ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
125                             "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId);
126                 }
127                 mLastTouch.set(ev.getRawX(), ev.getRawY());
128                 mDownTouch.set(mLastTouch);
129                 mAllowDraggingOffscreen = true;
130                 mIsUserInteracting = true;
131                 mDownTouchTime = ev.getEventTime();
132                 mIsDoubleTap = !mPreviouslyDragging
133                         && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
134                 mIsWaitingForDoubleTap = false;
135                 mIsDragging = false;
136                 mLastDownTouchTime = mDownTouchTime;
137                 if (mDoubleTapTimeoutCallback != null) {
138                     mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
139                 }
140                 break;
141             }
142             case MotionEvent.ACTION_MOVE: {
143                 // Skip event if we did not start processing this touch gesture
144                 if (!mIsUserInteracting) {
145                     break;
146                 }
147 
148                 // Update the velocity tracker
149                 addMovementToVelocityTracker(ev);
150                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
151                 if (pointerIndex == -1) {
152                     ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
153                             "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId);
154                     break;
155                 }
156 
157                 float x = ev.getRawX(pointerIndex);
158                 float y = ev.getRawY(pointerIndex);
159                 mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
160                 mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
161 
162                 boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
163                 if (!mIsDragging) {
164                     if (hasMovedBeyondTap) {
165                         mIsDragging = true;
166                         mStartedDragging = true;
167                     }
168                 } else {
169                     mStartedDragging = false;
170                 }
171                 mLastTouch.set(x, y);
172                 break;
173             }
174             case MotionEvent.ACTION_POINTER_UP: {
175                 // Skip event if we did not start processing this touch gesture
176                 if (!mIsUserInteracting) {
177                     break;
178                 }
179 
180                 // Update the velocity tracker
181                 addMovementToVelocityTracker(ev);
182 
183                 int pointerIndex = ev.getActionIndex();
184                 int pointerId = ev.getPointerId(pointerIndex);
185                 if (pointerId == mActivePointerId) {
186                     // Select a new active pointer id and reset the movement state
187                     final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
188                     mActivePointerId = ev.getPointerId(newPointerIndex);
189                     if (DEBUG) {
190                         ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
191                                 "%s: Relinquish active pointer id on POINTER_UP: %d",
192                                 TAG, mActivePointerId);
193                     }
194                     mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex));
195                 }
196                 break;
197             }
198             case MotionEvent.ACTION_UP: {
199                 // Skip event if we did not start processing this touch gesture
200                 if (!mIsUserInteracting) {
201                     break;
202                 }
203 
204                 // Update the velocity tracker
205                 addMovementToVelocityTracker(ev);
206                 mVelocityTracker.computeCurrentVelocity(1000,
207                         mViewConfig.getScaledMaximumFlingVelocity());
208                 mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
209 
210                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
211                 if (pointerIndex == -1) {
212                     ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
213                             "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId);
214                     break;
215                 }
216 
217                 mUpTouchTime = ev.getEventTime();
218                 mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex));
219                 mPreviouslyDragging = mIsDragging;
220                 mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging
221                         && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
222 
223                 // Fall through to clean up
224             }
225             case MotionEvent.ACTION_CANCEL: {
226                 recycleVelocityTracker();
227                 break;
228             }
229             case MotionEvent.ACTION_BUTTON_PRESS: {
230                 removeHoverExitTimeoutCallback();
231                 break;
232             }
233         }
234     }
235 
236     /**
237      * @return the velocity of the active touch pointer at the point it is lifted off the screen.
238      */
239     public PointF getVelocity() {
240         return mVelocity;
241     }
242 
243     /**
244      * @return the last touch position of the active pointer.
245      */
246     public PointF getLastTouchPosition() {
247         return mLastTouch;
248     }
249 
250     /**
251      * @return the movement delta between the last handled touch event and the previous touch
252      * position.
253      */
254     public PointF getLastTouchDelta() {
255         return mLastDelta;
256     }
257 
258     /**
259      * @return the down touch position.
260      */
261     public PointF getDownTouchPosition() {
262         return mDownTouch;
263     }
264 
265     /**
266      * @return the movement delta between the last handled touch event and the down touch
267      * position.
268      */
269     public PointF getDownTouchDelta() {
270         return mDownDelta;
271     }
272 
273     /**
274      * @return whether the user has started dragging.
275      */
276     public boolean isDragging() {
277         return mIsDragging;
278     }
279 
280     /**
281      * @return whether the user is currently interacting with the PiP.
282      */
283     public boolean isUserInteracting() {
284         return mIsUserInteracting;
285     }
286 
287     /**
288      * @return whether the user has started dragging just in the last handled touch event.
289      */
290     public boolean startedDragging() {
291         return mStartedDragging;
292     }
293 
294     /**
295      * @return Display ID of the last touch event.
296      */
297     public int getLastTouchDisplayId() {
298         return mLastTouchDisplayId;
299     }
300 
301     /**
302      * Sets whether touching is currently allowed.
303      */
304     public void setAllowTouches(boolean allowTouches) {
305         mAllowTouches = allowTouches;
306 
307         // If the user happens to touch down before this is sent from the system during a transition
308         // then block any additional handling by resetting the state now
309         if (mIsUserInteracting) {
310             reset();
311         }
312     }
313 
314     /**
315      * Disallows dragging offscreen for the duration of the current gesture.
316      */
317     public void setDisallowDraggingOffscreen() {
318         mAllowDraggingOffscreen = false;
319     }
320 
321     /**
322      * @return whether dragging offscreen is allowed during this gesture.
323      */
324     public boolean allowDraggingOffscreen() {
325         return mAllowDraggingOffscreen;
326     }
327 
328     /**
329      * @return whether this gesture is a double-tap.
330      */
331     public boolean isDoubleTap() {
332         return mIsDoubleTap;
333     }
334 
335     /**
336      * @return whether this gesture will potentially lead to a following double-tap.
337      */
338     public boolean isWaitingForDoubleTap() {
339         return mIsWaitingForDoubleTap;
340     }
341 
342     /**
343      * Schedules the callback to run if the next double tap does not occur.  Only runs if
344      * isWaitingForDoubleTap() is true.
345      */
346     public void scheduleDoubleTapTimeoutCallback() {
347         if (mIsWaitingForDoubleTap) {
348             long delay = getDoubleTapTimeoutCallbackDelay();
349             mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
350             mMainExecutor.executeDelayed(mDoubleTapTimeoutCallback, delay);
351         }
352     }
353 
354     @VisibleForTesting
355     public long getDoubleTapTimeoutCallbackDelay() {
356         if (mIsWaitingForDoubleTap) {
357             return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
358         }
359         return -1;
360     }
361 
362     /**
363      * Removes the timeout callback if it's in queue.
364      */
365     public void removeDoubleTapTimeoutCallback() {
366         mIsWaitingForDoubleTap = false;
367         mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
368     }
369 
370     @VisibleForTesting
371     public void scheduleHoverExitTimeoutCallback() {
372         mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
373         mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT);
374     }
375 
376     void removeHoverExitTimeoutCallback() {
377         mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
378     }
379 
380     void addMovementToVelocityTracker(MotionEvent event) {
381         if (mVelocityTracker == null) {
382             return;
383         }
384 
385         // Add movement to velocity tracker using raw screen X and Y coordinates instead
386         // of window coordinates because the window frame may be moving at the same time.
387         float deltaX = event.getRawX() - event.getX();
388         float deltaY = event.getRawY() - event.getY();
389         event.offsetLocation(deltaX, deltaY);
390         mVelocityTracker.addMovement(event);
391         event.offsetLocation(-deltaX, -deltaY);
392     }
393 
394     private void initOrResetVelocityTracker() {
395         if (mVelocityTracker == null) {
396             mVelocityTracker = VelocityTracker.obtain();
397         } else {
398             mVelocityTracker.clear();
399         }
400     }
401 
402     private void recycleVelocityTracker() {
403         if (mVelocityTracker != null) {
404             mVelocityTracker.recycle();
405             mVelocityTracker = null;
406         }
407     }
408 
409     public void dump(PrintWriter pw, String prefix) {
410         final String innerPrefix = prefix + "  ";
411         pw.println(prefix + TAG);
412         pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
413         pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents);
414         pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
415         pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId);
416         pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
417         pw.println(innerPrefix + "mDownDelta=" + mDownDelta);
418         pw.println(innerPrefix + "mLastTouch=" + mLastTouch);
419         pw.println(innerPrefix + "mLastDelta=" + mLastDelta);
420         pw.println(innerPrefix + "mVelocity=" + mVelocity);
421         pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting);
422         pw.println(innerPrefix + "mIsDragging=" + mIsDragging);
423         pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging);
424         pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen);
425     }
426 }
427