1/*
2 * Copyright (C) 2013 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
17package com.android.server.policy;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.SystemClock;
23import android.util.Slog;
24import android.view.GestureDetector;
25import android.view.InputDevice;
26import android.view.MotionEvent;
27import android.view.WindowManagerPolicy.PointerEventListener;
28import android.widget.OverScroller;
29
30/*
31 * Listens for system-wide input gestures, firing callbacks when detected.
32 * @hide
33 */
34public 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;
41
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;
47
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];
56
57    private GestureDetector mGestureDetector;
58    private OverScroller mOverscroller;
59
60    int screenHeight;
61    int screenWidth;
62    private int mDownPointers;
63    private boolean mSwipeFireable;
64    private boolean mDebugFireable;
65    private boolean mMouseHoveringAtEdge;
66    private long mLastFlingTime;
67
68    public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
69        mContext = context;
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);
76    }
77
78    private static <T> T checkNull(String name, T arg) {
79        if (arg == null) {
80            throw new IllegalArgumentException(name + " must not be null");
81        }
82        return arg;
83    }
84
85    public void systemReady() {
86        Handler h = new Handler(Looper.myLooper());
87        mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
88        mOverscroller = new OverScroller(mContext);
89    }
90
91    @Override
92    public void onPointerEvent(MotionEvent event) {
93        if (mGestureDetector != null && event.isTouchEvent()) {
94            mGestureDetector.onTouchEvent(event);
95        }
96        switch (event.getActionMasked()) {
97            case MotionEvent.ACTION_DOWN:
98                mSwipeFireable = true;
99                mDebugFireable = true;
100                mDownPointers = 0;
101                captureDown(event, 0);
102                if (mMouseHoveringAtEdge) {
103                    mMouseHoveringAtEdge = false;
104                    mCallbacks.onMouseLeaveFromEdge();
105                }
106                mCallbacks.onDown();
107                break;
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();
115                    }
116                }
117                break;
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();
134                    }
135                }
136                break;
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;
149                    }
150                }
151                break;
152            case MotionEvent.ACTION_UP:
153            case MotionEvent.ACTION_CANCEL:
154                mSwipeFireable = false;
155                mDebugFireable = false;
156                mCallbacks.onUpOrCancel();
157                break;
158            default:
159                if (DEBUG) Slog.d(TAG, "Ignoring " + event);
160        }
161    }
162
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]);
174        }
175    }
176
177    private int findIndex(int pointerId) {
178        for (int i = 0; i < mDownPointers; i++) {
179            if (mDownPointerId[i] == pointerId) {
180                return i;
181            }
182        }
183        if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
184            return UNTRACKED_POINTER;
185        }
186        mDownPointerId[mDownPointers++] = pointerId;
187        return mDownPointers - 1;
188    }
189
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) {
203                        return swipe;
204                    }
205                }
206                final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
207                if (swipe != SWIPE_NONE) {
208                    return swipe;
209                }
210            }
211        }
212        return SWIPE_NONE;
213    }
214
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;
225        }
226        if (fromY >= screenHeight - mSwipeStartThreshold
227                && y < fromY - mSwipeDistanceThreshold
228                && elapsed < SWIPE_TIMEOUT_MS) {
229            return SWIPE_FROM_BOTTOM;
230        }
231        if (fromX >= screenWidth - mSwipeStartThreshold
232                && x < fromX - mSwipeDistanceThreshold
233                && elapsed < SWIPE_TIMEOUT_MS) {
234            return SWIPE_FROM_RIGHT;
235        }
236        if (fromX <= mSwipeStartThreshold
237                && x > fromX + mSwipeDistanceThreshold
238                && elapsed < SWIPE_TIMEOUT_MS) {
239            return SWIPE_FROM_LEFT;
240        }
241        return SWIPE_NONE;
242    }
243
244    private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
245        @Override
246        public boolean onSingleTapUp(MotionEvent e) {
247            if (!mOverscroller.isFinished()) {
248                mOverscroller.forceFinished(true);
249            }
250            return true;
251        }
252        @Override
253        public boolean onFling(MotionEvent down, MotionEvent up,
254                float velocityX, float velocityY) {
255            mOverscroller.computeScrollOffset();
256            long now = SystemClock.uptimeMillis();
257
258            if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
259                mOverscroller.forceFinished(true);
260            }
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;
266            }
267            mLastFlingTime = now;
268            mCallbacks.onFling(duration);
269            return true;
270        }
271    }
272
273    interface Callbacks {
274        void onSwipeFromTop();
275        void onSwipeFromBottom();
276        void onSwipeFromRight();
277        void onSwipeFromLeft();
278        void onFling(int durationMs);
279        void onDown();
280        void onUpOrCancel();
281        void onMouseHoverAtTop();
282        void onMouseHoverAtBottom();
283        void onMouseLeaveFromEdge();
284        void onDebug();
285    }
286}
287