1 /*
2  * Copyright (C) 2016 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.systemui.pip.phone;
18 
19 import android.graphics.PointF;
20 import android.os.Handler;
21 import android.os.SystemClock;
22 import android.util.Log;
23 import android.view.MotionEvent;
24 import android.view.VelocityTracker;
25 import android.view.ViewConfiguration;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.io.PrintWriter;
30 
31 /**
32  * This keeps track of the touch state throughout the current touch gesture.
33  */
34 public class PipTouchState {
35     private static final String TAG = "PipTouchHandler";
36     private static final boolean DEBUG = false;
37 
38     @VisibleForTesting
39     static final long DOUBLE_TAP_TIMEOUT = 200;
40 
41     private final Handler mHandler;
42     private final ViewConfiguration mViewConfig;
43     private final Runnable mDoubleTapTimeoutCallback;
44 
45     private VelocityTracker mVelocityTracker;
46     private long mDownTouchTime = 0;
47     private long mLastDownTouchTime = 0;
48     private long mUpTouchTime = 0;
49     private final PointF mDownTouch = new PointF();
50     private final PointF mDownDelta = new PointF();
51     private final PointF mLastTouch = new PointF();
52     private final PointF mLastDelta = new PointF();
53     private final PointF mVelocity = new PointF();
54     private boolean mAllowTouches = true;
55     private boolean mIsUserInteracting = false;
56     // Set to true only if the multiple taps occur within the double tap timeout
57     private boolean mIsDoubleTap = false;
58     // Set to true only if a gesture
59     private boolean mIsWaitingForDoubleTap = false;
60     private boolean mIsDragging = false;
61     // The previous gesture was a drag
62     private boolean mPreviouslyDragging = false;
63     private boolean mStartedDragging = false;
64     private boolean mAllowDraggingOffscreen = false;
65     private int mActivePointerId;
66 
PipTouchState(ViewConfiguration viewConfig, Handler handler, Runnable doubleTapTimeoutCallback)67     public PipTouchState(ViewConfiguration viewConfig, Handler handler,
68             Runnable doubleTapTimeoutCallback) {
69         mViewConfig = viewConfig;
70         mHandler = handler;
71         mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
72     }
73 
74     /**
75      * Resets this state.
76      */
reset()77     public void reset() {
78         mAllowDraggingOffscreen = false;
79         mIsDragging = false;
80         mStartedDragging = false;
81         mIsUserInteracting = false;
82     }
83 
84     /**
85      * Processes a given touch event and updates the state.
86      */
onTouchEvent(MotionEvent ev)87     public void onTouchEvent(MotionEvent ev) {
88         switch (ev.getAction()) {
89             case MotionEvent.ACTION_DOWN: {
90                 if (!mAllowTouches) {
91                     return;
92                 }
93 
94                 // Initialize the velocity tracker
95                 initOrResetVelocityTracker();
96 
97                 mActivePointerId = ev.getPointerId(0);
98                 if (DEBUG) {
99                     Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId);
100                 }
101                 mLastTouch.set(ev.getX(), ev.getY());
102                 mDownTouch.set(mLastTouch);
103                 mAllowDraggingOffscreen = true;
104                 mIsUserInteracting = true;
105                 mDownTouchTime = ev.getEventTime();
106                 mIsDoubleTap = !mPreviouslyDragging &&
107                         (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
108                 mIsWaitingForDoubleTap = false;
109                 mLastDownTouchTime = mDownTouchTime;
110                 if (mDoubleTapTimeoutCallback != null) {
111                     mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
112                 }
113                 break;
114             }
115             case MotionEvent.ACTION_MOVE: {
116                 // Skip event if we did not start processing this touch gesture
117                 if (!mIsUserInteracting) {
118                     break;
119                 }
120 
121                 // Update the velocity tracker
122                 mVelocityTracker.addMovement(ev);
123                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
124                 if (pointerIndex == -1) {
125                     Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId);
126                     break;
127                 }
128 
129                 float x = ev.getX(pointerIndex);
130                 float y = ev.getY(pointerIndex);
131                 mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
132                 mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
133 
134                 boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
135                 if (!mIsDragging) {
136                     if (hasMovedBeyondTap) {
137                         mIsDragging = true;
138                         mStartedDragging = true;
139                     }
140                 } else {
141                     mStartedDragging = false;
142                 }
143                 mLastTouch.set(x, y);
144                 break;
145             }
146             case MotionEvent.ACTION_POINTER_UP: {
147                 // Skip event if we did not start processing this touch gesture
148                 if (!mIsUserInteracting) {
149                     break;
150                 }
151 
152                 // Update the velocity tracker
153                 mVelocityTracker.addMovement(ev);
154 
155                 int pointerIndex = ev.getActionIndex();
156                 int pointerId = ev.getPointerId(pointerIndex);
157                 if (pointerId == mActivePointerId) {
158                     // Select a new active pointer id and reset the movement state
159                     final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
160                     mActivePointerId = ev.getPointerId(newPointerIndex);
161                     if (DEBUG) {
162                         Log.e(TAG, "Relinquish active pointer id on POINTER_UP: " +
163                                 mActivePointerId);
164                     }
165                     mLastTouch.set(ev.getX(newPointerIndex), ev.getY(newPointerIndex));
166                 }
167                 break;
168             }
169             case MotionEvent.ACTION_UP: {
170                 // Skip event if we did not start processing this touch gesture
171                 if (!mIsUserInteracting) {
172                     break;
173                 }
174 
175                 // Update the velocity tracker
176                 mVelocityTracker.addMovement(ev);
177                 mVelocityTracker.computeCurrentVelocity(1000,
178                         mViewConfig.getScaledMaximumFlingVelocity());
179                 mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
180 
181                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
182                 if (pointerIndex == -1) {
183                     Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId);
184                     break;
185                 }
186 
187                 mUpTouchTime = ev.getEventTime();
188                 mLastTouch.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
189                 mPreviouslyDragging = mIsDragging;
190                 mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging &&
191                         (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
192 
193                 // Fall through to clean up
194             }
195             case MotionEvent.ACTION_CANCEL: {
196                 recycleVelocityTracker();
197                 break;
198             }
199         }
200     }
201 
202     /**
203      * @return the velocity of the active touch pointer at the point it is lifted off the screen.
204      */
205     public PointF getVelocity() {
206         return mVelocity;
207     }
208 
209     /**
210      * @return the last touch position of the active pointer.
211      */
212     public PointF getLastTouchPosition() {
213         return mLastTouch;
214     }
215 
216     /**
217      * @return the movement delta between the last handled touch event and the previous touch
218      *         position.
219      */
220     public PointF getLastTouchDelta() {
221         return mLastDelta;
222     }
223 
224     /**
225      * @return the down touch position.
226      */
227     public PointF getDownTouchPosition() {
228         return mDownTouch;
229     }
230 
231     /**
232      * @return the movement delta between the last handled touch event and the down touch
233      *         position.
234      */
235     public PointF getDownTouchDelta() {
236         return mDownDelta;
237     }
238 
239     /**
240      * @return whether the user has started dragging.
241      */
242     public boolean isDragging() {
243         return mIsDragging;
244     }
245 
246     /**
247      * @return whether the user is currently interacting with the PiP.
248      */
249     public boolean isUserInteracting() {
250         return mIsUserInteracting;
251     }
252 
253     /**
254      * @return whether the user has started dragging just in the last handled touch event.
255      */
256     public boolean startedDragging() {
257         return mStartedDragging;
258     }
259 
260     /**
261      * Sets whether touching is currently allowed.
262      */
263     public void setAllowTouches(boolean allowTouches) {
264         mAllowTouches = allowTouches;
265 
266         // If the user happens to touch down before this is sent from the system during a transition
267         // then block any additional handling by resetting the state now
268         if (mIsUserInteracting) {
269             reset();
270         }
271     }
272 
273     /**
274      * Disallows dragging offscreen for the duration of the current gesture.
275      */
276     public void setDisallowDraggingOffscreen() {
277         mAllowDraggingOffscreen = false;
278     }
279 
280     /**
281      * @return whether dragging offscreen is allowed during this gesture.
282      */
283     public boolean allowDraggingOffscreen() {
284         return mAllowDraggingOffscreen;
285     }
286 
287     /**
288      * @return whether this gesture is a double-tap.
289      */
290     public boolean isDoubleTap() {
291         return mIsDoubleTap;
292     }
293 
294     /**
295      * @return whether this gesture will potentially lead to a following double-tap.
296      */
297     public boolean isWaitingForDoubleTap() {
298         return mIsWaitingForDoubleTap;
299     }
300 
301     /**
302      * Schedules the callback to run if the next double tap does not occur.  Only runs if
303      * isWaitingForDoubleTap() is true.
304      */
305     public void scheduleDoubleTapTimeoutCallback() {
306         if (mIsWaitingForDoubleTap) {
307             long delay = getDoubleTapTimeoutCallbackDelay();
308             mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
309             mHandler.postDelayed(mDoubleTapTimeoutCallback, delay);
310         }
311     }
312 
313     @VisibleForTesting long getDoubleTapTimeoutCallbackDelay() {
314         if (mIsWaitingForDoubleTap) {
315             return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
316         }
317         return -1;
318     }
319 
320     private void initOrResetVelocityTracker() {
321         if (mVelocityTracker == null) {
322             mVelocityTracker = VelocityTracker.obtain();
323         } else {
324             mVelocityTracker.clear();
325         }
326     }
327 
328     private void recycleVelocityTracker() {
329         if (mVelocityTracker != null) {
330             mVelocityTracker.recycle();
331             mVelocityTracker = null;
332         }
333     }
334 
335     public void dump(PrintWriter pw, String prefix) {
336         final String innerPrefix = prefix + "  ";
337         pw.println(prefix + TAG);
338         pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
339         pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
340         pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
341         pw.println(innerPrefix + "mDownDelta=" + mDownDelta);
342         pw.println(innerPrefix + "mLastTouch=" + mLastTouch);
343         pw.println(innerPrefix + "mLastDelta=" + mLastDelta);
344         pw.println(innerPrefix + "mVelocity=" + mVelocity);
345         pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting);
346         pw.println(innerPrefix + "mIsDragging=" + mIsDragging);
347         pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging);
348         pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen);
349     }
350 }
351