2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.server.policy;
19 import android.content.Context;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.SystemClock;
23 import android.util.Slog;
24 import android.view.GestureDetector;
25 import android.view.InputDevice;
26 import android.view.MotionEvent;
27 import android.view.WindowManagerPolicy.PointerEventListener;
28 import android.widget.OverScroller;
31 * Listens for system-wide input gestures, firing callbacks when detected.
34 public class SystemGesturesPointerEventListener implements PointerEventListener {
35 private static final String TAG = "SystemGestures";
36 private static final boolean DEBUG = false;
37 private static final long SWIPE_TIMEOUT_MS = 500;
38 private static final int MAX_TRACKED_POINTERS = 32; // max per input system
39 private static final int UNTRACKED_POINTER = -1;
40 private static final int MAX_FLING_TIME_MILLIS = 5000;
42 private static final int SWIPE_NONE = 0;
43 private static final int SWIPE_FROM_TOP = 1;
44 private static final int SWIPE_FROM_BOTTOM = 2;
45 private static final int SWIPE_FROM_RIGHT = 3;
46 private static final int SWIPE_FROM_LEFT = 4;
48 private final Context mContext;
49 private final int mSwipeStartThreshold;
50 private final int mSwipeDistanceThreshold;
51 private final Callbacks mCallbacks;
52 private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];
53 private final float[] mDownX = new float[MAX_TRACKED_POINTERS];
54 private final float[] mDownY = new float[MAX_TRACKED_POINTERS];
55 private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];
57 private GestureDetector mGestureDetector;
58 private OverScroller mOverscroller;
62 private int mDownPointers;
63 private boolean mSwipeFireable;
64 private boolean mDebugFireable;
65 private boolean mMouseHoveringAtEdge;
66 private long mLastFlingTime;
68 public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
70 mCallbacks = checkNull("callbacks", callbacks);
71 mSwipeStartThreshold = checkNull("context", context).getResources()
72 .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
73 mSwipeDistanceThreshold = mSwipeStartThreshold;
74 if (DEBUG) Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold
75 + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
78 private static <T> T checkNull(String name, T arg) {
80 throw new IllegalArgumentException(name + " must not be null");
85 public void systemReady() {
86 Handler h = new Handler(Looper.myLooper());
87 mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
88 mOverscroller = new OverScroller(mContext);
92 public void onPointerEvent(MotionEvent event) {
93 if (mGestureDetector != null && event.isTouchEvent()) {
94 mGestureDetector.onTouchEvent(event);
96 switch (event.getActionMasked()) {
97 case MotionEvent.ACTION_DOWN:
98 mSwipeFireable = true;
99 mDebugFireable = true;
101 captureDown(event, 0);
102 if (mMouseHoveringAtEdge) {
103 mMouseHoveringAtEdge = false;
104 mCallbacks.onMouseLeaveFromEdge();
108 case MotionEvent.ACTION_POINTER_DOWN:
109 captureDown(event, event.getActionIndex());
110 if (mDebugFireable) {
111 mDebugFireable = event.getPointerCount() < 5;
112 if (!mDebugFireable) {
113 if (DEBUG) Slog.d(TAG, "Firing debug");
114 mCallbacks.onDebug();
118 case MotionEvent.ACTION_MOVE:
119 if (mSwipeFireable) {
120 final int swipe = detectSwipe(event);
121 mSwipeFireable = swipe == SWIPE_NONE;
122 if (swipe == SWIPE_FROM_TOP) {
123 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop");
124 mCallbacks.onSwipeFromTop();
125 } else if (swipe == SWIPE_FROM_BOTTOM) {
126 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom");
127 mCallbacks.onSwipeFromBottom();
128 } else if (swipe == SWIPE_FROM_RIGHT) {
129 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight");
130 mCallbacks.onSwipeFromRight();
131 } else if (swipe == SWIPE_FROM_LEFT) {
132 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft");
133 mCallbacks.onSwipeFromLeft();
137 case MotionEvent.ACTION_HOVER_MOVE:
138 if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
139 if (!mMouseHoveringAtEdge && event.getY() == 0) {
140 mCallbacks.onMouseHoverAtTop();
141 mMouseHoveringAtEdge = true;
142 } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
143 mCallbacks.onMouseHoverAtBottom();
144 mMouseHoveringAtEdge = true;
145 } else if (mMouseHoveringAtEdge
146 && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
147 mCallbacks.onMouseLeaveFromEdge();
148 mMouseHoveringAtEdge = false;
152 case MotionEvent.ACTION_UP:
153 case MotionEvent.ACTION_CANCEL:
154 mSwipeFireable = false;
155 mDebugFireable = false;
156 mCallbacks.onUpOrCancel();
159 if (DEBUG) Slog.d(TAG, "Ignoring " + event);
163 private void captureDown(MotionEvent event, int pointerIndex) {
164 final int pointerId = event.getPointerId(pointerIndex);
165 final int i = findIndex(pointerId);
166 if (DEBUG) Slog.d(TAG, "pointer " + pointerId +
167 " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
168 if (i != UNTRACKED_POINTER) {
169 mDownX[i] = event.getX(pointerIndex);
170 mDownY[i] = event.getY(pointerIndex);
171 mDownTime[i] = event.getEventTime();
172 if (DEBUG) Slog.d(TAG, "pointer " + pointerId +
173 " down x=" + mDownX[i] + " y=" + mDownY[i]);
177 private int findIndex(int pointerId) {
178 for (int i = 0; i < mDownPointers; i++) {
179 if (mDownPointerId[i] == pointerId) {
183 if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
184 return UNTRACKED_POINTER;
186 mDownPointerId[mDownPointers++] = pointerId;
187 return mDownPointers - 1;
190 private int detectSwipe(MotionEvent move) {
191 final int historySize = move.getHistorySize();
192 final int pointerCount = move.getPointerCount();
193 for (int p = 0; p < pointerCount; p++) {
194 final int pointerId = move.getPointerId(p);
195 final int i = findIndex(pointerId);
196 if (i != UNTRACKED_POINTER) {
197 for (int h = 0; h < historySize; h++) {
198 final long time = move.getHistoricalEventTime(h);
199 final float x = move.getHistoricalX(p, h);
200 final float y = move.getHistoricalY(p, h);
201 final int swipe = detectSwipe(i, time, x, y);
202 if (swipe != SWIPE_NONE) {
206 final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
207 if (swipe != SWIPE_NONE) {
215 private int detectSwipe(int i, long time, float x, float y) {
216 final float fromX = mDownX[i];
217 final float fromY = mDownY[i];
218 final long elapsed = time - mDownTime[i];
219 if (DEBUG) Slog.d(TAG, "pointer " + mDownPointerId[i]
220 + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
221 if (fromY <= mSwipeStartThreshold
222 && y > fromY + mSwipeDistanceThreshold
223 && elapsed < SWIPE_TIMEOUT_MS) {
224 return SWIPE_FROM_TOP;
226 if (fromY >= screenHeight - mSwipeStartThreshold
227 && y < fromY - mSwipeDistanceThreshold
228 && elapsed < SWIPE_TIMEOUT_MS) {
229 return SWIPE_FROM_BOTTOM;
231 if (fromX >= screenWidth - mSwipeStartThreshold
232 && x < fromX - mSwipeDistanceThreshold
233 && elapsed < SWIPE_TIMEOUT_MS) {
234 return SWIPE_FROM_RIGHT;
236 if (fromX <= mSwipeStartThreshold
237 && x > fromX + mSwipeDistanceThreshold
238 && elapsed < SWIPE_TIMEOUT_MS) {
239 return SWIPE_FROM_LEFT;
244 private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
246 public boolean onSingleTapUp(MotionEvent e) {
247 if (!mOverscroller.isFinished()) {
248 mOverscroller.forceFinished(true);
253 public boolean onFling(MotionEvent down, MotionEvent up,
254 float velocityX, float velocityY) {
255 mOverscroller.computeScrollOffset();
256 long now = SystemClock.uptimeMillis();
258 if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
259 mOverscroller.forceFinished(true);
261 mOverscroller.fling(0, 0, (int)velocityX, (int)velocityY,
262 Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
263 int duration = mOverscroller.getDuration();
264 if (duration > MAX_FLING_TIME_MILLIS) {
265 duration = MAX_FLING_TIME_MILLIS;
267 mLastFlingTime = now;
268 mCallbacks.onFling(duration);
273 interface Callbacks {
274 void onSwipeFromTop();
275 void onSwipeFromBottom();
276 void onSwipeFromRight();
277 void onSwipeFromLeft();
278 void onFling(int durationMs);
281 void onMouseHoverAtTop();
282 void onMouseHoverAtBottom();
283 void onMouseLeaveFromEdge();