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