1 /*
2  * Copyright (C) 2018 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.server.wm;
18 
19 import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
20 import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
21 import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
22 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
23 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
24 import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
25 
26 import android.annotation.NonNull;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Rect;
30 import android.graphics.Region;
31 import android.hardware.display.DisplayManagerGlobal;
32 import android.os.Handler;
33 import android.os.SystemClock;
34 import android.util.Slog;
35 import android.view.Display;
36 import android.view.DisplayCutout;
37 import android.view.DisplayInfo;
38 import android.view.GestureDetector;
39 import android.view.InputDevice;
40 import android.view.MotionEvent;
41 import android.view.WindowManagerPolicyConstants.PointerEventListener;
42 import android.widget.OverScroller;
43 
44 import java.io.PrintWriter;
45 
46 /**
47  * Listens for system-wide input gestures, firing callbacks when detected.
48  * @hide
49  */
50 class SystemGesturesPointerEventListener implements PointerEventListener {
51     private static final String TAG = "SystemGestures";
52     private static final boolean DEBUG = false;
53     private static final long SWIPE_TIMEOUT_MS = 500;
54     private static final int MAX_TRACKED_POINTERS = 32;  // max per input system
55     private static final int UNTRACKED_POINTER = -1;
56     private static final int MAX_FLING_TIME_MILLIS = 5000;
57 
58     private static final int SWIPE_NONE = 0;
59     private static final int SWIPE_FROM_TOP = 1;
60     private static final int SWIPE_FROM_BOTTOM = 2;
61     private static final int SWIPE_FROM_RIGHT = 3;
62     private static final int SWIPE_FROM_LEFT = 4;
63 
64     private static final int TRACKPAD_SWIPE_NONE = 0;
65     private static final int TRACKPAD_SWIPE_FROM_TOP = 1;
66     private static final int TRACKPAD_SWIPE_FROM_BOTTOM = 2;
67     private static final int TRACKPAD_SWIPE_FROM_RIGHT = 3;
68     private static final int TRACKPAD_SWIPE_FROM_LEFT = 4;
69 
70     private final Context mContext;
71     private final Handler mHandler;
72     private int mDisplayCutoutTouchableRegionSize;
73     // The thresholds for each edge of the display
74     private final Rect mSwipeStartThreshold = new Rect();
75     private int mSwipeDistanceThreshold;
76     private final Callbacks mCallbacks;
77     private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];
78     private final float[] mDownX = new float[MAX_TRACKED_POINTERS];
79     private final float[] mDownY = new float[MAX_TRACKED_POINTERS];
80     private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];
81 
82     private GestureDetector mGestureDetector;
83 
84     int screenHeight;
85     int screenWidth;
86     private int mDownPointers;
87     private boolean mSwipeFireable;
88     private boolean mDebugFireable;
89     private boolean mMouseHoveringAtLeft;
90     private boolean mMouseHoveringAtTop;
91     private boolean mMouseHoveringAtRight;
92     private boolean mMouseHoveringAtBottom;
93     private long mLastFlingTime;
94 
SystemGesturesPointerEventListener(Context context, Handler handler, Callbacks callbacks)95     SystemGesturesPointerEventListener(Context context, Handler handler, Callbacks callbacks) {
96         mContext = checkNull("context", context);
97         mHandler = handler;
98         mCallbacks = checkNull("callbacks", callbacks);
99         onConfigurationChanged();
100     }
101 
onDisplayInfoChanged(DisplayInfo info)102     void onDisplayInfoChanged(DisplayInfo info) {
103         screenWidth = info.logicalWidth;
104         screenHeight = info.logicalHeight;
105         onConfigurationChanged();
106     }
107 
onConfigurationChanged()108     void onConfigurationChanged() {
109         final Resources r = mContext.getResources();
110         final int defaultThreshold = r.getDimensionPixelSize(
111                 com.android.internal.R.dimen.system_gestures_start_threshold);
112         mSwipeStartThreshold.set(defaultThreshold, defaultThreshold, defaultThreshold,
113                 defaultThreshold);
114         mSwipeDistanceThreshold = defaultThreshold;
115 
116         final Display display = DisplayManagerGlobal.getInstance()
117                 .getRealDisplay(Display.DEFAULT_DISPLAY);
118         final DisplayCutout displayCutout = display.getCutout();
119         if (displayCutout != null) {
120             // Expand swipe start threshold such that we can catch touches that just start beyond
121             // the notch area
122             mDisplayCutoutTouchableRegionSize = r.getDimensionPixelSize(
123                     com.android.internal.R.dimen.display_cutout_touchable_region_size);
124             final Rect[] bounds = displayCutout.getBoundingRectsAll();
125             if (bounds[BOUNDS_POSITION_LEFT] != null) {
126                 mSwipeStartThreshold.left = Math.max(mSwipeStartThreshold.left,
127                         bounds[BOUNDS_POSITION_LEFT].width() + mDisplayCutoutTouchableRegionSize);
128             }
129             if (bounds[BOUNDS_POSITION_TOP] != null) {
130                 mSwipeStartThreshold.top = Math.max(mSwipeStartThreshold.top,
131                         bounds[BOUNDS_POSITION_TOP].height() + mDisplayCutoutTouchableRegionSize);
132             }
133             if (bounds[BOUNDS_POSITION_RIGHT] != null) {
134                 mSwipeStartThreshold.right = Math.max(mSwipeStartThreshold.right,
135                         bounds[BOUNDS_POSITION_RIGHT].width() + mDisplayCutoutTouchableRegionSize);
136             }
137             if (bounds[BOUNDS_POSITION_BOTTOM] != null) {
138                 mSwipeStartThreshold.bottom = Math.max(mSwipeStartThreshold.bottom,
139                         bounds[BOUNDS_POSITION_BOTTOM].height()
140                                 + mDisplayCutoutTouchableRegionSize);
141             }
142         }
143         if (DEBUG) Slog.d(TAG,  "mSwipeStartThreshold=" + mSwipeStartThreshold
144                 + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
145     }
146 
checkNull(String name, T arg)147     private static <T> T checkNull(String name, T arg) {
148         if (arg == null) {
149             throw new IllegalArgumentException(name + " must not be null");
150         }
151         return arg;
152     }
153 
systemReady()154     public void systemReady() {
155         // GestureDetector records statistics about gesture classification events to inform gesture
156         // usage trends. SystemGesturesPointerEventListener creates a lot of noise in these
157         // statistics because it passes every touch event though a GestureDetector. By creating an
158         // anonymous subclass of GestureDetector, these statistics will be recorded with a unique
159         // source name that can be filtered.
160 
161         // GestureDetector would get a ViewConfiguration instance by context, that may also
162         // create a new WindowManagerImpl for the new display, and lock WindowManagerGlobal
163         // temporarily in the constructor that would make a deadlock.
164         mHandler.post(() -> {
165             final int displayId = mContext.getDisplayId();
166             final DisplayInfo info = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
167             if (info == null) {
168                 // Display already removed, stop here.
169                 Slog.w(TAG, "Cannot create GestureDetector, display removed:" + displayId);
170                 return;
171             }
172             mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), mHandler) {
173             };
174         });
175     }
176 
177     @Override
onPointerEvent(MotionEvent event)178     public void onPointerEvent(MotionEvent event) {
179         if (mGestureDetector != null && event.isTouchEvent()) {
180             mGestureDetector.onTouchEvent(event);
181         }
182         switch (event.getActionMasked()) {
183             case MotionEvent.ACTION_DOWN:
184                 mSwipeFireable = true;
185                 mDebugFireable = true;
186                 mDownPointers = 0;
187                 captureDown(event, 0);
188                 if (mMouseHoveringAtLeft) {
189                     mMouseHoveringAtLeft = false;
190                     mCallbacks.onMouseLeaveFromLeft();
191                 }
192                 if (mMouseHoveringAtTop) {
193                     mMouseHoveringAtTop = false;
194                     mCallbacks.onMouseLeaveFromTop();
195                 }
196                 if (mMouseHoveringAtRight) {
197                     mMouseHoveringAtRight = false;
198                     mCallbacks.onMouseLeaveFromRight();
199                 }
200                 if (mMouseHoveringAtBottom) {
201                     mMouseHoveringAtBottom = false;
202                     mCallbacks.onMouseLeaveFromBottom();
203                 }
204                 mCallbacks.onDown();
205                 break;
206             case MotionEvent.ACTION_POINTER_DOWN:
207                 captureDown(event, event.getActionIndex());
208                 if (mDebugFireable) {
209                     mDebugFireable = event.getPointerCount() < 5;
210                     if (!mDebugFireable) {
211                         if (DEBUG) Slog.d(TAG, "Firing debug");
212                         mCallbacks.onDebug();
213                     }
214                 }
215                 break;
216             case MotionEvent.ACTION_MOVE:
217                 if (mSwipeFireable) {
218                     int trackpadSwipe = detectTrackpadThreeFingerSwipe(event);
219                     mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE;
220                     if (!mSwipeFireable) {
221                         if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) {
222                             if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop from trackpad");
223                             mCallbacks.onSwipeFromTop();
224                         } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) {
225                             if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom from trackpad");
226                             mCallbacks.onSwipeFromBottom();
227                         } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) {
228                             if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight from trackpad");
229                             mCallbacks.onSwipeFromRight();
230                         } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) {
231                             if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft from trackpad");
232                             mCallbacks.onSwipeFromLeft();
233                         }
234                         break;
235                     }
236 
237                     final int swipe = detectSwipe(event);
238                     mSwipeFireable = swipe == SWIPE_NONE;
239                     if (swipe == SWIPE_FROM_TOP) {
240                         if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop");
241                         mCallbacks.onSwipeFromTop();
242                     } else if (swipe == SWIPE_FROM_BOTTOM) {
243                         if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom");
244                         mCallbacks.onSwipeFromBottom();
245                     } else if (swipe == SWIPE_FROM_RIGHT) {
246                         if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight");
247                         mCallbacks.onSwipeFromRight();
248                     } else if (swipe == SWIPE_FROM_LEFT) {
249                         if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft");
250                         mCallbacks.onSwipeFromLeft();
251                     }
252                 }
253                 break;
254             case MotionEvent.ACTION_HOVER_MOVE:
255                 if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
256                     final float eventX = event.getX();
257                     final float eventY = event.getY();
258                     if (!mMouseHoveringAtLeft && eventX == 0) {
259                         mCallbacks.onMouseHoverAtLeft();
260                         mMouseHoveringAtLeft = true;
261                     } else if (mMouseHoveringAtLeft && eventX > 0) {
262                         mCallbacks.onMouseLeaveFromLeft();
263                         mMouseHoveringAtLeft = false;
264                     }
265                     if (!mMouseHoveringAtTop && eventY == 0) {
266                         mCallbacks.onMouseHoverAtTop();
267                         mMouseHoveringAtTop = true;
268                     } else if (mMouseHoveringAtTop && eventY > 0) {
269                         mCallbacks.onMouseLeaveFromTop();
270                         mMouseHoveringAtTop = false;
271                     }
272                     if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) {
273                         mCallbacks.onMouseHoverAtRight();
274                         mMouseHoveringAtRight = true;
275                     } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) {
276                         mCallbacks.onMouseLeaveFromRight();
277                         mMouseHoveringAtRight = false;
278                     }
279                     if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) {
280                         mCallbacks.onMouseHoverAtBottom();
281                         mMouseHoveringAtBottom = true;
282                     } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) {
283                         mCallbacks.onMouseLeaveFromBottom();
284                         mMouseHoveringAtBottom = false;
285                     }
286                 }
287                 break;
288             case MotionEvent.ACTION_UP:
289             case MotionEvent.ACTION_CANCEL:
290                 mSwipeFireable = false;
291                 mDebugFireable = false;
292                 mCallbacks.onUpOrCancel();
293                 break;
294             default:
295                 if (DEBUG) Slog.d(TAG, "Ignoring " + event);
296         }
297     }
298 
captureDown(MotionEvent event, int pointerIndex)299     private void captureDown(MotionEvent event, int pointerIndex) {
300         final int pointerId = event.getPointerId(pointerIndex);
301         final int i = findIndex(pointerId);
302         if (DEBUG) Slog.d(TAG, "pointer " + pointerId
303                 + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
304         if (i != UNTRACKED_POINTER) {
305             mDownX[i] = event.getX(pointerIndex);
306             mDownY[i] = event.getY(pointerIndex);
307             mDownTime[i] = event.getEventTime();
308             if (DEBUG) Slog.d(TAG, "pointer " + pointerId
309                     + " down x=" + mDownX[i] + " y=" + mDownY[i]);
310         }
311     }
312 
currentGestureStartedInRegion(Region r)313     protected boolean currentGestureStartedInRegion(Region r) {
314         return r.contains((int) mDownX[0], (int) mDownY[0]);
315     }
316 
findIndex(int pointerId)317     private int findIndex(int pointerId) {
318         for (int i = 0; i < mDownPointers; i++) {
319             if (mDownPointerId[i] == pointerId) {
320                 return i;
321             }
322         }
323         if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
324             return UNTRACKED_POINTER;
325         }
326         mDownPointerId[mDownPointers++] = pointerId;
327         return mDownPointers - 1;
328     }
329 
detectTrackpadThreeFingerSwipe(MotionEvent move)330     private int detectTrackpadThreeFingerSwipe(MotionEvent move) {
331         if (!isTrackpadThreeFingerSwipe(move)) {
332             return TRACKPAD_SWIPE_NONE;
333         }
334 
335         float dx = move.getX() - mDownX[0];
336         float dy = move.getY() - mDownY[0];
337         if (Math.abs(dx) < Math.abs(dy)) {
338             if (Math.abs(dy) > mSwipeDistanceThreshold) {
339                 return dy > 0 ? TRACKPAD_SWIPE_FROM_TOP : TRACKPAD_SWIPE_FROM_BOTTOM;
340             }
341         } else {
342             if (Math.abs(dx) > mSwipeDistanceThreshold) {
343                 return dx > 0 ? TRACKPAD_SWIPE_FROM_LEFT : TRACKPAD_SWIPE_FROM_RIGHT;
344             }
345         }
346 
347         return TRACKPAD_SWIPE_NONE;
348     }
349 
isTrackpadThreeFingerSwipe(MotionEvent event)350     private static boolean isTrackpadThreeFingerSwipe(MotionEvent event) {
351         return event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE
352                 && event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3;
353     }
354 
detectSwipe(MotionEvent move)355     private int detectSwipe(MotionEvent move) {
356         final int historySize = move.getHistorySize();
357         final int pointerCount = move.getPointerCount();
358         for (int p = 0; p < pointerCount; p++) {
359             final int pointerId = move.getPointerId(p);
360             final int i = findIndex(pointerId);
361             if (i != UNTRACKED_POINTER) {
362                 for (int h = 0; h < historySize; h++) {
363                     final long time = move.getHistoricalEventTime(h);
364                     final float x = move.getHistoricalX(p, h);
365                     final float y = move.getHistoricalY(p,  h);
366                     final int swipe = detectSwipe(i, time, x, y);
367                     if (swipe != SWIPE_NONE) {
368                         return swipe;
369                     }
370                 }
371                 final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
372                 if (swipe != SWIPE_NONE) {
373                     return swipe;
374                 }
375             }
376         }
377         return SWIPE_NONE;
378     }
379 
detectSwipe(int i, long time, float x, float y)380     private int detectSwipe(int i, long time, float x, float y) {
381         final float fromX = mDownX[i];
382         final float fromY = mDownY[i];
383         final long elapsed = time - mDownTime[i];
384         if (DEBUG) Slog.d(TAG, "pointer " + mDownPointerId[i]
385                 + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
386         if (fromY <= mSwipeStartThreshold.top
387                 && y > fromY + mSwipeDistanceThreshold
388                 && elapsed < SWIPE_TIMEOUT_MS) {
389             return SWIPE_FROM_TOP;
390         }
391         if (fromY >= screenHeight - mSwipeStartThreshold.bottom
392                 && y < fromY - mSwipeDistanceThreshold
393                 && elapsed < SWIPE_TIMEOUT_MS) {
394             return SWIPE_FROM_BOTTOM;
395         }
396         if (fromX >= screenWidth - mSwipeStartThreshold.right
397                 && x < fromX - mSwipeDistanceThreshold
398                 && elapsed < SWIPE_TIMEOUT_MS) {
399             return SWIPE_FROM_RIGHT;
400         }
401         if (fromX <= mSwipeStartThreshold.left
402                 && x > fromX + mSwipeDistanceThreshold
403                 && elapsed < SWIPE_TIMEOUT_MS) {
404             return SWIPE_FROM_LEFT;
405         }
406         return SWIPE_NONE;
407     }
408 
dump(@onNull PrintWriter pw, @NonNull String prefix)409     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
410         final String inner = prefix  + "  ";
411         pw.println(prefix + TAG + ":");
412         pw.print(inner); pw.print("mDisplayCutoutTouchableRegionSize=");
413         pw.println(mDisplayCutoutTouchableRegionSize);
414         pw.print(inner); pw.print("mSwipeStartThreshold="); pw.println(mSwipeStartThreshold);
415         pw.print(inner); pw.print("mSwipeDistanceThreshold="); pw.println(mSwipeDistanceThreshold);
416     }
417 
418     private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
419 
420         private OverScroller mOverscroller;
421 
FlingGestureDetector()422         FlingGestureDetector() {
423             mOverscroller = new OverScroller(mContext);
424         }
425 
426         @Override
onSingleTapUp(MotionEvent e)427         public boolean onSingleTapUp(MotionEvent e) {
428             if (!mOverscroller.isFinished()) {
429                 mOverscroller.forceFinished(true);
430             }
431             return true;
432         }
433         @Override
onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY)434         public boolean onFling(MotionEvent down, MotionEvent up,
435                 float velocityX, float velocityY) {
436             mOverscroller.computeScrollOffset();
437             long now = SystemClock.uptimeMillis();
438 
439             if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
440                 mOverscroller.forceFinished(true);
441             }
442             mOverscroller.fling(0, 0, (int)velocityX, (int)velocityY,
443                     Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
444             int duration = mOverscroller.getDuration();
445             if (duration > MAX_FLING_TIME_MILLIS) {
446                 duration = MAX_FLING_TIME_MILLIS;
447             }
448             mLastFlingTime = now;
449             mCallbacks.onFling(duration);
450             return true;
451         }
452     }
453 
454     interface Callbacks {
455         void onSwipeFromTop();
456         void onSwipeFromBottom();
457         void onSwipeFromRight();
458         void onSwipeFromLeft();
459         void onFling(int durationMs);
460         void onDown();
461         void onUpOrCancel();
462         void onMouseHoverAtLeft();
463         void onMouseHoverAtTop();
464         void onMouseHoverAtRight();
465         void onMouseHoverAtBottom();
466         void onMouseLeaveFromLeft();
467         void onMouseLeaveFromTop();
468         void onMouseLeaveFromRight();
469         void onMouseLeaveFromBottom();
470         void onDebug();
471     }
472 }
473