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