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;
47 private final Context mContext;
48 private final int mSwipeStartThreshold;
49 private final int mSwipeDistanceThreshold;
50 private final Callbacks mCallbacks;
51 private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];
52 private final float[] mDownX = new float[MAX_TRACKED_POINTERS];
53 private final float[] mDownY = new float[MAX_TRACKED_POINTERS];
54 private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];
56 private GestureDetector mGestureDetector;
57 private OverScroller mOverscroller;
61 private int mDownPointers;
62 private boolean mSwipeFireable;
63 private boolean mDebugFireable;
64 private boolean mMouseHoveringAtEdge;
65 private long mLastFlingTime;
67 public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
69 mCallbacks = checkNull("callbacks", callbacks);
70 mSwipeStartThreshold = checkNull("context", context).getResources()
71 .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
72 mSwipeDistanceThreshold = mSwipeStartThreshold;
73 if (DEBUG) Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold
74 + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
77 private static <T> T checkNull(String name, T arg) {
79 throw new IllegalArgumentException(name + " must not be null");
84 public void systemReady() {
85 Handler h = new Handler(Looper.myLooper());
86 mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
87 mOverscroller = new OverScroller(mContext);
91 public void onPointerEvent(MotionEvent event) {
92 if (mGestureDetector != null && event.isTouchEvent()) {
93 mGestureDetector.onTouchEvent(event);
95 switch (event.getActionMasked()) {
96 case MotionEvent.ACTION_DOWN:
97 mSwipeFireable = true;
98 mDebugFireable = true;
100 captureDown(event, 0);
101 if (mMouseHoveringAtEdge) {
102 mMouseHoveringAtEdge = false;
103 mCallbacks.onMouseLeaveFromEdge();
107 case MotionEvent.ACTION_POINTER_DOWN:
108 captureDown(event, event.getActionIndex());
109 if (mDebugFireable) {
110 mDebugFireable = event.getPointerCount() < 5;
111 if (!mDebugFireable) {
112 if (DEBUG) Slog.d(TAG, "Firing debug");
113 mCallbacks.onDebug();
117 case MotionEvent.ACTION_MOVE:
118 if (mSwipeFireable) {
119 final int swipe = detectSwipe(event);
120 mSwipeFireable = swipe == SWIPE_NONE;
121 if (swipe == SWIPE_FROM_TOP) {
122 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop");
123 mCallbacks.onSwipeFromTop();
124 } else if (swipe == SWIPE_FROM_BOTTOM) {
125 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom");
126 mCallbacks.onSwipeFromBottom();
127 } else if (swipe == SWIPE_FROM_RIGHT) {
128 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight");
129 mCallbacks.onSwipeFromRight();
133 case MotionEvent.ACTION_HOVER_MOVE:
134 if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
135 if (!mMouseHoveringAtEdge && event.getY() == 0) {
136 mCallbacks.onMouseHoverAtTop();
137 mMouseHoveringAtEdge = true;
138 } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
139 mCallbacks.onMouseHoverAtBottom();
140 mMouseHoveringAtEdge = true;
141 } else if (mMouseHoveringAtEdge
142 && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
143 mCallbacks.onMouseLeaveFromEdge();
144 mMouseHoveringAtEdge = false;
148 case MotionEvent.ACTION_UP:
149 case MotionEvent.ACTION_CANCEL:
150 mSwipeFireable = false;
151 mDebugFireable = false;
152 mCallbacks.onUpOrCancel();
155 if (DEBUG) Slog.d(TAG, "Ignoring " + event);
159 private void captureDown(MotionEvent event, int pointerIndex) {
160 final int pointerId = event.getPointerId(pointerIndex);
161 final int i = findIndex(pointerId);
162 if (DEBUG) Slog.d(TAG, "pointer " + pointerId +
163 " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
164 if (i != UNTRACKED_POINTER) {
165 mDownX[i] = event.getX(pointerIndex);
166 mDownY[i] = event.getY(pointerIndex);
167 mDownTime[i] = event.getEventTime();
168 if (DEBUG) Slog.d(TAG, "pointer " + pointerId +
169 " down x=" + mDownX[i] + " y=" + mDownY[i]);
173 private int findIndex(int pointerId) {
174 for (int i = 0; i < mDownPointers; i++) {
175 if (mDownPointerId[i] == pointerId) {
179 if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
180 return UNTRACKED_POINTER;
182 mDownPointerId[mDownPointers++] = pointerId;
183 return mDownPointers - 1;
186 private int detectSwipe(MotionEvent move) {
187 final int historySize = move.getHistorySize();
188 final int pointerCount = move.getPointerCount();
189 for (int p = 0; p < pointerCount; p++) {
190 final int pointerId = move.getPointerId(p);
191 final int i = findIndex(pointerId);
192 if (i != UNTRACKED_POINTER) {
193 for (int h = 0; h < historySize; h++) {
194 final long time = move.getHistoricalEventTime(h);
195 final float x = move.getHistoricalX(p, h);
196 final float y = move.getHistoricalY(p, h);
197 final int swipe = detectSwipe(i, time, x, y);
198 if (swipe != SWIPE_NONE) {
202 final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
203 if (swipe != SWIPE_NONE) {
211 private int detectSwipe(int i, long time, float x, float y) {
212 final float fromX = mDownX[i];
213 final float fromY = mDownY[i];
214 final long elapsed = time - mDownTime[i];
215 if (DEBUG) Slog.d(TAG, "pointer " + mDownPointerId[i]
216 + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
217 if (fromY <= mSwipeStartThreshold
218 && y > fromY + mSwipeDistanceThreshold
219 && elapsed < SWIPE_TIMEOUT_MS) {
220 return SWIPE_FROM_TOP;
222 if (fromY >= screenHeight - mSwipeStartThreshold
223 && y < fromY - mSwipeDistanceThreshold
224 && elapsed < SWIPE_TIMEOUT_MS) {
225 return SWIPE_FROM_BOTTOM;
227 if (fromX >= screenWidth - mSwipeStartThreshold
228 && x < fromX - mSwipeDistanceThreshold
229 && elapsed < SWIPE_TIMEOUT_MS) {
230 return SWIPE_FROM_RIGHT;
235 private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
237 public boolean onSingleTapUp(MotionEvent e) {
238 if (!mOverscroller.isFinished()) {
239 mOverscroller.forceFinished(true);
244 public boolean onFling(MotionEvent down, MotionEvent up,
245 float velocityX, float velocityY) {
246 mOverscroller.computeScrollOffset();
247 long now = SystemClock.uptimeMillis();
249 if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
250 mOverscroller.forceFinished(true);
252 mOverscroller.fling(0, 0, (int)velocityX, (int)velocityY,
253 Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
254 int duration = mOverscroller.getDuration();
255 if (duration > MAX_FLING_TIME_MILLIS) {
256 duration = MAX_FLING_TIME_MILLIS;
258 mLastFlingTime = now;
259 mCallbacks.onFling(duration);
264 interface Callbacks {
265 void onSwipeFromTop();
266 void onSwipeFromBottom();
267 void onSwipeFromRight();
268 void onFling(int durationMs);
271 void onMouseHoverAtTop();
272 void onMouseHoverAtBottom();
273 void onMouseLeaveFromEdge();