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