summaryrefslogtreecommitdiffstats
path: root/services/java/com/android/server/gesture/EdgeGestureInputFilter.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/java/com/android/server/gesture/EdgeGestureInputFilter.java')
-rw-r--r--services/java/com/android/server/gesture/EdgeGestureInputFilter.java539
1 files changed, 539 insertions, 0 deletions
diff --git a/services/java/com/android/server/gesture/EdgeGestureInputFilter.java b/services/java/com/android/server/gesture/EdgeGestureInputFilter.java
new file mode 100644
index 0000000..c42b7d0
--- /dev/null
+++ b/services/java/com/android/server/gesture/EdgeGestureInputFilter.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project (Jens Doll)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.server.gesture;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Slog;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.IInputFilter;
+import android.view.IInputFilterHost;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.WindowManagerPolicy;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import com.android.internal.R;
+import com.android.internal.util.gesture.EdgeGesturePosition;
+import com.android.server.gesture.EdgeGestureTracker.OnActivationListener;
+
+import java.io.PrintWriter;
+
+/**
+ * A simple input filter, that listens for edge swipe gestures in the motion event input
+ * stream.
+ * <p>
+ * There are 5 distinct states of this filter.
+ * 1) LISTEN:
+ * mTracker.active == false
+ * All motion events are passed through. If a ACTION_DOWN within a gesture trigger area happen
+ * switch to DETECTING.
+ * 2) DETECTING:
+ * mTracker.active == true
+ * All events are buffered now, and the gesture is checked by mTracker. If mTracker rejects
+ * the gesture (hopefully as fast as possible) all cached events will be flushed out and the
+ * filter falls back to LISTEN.
+ * If mTracker accepts the gesture, clear all cached events and go to LOCKED.
+ * 3) LOCKED:
+ * mTracker.active == false
+ * All events will be cached until the state changes to SYNTHESIZE through a filter
+ * unlock event. If there is a ACTION_UP, _CANCEL or any PointerId differently to the last
+ * event seen when mTracker accepted the gesture, we flush all events and go to LISTEN.
+ * 4) SYNTHESIZE:
+ * The first motion event found will be turned into a ACTION_DOWN event, all previous events
+ * will be discarded.
+ * 5) POSTSYNTHESIZE:
+ * mSyntheticDownTime != -1
+ * All following events will have the down time set to the synthesized ACTION_DOWN event time
+ * until an ACTION_UP or ACTION_CANCEL is encountered and the state is reset to LISTEN.
+ * 6) DROP:
+ * All following events will be discarded. If there is an ACTION_UP, _CANCEL
+ * we go to LISTEN state.
+ * <p>
+ * If you are reading this within Java Doc, you are doing something wrong ;)
+ */
+public class EdgeGestureInputFilter implements IInputFilter {
+ /* WARNING!! The IInputFilter interface is used directly, there is no Binder between this and
+ * the InputDispatcher.
+ * This is fine, because it prevents unnecessary parceling, but beware:
+ * This means we are running on the dispatch or listener thread of the input dispatcher. Every
+ * cycle we waste here adds to the overall input latency.
+ */
+ private static final String TAG = "EdgeGestureInputFilter";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_INPUT = false;
+ // TODO: Should be turned off in final commit
+ private static final boolean SYSTRACE = false;
+
+ private final Handler mHandler;
+
+ private IInputFilterHost mHost = null; // dispatcher thread
+
+ private static final class MotionEventInfo {
+ private static final int MAX_POOL_SIZE = 16;
+
+ private static final Object sLock = new Object();
+ private static MotionEventInfo sPool;
+ private static int sPoolSize;
+
+ private boolean mInPool;
+
+ public static MotionEventInfo obtain(MotionEvent event, int policyFlags) {
+ synchronized (sLock) {
+ MotionEventInfo info;
+ if (sPoolSize > 0) {
+ sPoolSize--;
+ info = sPool;
+ sPool = info.next;
+ info.next = null;
+ info.mInPool = false;
+ } else {
+ info = new MotionEventInfo();
+ }
+ info.initialize(event, policyFlags);
+ return info;
+ }
+ }
+
+ private void initialize(MotionEvent event, int policyFlags) {
+ this.event = MotionEvent.obtain(event);
+ this.policyFlags = policyFlags;
+ cachedTimeMillis = SystemClock.uptimeMillis();
+ }
+
+ public void recycle() {
+ synchronized (sLock) {
+ if (mInPool) {
+ throw new IllegalStateException("Already recycled.");
+ }
+ clear();
+ if (sPoolSize < MAX_POOL_SIZE) {
+ sPoolSize++;
+ next = sPool;
+ sPool = this;
+ mInPool = true;
+ }
+ }
+ }
+
+ private void clear() {
+ event.recycle();
+ event = null;
+ policyFlags = 0;
+ }
+
+ public MotionEventInfo next;
+ public MotionEvent event;
+ public int policyFlags;
+ public long cachedTimeMillis;
+ }
+ private final Object mLock = new Object();
+ private MotionEventInfo mMotionEventQueue; // guarded by mLock
+ private MotionEventInfo mMotionEventQueueTail; // guarded by mLock
+ /* DEBUG */
+ private int mMotionEventQueueCountDebug; // guarded by mLock
+
+ private int mDeviceId; // dispatcher only
+ private enum State {
+ LISTEN, DETECTING, LOCKED, SYNTHESIZE, POSTSYNTHESIZE, DROP;
+ }
+ private State mState = State.LISTEN; // guarded by mLock
+ private EdgeGestureTracker mTracker; // guarded by mLock
+ private volatile int mPositions; // written by handler / read by dispatcher
+ private volatile int mSensitivity; // written by handler / read by dispatcher
+
+ // only used by dispatcher
+ private long mSyntheticDownTime = -1;
+ private PointerCoords[] mTempPointerCoords = new PointerCoords[1];
+ private PointerProperties[] mTempPointerProperties = new PointerProperties[1];
+
+ public EdgeGestureInputFilter(Context context, Handler handler) {
+ mHandler = handler;
+
+ final Resources res = context.getResources();
+ mTracker = new EdgeGestureTracker(res.getDimensionPixelSize(
+ R.dimen.edge_gesture_trigger_thickness),
+ res.getDimensionPixelSize(R.dimen.edge_gesture_trigger_distance),
+ res.getDimensionPixelSize(R.dimen.edge_gesture_perpendicular_distance));
+ mTracker.setOnActivationListener(new OnActivationListener() {
+ public void onActivation(MotionEvent event, int touchX, int touchY, EdgeGesturePosition position) {
+ // mLock is held by #processMotionEvent
+ mHandler.obtainMessage(EdgeGestureService.MSG_EDGE_GESTURE_ACTIVATION,
+ touchX, touchY, position).sendToTarget();
+ mState = State.LOCKED;
+ }
+ });
+ mTempPointerCoords[0] = new PointerCoords();
+ mTempPointerProperties[0] = new PointerProperties();
+ }
+
+ // called from handler thread (lock taken)
+ public void updateDisplay(Display display, DisplayInfo displayInfo) {
+ synchronized (mLock) {
+ mTracker.updateDisplay(display);
+ }
+ }
+
+ // called from handler thread (lock taken)
+ public void updatePositions(int positions) {
+ mPositions = positions;
+ }
+
+ // called from handler thread (lock taken)
+ public void updateSensitivity(int sensitivity) {
+ mSensitivity = sensitivity;
+ }
+
+ // called from handler thread
+ public boolean unlockFilter() {
+ synchronized (mLock) {
+ if (mState == State.LOCKED) {
+ mState = State.SYNTHESIZE;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean dropSequence() {
+ synchronized (mLock) {
+ if (mState == State.LOCKED) {
+ mState = State.DROP;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called to enqueue the input event for filtering.
+ * The event must be recycled after the input filter processed it.
+ * This method is guaranteed to be non-reentrant.
+ *
+ * @see InputFilter#filterInputEvent(InputEvent, int)
+ * @param event The input event to enqueue.
+ */
+ // called by the input dispatcher thread
+ public void filterInputEvent(InputEvent event, int policyFlags) throws RemoteException {
+ if (SYSTRACE) {
+ Trace.traceBegin(Trace.TRACE_TAG_INPUT, "filterInputEvent");
+ }
+ try {
+ if (((event.getSource() & InputDevice.SOURCE_TOUCHSCREEN)
+ != InputDevice.SOURCE_TOUCHSCREEN)
+ || !(event instanceof MotionEvent)) {
+ sendInputEvent(event, policyFlags);
+ return;
+ }
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
+ + Integer.toHexString(policyFlags));
+ }
+ MotionEvent motionEvent = (MotionEvent) event;
+ final int deviceId = event.getDeviceId();
+ if (deviceId != mDeviceId) {
+ processDeviceSwitch(deviceId, motionEvent, policyFlags);
+ } else {
+ if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
+ synchronized (mLock) {
+ clearAndResetStateLocked(false, true);
+ }
+ }
+ processMotionEvent(motionEvent, policyFlags);
+ }
+ } finally {
+ event.recycle();
+ if (SYSTRACE) {
+ Trace.traceEnd(Trace.TRACE_TAG_INPUT);
+ }
+ }
+ }
+
+ private void processDeviceSwitch(int deviceId, MotionEvent motionEvent, int policyFlags) {
+ if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mDeviceId = deviceId;
+ synchronized (mLock) {
+ clearAndResetStateLocked(true, false);
+ processMotionEvent(motionEvent, policyFlags);
+ }
+ } else {
+ sendInputEvent(motionEvent, policyFlags);
+ }
+ }
+
+ private void processMotionEvent(MotionEvent motionEvent, int policyFlags) {
+ final int action = motionEvent.getActionMasked();
+
+ synchronized (mLock) {
+ switch (mState) {
+ case LISTEN:
+ if (action == MotionEvent.ACTION_DOWN) {
+ boolean hit = mPositions != 0
+ && mTracker.start(motionEvent, mPositions, mSensitivity);
+ if (DEBUG) Slog.d(TAG, "start:" + hit);
+ if (hit) {
+ // cache the down event
+ cacheDelayedMotionEventLocked(motionEvent, policyFlags);
+ mState = State.DETECTING;
+ return;
+ }
+ }
+ sendInputEvent(motionEvent, policyFlags);
+ break;
+ case DETECTING:
+ cacheDelayedMotionEventLocked(motionEvent, policyFlags);
+ if (action == MotionEvent.ACTION_MOVE) {
+ if (mTracker.move(motionEvent)) {
+ // return: the tracker is either detecting or triggered onActivation
+ return;
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "move: reset!");
+ }
+ clearAndResetStateLocked(false, true);
+ break;
+ case LOCKED:
+ cacheDelayedMotionEventLocked(motionEvent, policyFlags);
+ if (action != MotionEvent.ACTION_MOVE) {
+ clearAndResetStateLocked(false, true);
+ }
+ break;
+ case SYNTHESIZE:
+ if (action == MotionEvent.ACTION_MOVE) {
+ clearDelayedMotionEventsLocked();
+ sendSynthesizedMotionEventLocked(motionEvent, policyFlags);
+ mState = State.POSTSYNTHESIZE;
+ } else {
+ // This is the case where a race condition caught us: We already
+ // returned the handler thread that it is all right to call
+ // #gainTouchFocus(), but apparently this was wrong, as the gesture
+ // was canceled now.
+ clearAndResetStateLocked(false, true);
+ }
+ break;
+ case POSTSYNTHESIZE:
+ motionEvent.setDownTime(mSyntheticDownTime);
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mState = State.LISTEN;
+ mSyntheticDownTime = -1;
+ }
+ sendInputEvent(motionEvent, policyFlags);
+ break;
+ case DROP:
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ clearDelayedMotionEventsLocked();
+ mState = State.LISTEN;
+ }
+ break;
+ }
+ }
+ }
+
+ private void clearAndResetStateLocked(boolean force, boolean shift) {
+ // ignore soft reset in POSTSYNTHESIZE, because we need to tamper with
+ // the event stream and going to LISTEN after an ACTION_UP anyway
+ if (!force && (mState == State.POSTSYNTHESIZE)) {
+ return;
+ }
+ switch (mState) {
+ case LISTEN:
+ // this is a nop
+ break;
+ case DETECTING:
+ mTracker.reset();
+ // intentionally no break here
+ case LOCKED:
+ case SYNTHESIZE:
+ sendDelayedMotionEventsLocked(shift);
+ break;
+ case POSTSYNTHESIZE:
+ // hard reset (this will break the event stream)
+ Slog.w(TAG, "Quit POSTSYNTHESIZE without ACTION_UP from ACTION_DOWN at "
+ + mSyntheticDownTime);
+ mSyntheticDownTime = -1;
+ break;
+ }
+ // if there are future events that need to be tampered with, goto POSTSYNTHESIZE
+ mState = mSyntheticDownTime == -1 ? State.LISTEN : State.POSTSYNTHESIZE;
+ }
+
+ private void sendInputEvent(InputEvent event, int policyFlags) {
+ try {
+ mHost.sendInputEvent(event, policyFlags);
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ }
+
+ private void cacheDelayedMotionEventLocked(MotionEvent event, int policyFlags) {
+ MotionEventInfo info = MotionEventInfo.obtain(event, policyFlags);
+ if (mMotionEventQueue == null) {
+ mMotionEventQueue = info;
+ } else {
+ mMotionEventQueueTail.next = info;
+ }
+ mMotionEventQueueTail = info;
+ mMotionEventQueueCountDebug++;
+ if (SYSTRACE) {
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, "meq", mMotionEventQueueCountDebug);
+ }
+ }
+
+ private void sendDelayedMotionEventsLocked(boolean shift) {
+ while (mMotionEventQueue != null) {
+ MotionEventInfo info = mMotionEventQueue;
+ mMotionEventQueue = info.next;
+
+ if (DEBUG) {
+ Slog.d(TAG, "Replay event: " + info.event);
+ }
+ mMotionEventQueueCountDebug--;
+ if (SYSTRACE) {
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, "meq", mMotionEventQueueCountDebug);
+ }
+ if (shift) {
+ final long offset = SystemClock.uptimeMillis() - info.cachedTimeMillis;
+ if (info.event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mSyntheticDownTime = info.event.getDownTime() + offset;
+ }
+ sendMotionEventWithOffsetLocked(info.event, info.policyFlags, mSyntheticDownTime, offset);
+ if (info.event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mSyntheticDownTime = -1;
+ }
+ } else {
+ sendInputEvent(info.event, info.policyFlags);
+ }
+ info.recycle();
+ }
+ mMotionEventQueueTail = null;
+ }
+
+ private void clearDelayedMotionEventsLocked() {
+ while (mMotionEventQueue != null) {
+ MotionEventInfo next = mMotionEventQueue.next;
+ mMotionEventQueue.recycle();
+ mMotionEventQueue = next;
+ }
+ mMotionEventQueueTail = null;
+ mMotionEventQueueCountDebug = 0;
+ if (SYSTRACE) {
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, "meq", mMotionEventQueueCountDebug);
+ }
+ }
+
+ private void sendMotionEventWithOffsetLocked(MotionEvent event, int policyFlags,
+ long downTime, long offset) {
+ final int pointerCount = event.getPointerCount();
+ PointerCoords[] coords = getTempPointerCoordsWithMinSizeLocked(pointerCount);
+ PointerProperties[] properties = getTempPointerPropertiesWithMinSizeLocked(pointerCount);
+ for (int i = 0; i < pointerCount; i++) {
+ event.getPointerCoords(i, coords[i]);
+ event.getPointerProperties(i, properties[i]);
+ }
+ final long eventTime = event.getEventTime() + offset;
+ sendInputEvent(MotionEvent.obtain(downTime, eventTime, event.getAction(), pointerCount,
+ properties, coords, event.getMetaState(), event.getButtonState(), 1.0f, 1.0f,
+ event.getDeviceId(), event.getEdgeFlags(), event.getSource(), event.getFlags()),
+ policyFlags);
+ }
+
+ private PointerCoords[] getTempPointerCoordsWithMinSizeLocked(int size) {
+ final int oldSize = mTempPointerCoords.length;
+ if (oldSize < size) {
+ PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
+ mTempPointerCoords = new PointerCoords[size];
+ System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
+ }
+ for (int i = oldSize; i < size; i++) {
+ mTempPointerCoords[i] = new PointerCoords();
+ }
+ return mTempPointerCoords;
+ }
+
+ private PointerProperties[] getTempPointerPropertiesWithMinSizeLocked(int size) {
+ final int oldSize = mTempPointerProperties.length;
+ if (oldSize < size) {
+ PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
+ mTempPointerProperties = new PointerProperties[size];
+ System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
+ }
+ for (int i = oldSize; i < size; i++) {
+ mTempPointerProperties[i] = new PointerProperties();
+ }
+ return mTempPointerProperties;
+ }
+
+ private void sendSynthesizedMotionEventLocked(MotionEvent event, int policyFlags) {
+ if (event.getPointerCount() == 1) {
+ event.getPointerCoords(0, mTempPointerCoords[0]);
+ event.getPointerProperties(0, mTempPointerProperties[0]);
+ MotionEvent down = MotionEvent.obtain(event.getEventTime(), event.getEventTime(),
+ MotionEvent.ACTION_DOWN, 1, mTempPointerProperties, mTempPointerCoords,
+ event.getMetaState(), event.getButtonState(),
+ 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
+ event.getSource(), event.getFlags());
+ Slog.d(TAG, "Synthesized event:" + down);
+ sendInputEvent(down, policyFlags);
+ mSyntheticDownTime = event.getEventTime();
+ } else {
+ Slog.w(TAG, "Could not synthesize MotionEvent, this will drop all following events!");
+ }
+ }
+
+ // should never be called
+ public IBinder asBinder() {
+ throw new UnsupportedOperationException();
+ }
+
+ // called by the input dispatcher thread
+ public void install(IInputFilterHost host) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "EdgeGesture input filter installed.");
+ }
+ mHost = host;
+ synchronized (mLock) {
+ clearAndResetStateLocked(true, false);
+ }
+ }
+
+ // called by the input dispatcher thread
+ public void uninstall() throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "EdgeGesture input filter uninstalled.");
+ }
+ }
+
+ // called by a Binder thread
+ public void dump(PrintWriter pw, String prefix) {
+ synchronized (mLock) {
+ pw.print(prefix);
+ pw.println("mState=" + mState.name());
+ pw.print(prefix);
+ pw.println("mPositions=0x" + Integer.toHexString(mPositions));
+ pw.print(prefix);
+ pw.println("mQueue=" + mMotionEventQueueCountDebug + " items");
+ }
+ }
+}