diff --git a/Android.mk b/Android.mk
index 28036cfce4d55..d1e7d9031c17f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -145,6 +145,9 @@ LOCAL_SRC_FILES += \
core/java/android/os/IVibratorService.aidl \
core/java/android/service/dreams/IDreamManager.aidl \
core/java/android/service/dreams/IDreamService.aidl \
+ core/java/android/service/pie/IPieService.aidl \
+ core/java/android/service/pie/IPieActivationListener.aidl \
+ core/java/android/service/pie/IPieHostCallback.aidl \
core/java/android/service/wallpaper/IWallpaperConnection.aidl \
core/java/android/service/wallpaper/IWallpaperEngine.aidl \
core/java/android/service/wallpaper/IWallpaperService.aidl \
diff --git a/core/java/android/service/pie/IPieActivationListener.aidl b/core/java/android/service/pie/IPieActivationListener.aidl
new file mode 100644
index 0000000000000..f05e4721751b7
--- /dev/null
+++ b/core/java/android/service/pie/IPieActivationListener.aidl
@@ -0,0 +1,14 @@
+
+package android.service.pie;
+
+import android.view.InputEvent;
+
+/** @hide */
+interface IPieActivationListener {
+
+ /** Called when a gesture is detected that fits to the pie activation gesture. At this point in
+ * time gesture detection is disabled. Call IPieHostCallback.restoreState() to
+ * recover from this.
+ */
+ oneway void onPieActivation(int touchX, int touchY, int positionIndex, int flags);
+}
\ No newline at end of file
diff --git a/core/java/android/service/pie/IPieHostCallback.aidl b/core/java/android/service/pie/IPieHostCallback.aidl
new file mode 100644
index 0000000000000..1853294a091fd
--- /dev/null
+++ b/core/java/android/service/pie/IPieHostCallback.aidl
@@ -0,0 +1,15 @@
+package android.service.pie;
+
+/** @hide */
+interface IPieHostCallback {
+
+ /** After being activated, this allows the pie control to steal focus from the current
+ * window
+ */
+ boolean gainTouchFocus(IBinder windowToken);
+
+ /** Turns listening for pie activation gestures on again, after it was disabled during
+ * the call to the listener.
+ */
+ oneway void restoreListenerState();
+}
\ No newline at end of file
diff --git a/core/java/android/service/pie/IPieService.aidl b/core/java/android/service/pie/IPieService.aidl
new file mode 100644
index 0000000000000..4fd1a0f3809ab
--- /dev/null
+++ b/core/java/android/service/pie/IPieService.aidl
@@ -0,0 +1,20 @@
+package android.service.pie;
+
+import android.service.pie.IPieActivationListener;
+import android.service.pie.IPieHostCallback;
+
+/** @hide */
+interface IPieService {
+
+ /** Register a listener for pie activation gestures. Initially the listener
+ * is set to listen for no position. Use updatePieActivationListener() to
+ * bind the listener to positions.
+ * Use the returned IPieHostCallback to manipulate the state after activation.
+ */
+ IPieHostCallback registerPieActivationListener(in IPieActivationListener listener);
+
+ /** Update the listener to react on gestures in the given positions.
+ */
+ void updatePieActivationListener(in IBinder listener, int positionFlags);
+
+}
\ No newline at end of file
diff --git a/core/java/android/service/pie/PieManager.java b/core/java/android/service/pie/PieManager.java
new file mode 100644
index 0000000000000..1ba9d371d76ca
--- /dev/null
+++ b/core/java/android/service/pie/PieManager.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.service.pie;
+
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.pie.IPieService;
+import android.util.Slog;
+
+import com.android.internal.util.pie.PiePosition;
+
+/**
+ * This is a simple Manager class for pie service on the application side. The application need
+ * {@code INJECT_EVENTS} permission to register {@code PieActivationListener}s.
+ * See {@link IPieService} for more information.
+ *
+ * @see IPieService
+ * @hide
+ */
+public class PieManager {
+ public static final String TAG = "PieManager";
+ public static final boolean DEBUG = false;
+
+ private static PieManager sInstance;
+
+ private final IPieService mPs;
+
+ public static abstract class PieActivationListener {
+ private Handler mHandler;
+ private IPieHostCallback mCallback;
+
+ private class Delegator extends IPieActivationListener.Stub {
+ public void onPieActivation(final int touchX, final int touchY, final int positionIndex, final int flags)
+ throws RemoteException {
+ mHandler.post(new Runnable() {
+ public void run() {
+ PieActivationListener.this.onPieActivation(touchX, touchY, PiePosition.values()[positionIndex], flags);
+ }
+ });
+ }
+ }
+ private Delegator mDelegator;
+
+ public PieActivationListener() {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ public PieActivationListener(Looper looper) {
+ mHandler = new Handler(looper);
+ mDelegator = new Delegator();
+ }
+
+ /* package */ void setHostCallback(IPieHostCallback hostCallback) {
+ mCallback = hostCallback;
+ }
+
+ /**
+ * Override this to receive activations from the pie service.
+ *
+ * @param touchX the last X position a touch event was registered.
+ * @param touchY the last Y position a touch event was registered.
+ * @param position the position of the activation.
+ * @param flags currently 0.
+ * @see IPieActivationListener#onPieActivation(int, int, int, int)
+ */
+ public abstract void onPieActivation(int touchX, int touchY, PiePosition position, int flags);
+
+ /**
+ * After being activated, this allows the pie control to steal focus from the current
+ * window.
+ *
+ * @see IPieHostCallback#gainTouchFocus(IBinder)
+ */
+ public boolean gainTouchFocus(IBinder applicationWindowToken) {
+ try {
+ return mCallback.gainTouchFocus(applicationWindowToken);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "gainTouchFocus failed: " + e.getMessage());
+ /* fall through */
+ }
+ return false;
+ }
+
+ /**
+ * Turns listening for pie activation gestures on again, after it was disabled during
+ * the call to the listener.
+ *
+ * @see IPieHostCallback#restoreListenerState()
+ */
+ public void restoreListenerState() {
+ if (DEBUG) {
+ Slog.d(TAG, "restore listener state: " + Thread.currentThread().getName());
+ }
+ try {
+ mCallback.restoreListenerState();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "restoreListenerState failed: " + e.getMessage());
+ /* fall through */
+ }
+ }
+ }
+
+ private PieManager(IPieService ps) {
+ mPs = ps;
+ }
+
+ /**
+ * Gets an instance of the pie manager.
+ *
+ * @return The pie manager instance.
+ * @hide
+ */
+ public static PieManager getInstance() {
+ synchronized (PieManager.class) {
+ if (sInstance == null) {
+ IBinder b = ServiceManager.getService("pieservice");
+ sInstance = new PieManager(IPieService.Stub.asInterface(b));
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Checks if the pie service is present.
+ *
+ * Since the service is only started at boot time and is bound to the system server, this + * is constant for the devices up time. + * + * @return {@code true} when the pie service is running on this device. + * @hide + */ + public boolean isPresent() { + return mPs != null; + } + + /** + * Register a listener for pie activation gestures. Initially the listener + * is set to listen for no position. Use updatePieActivationListener() to + * bind the listener to positions. + * + * @param listener is the activation listener. + * @return {@code true} if the registration was successful. + * @hide + */ + public boolean setPieActivationListener(PieActivationListener listener) { + if (DEBUG) { + Slog.d(TAG, "Set pie activation listener"); + } + try { + IPieHostCallback callback = mPs.registerPieActivationListener(listener.mDelegator); + listener.setHostCallback(callback); + return true; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set pie activation listener: " + e.getMessage()); + return false; + } + } + + /** + * Update the listener to react on gestures in the given positions. + * + * @param listener is a already registered listener. + * @param positions is a bit mask describing the positions to listen to. + * @hide + */ + public void updatePieActivationListener(PieActivationListener listener, int positions) { + if (DEBUG) { + Slog.d(TAG, "Update pie activation listener: 0x" + Integer.toHexString(positions)); + } + try { + mPs.updatePieActivationListener(listener.mDelegator.asBinder(), positions); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to update pie activation listener: " + e.getMessage()); + } + } + +} diff --git a/core/java/android/service/pie/package.html b/core/java/android/service/pie/package.html new file mode 100644 index 0000000000000..c9f96a66ab3bc --- /dev/null +++ b/core/java/android/service/pie/package.html @@ -0,0 +1,5 @@ +
+ +{@hide} + + diff --git a/core/java/com/android/internal/util/pie/PiePosition.java b/core/java/com/android/internal/util/pie/PiePosition.java new file mode 100644 index 0000000000000..be9626f4e55c4 --- /dev/null +++ b/core/java/com/android/internal/util/pie/PiePosition.java @@ -0,0 +1,42 @@ +/* + * 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.internal.util.pie; + +/** + * Defines the positions in which pie controls may appear and gestures may be recognized by the + * pie service. + * This defines an index and an flag for each position. + */ +public enum PiePosition { + LEFT(0, 0), + BOTTOM(1, 1), + RIGHT(2, 1), + TOP(3, 0); + + PiePosition(int index, int factor) { + INDEX = index; + FLAG = (0x01<+ * Note: Even though this implementation using AIDL interfaces, it is designed to only + * provide direct access. Therefore, any filter registering should reside in the + * system server DVM only! + * + * @param filter The input filter to register. + */ + public void registerSecondaryInputFilter(IInputFilter filter) { + synchronized (mInputFilterLock) { + ChainedInputFilterHost host = new ChainedInputFilterHost(filter, null); + if (!mInputFilterChain.isEmpty()) { + mInputFilterChain.get(mInputFilterChain.size() - 1).mNext = host; + } + host.connectLocked(); + mInputFilterChain.add(host); + + nativeSetInputFilterEnabled(mPtr, !mInputFilterChain.isEmpty()); + } + } + + public void unregisterSecondaryInputFilter(IInputFilter filter) { + synchronized (mInputFilterLock) { + int index = findInputFilterIndexLocked(filter); + if (index >= 0) { + ChainedInputFilterHost host = mInputFilterChain.get(index); + host.disconnectLocked(); + if (index >= 1) { + mInputFilterChain.get(index - 1).mNext = host.mNext; } + mInputFilterChain.remove(index); } - nativeSetInputFilterEnabled(mPtr, filter != null); + nativeSetInputFilterEnabled(mPtr, !mInputFilterChain.isEmpty()); + } + } + + private int findInputFilterIndexLocked(IInputFilter filter) { + for (int i = 0; i < mInputFilterChain.size(); i++) { + if (mInputFilterChain.get(i).mInputFilter == filter) { + return i; + } } + return -1; } @Override // Binder call @@ -1327,16 +1372,23 @@ private long notifyANR(InputApplicationHandle inputApplicationHandle, // Native callback. final boolean filterInputEvent(InputEvent event, int policyFlags) { + ChainedInputFilterHost head = null; synchronized (mInputFilterLock) { - if (mInputFilter != null) { - try { - mInputFilter.filterInputEvent(event, policyFlags); - } catch (RemoteException e) { - /* ignore */ - } - return false; + if (!mInputFilterChain.isEmpty()) { + head = mInputFilterChain.get(0); } } + // call filter input event outside of the lock. + // this is safe, because we know that mInputFilter never changes. + // we may loose a event, but this does not differ from the original implementation. + if (head != null) { + try { + head.mInputFilter.filterInputEvent(event, policyFlags); + } catch (RemoteException e) { + /* ignore */ + } + return false; + } event.recycle(); return true; } @@ -1561,10 +1613,32 @@ public void handleMessage(Message msg) { /** * Hosting interface for input filters to call back into the input manager. */ - private final class InputFilterHost extends IInputFilterHost.Stub { + private final class ChainedInputFilterHost extends IInputFilterHost.Stub { + private final IInputFilter mInputFilter; + private ChainedInputFilterHost mNext; private boolean mDisconnected; + private ChainedInputFilterHost(IInputFilter filter, ChainedInputFilterHost next) { + mInputFilter = filter; + mNext = next; + mDisconnected = false; + } + + public void connectLocked() { + try { + mInputFilter.install(this); + } catch (RemoteException re) { + /* ignore */ + } + } + public void disconnectLocked() { + try { + mInputFilter.uninstall(); + } catch (RemoteException re) { + /* ignore */ + } + // DO NOT set mInputFilter to null here! mInputFilter is used outside of the lock! mDisconnected = true; } @@ -1576,9 +1650,17 @@ public void sendInputEvent(InputEvent event, int policyFlags) { synchronized (mInputFilterLock) { if (!mDisconnected) { - nativeInjectInputEvent(mPtr, event, 0, 0, - InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0, - policyFlags | WindowManagerPolicy.FLAG_FILTERED); + if (mNext == null) { + nativeInjectInputEvent(mPtr, event, 0, 0, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0, + policyFlags | WindowManagerPolicy.FLAG_FILTERED); + } else { + try { + mNext.mInputFilter.filterInputEvent(event, policyFlags); + } catch (RemoteException e) { + /* ignore */ + } + } } } } diff --git a/services/java/com/android/server/pie/PieGestureTracker.java b/services/java/com/android/server/pie/PieGestureTracker.java new file mode 100644 index 0000000000000..d9926576c797c --- /dev/null +++ b/services/java/com/android/server/pie/PieGestureTracker.java @@ -0,0 +1,221 @@ +/* + * 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.pie; + +import android.graphics.Point; +import android.os.SystemClock; +import android.util.Slog; +import android.view.Display; +import android.view.MotionEvent; + +import com.android.internal.util.pie.PiePosition; + +/** + * A simple {@link MotionEvent} tracker class. The main aim of this tracker is to + * reject gestures as fast as possible, so there is only a small amount of events + * that will be delayed. + */ +public class PieGestureTracker { + public final static String TAG = "PieTracker"; + public final static boolean DEBUG = false; + + public final static long TRIGGER_TIME_MS = 140; + public final static int PIXEL_SWIPE_OFFTAKE_SLOP = 2; + + private final int mTriggerThickness; + private final int mTriggerDistance; + private final int mPerpendicularDistance; + + private int mDisplayWidth; + private int mDisplayHeight; + + private boolean mActive; + private PiePosition mPosition; + private long mDownTime; + private int mInitialX; + private int mInitialY; + private int mOffTake; + private int mGracePeriod; + + public interface OnActivationListener { + public void onActivation(MotionEvent event, int touchX, int touchY, PiePosition position); + } + private OnActivationListener mActivationListener; + + public PieGestureTracker(int thickness, int distance, int perpendicular) { + if (DEBUG) { + Slog.d(TAG, "init: " + thickness + "," + distance); + } + mTriggerThickness = thickness; + mTriggerDistance = distance; + mPerpendicularDistance = perpendicular; + } + + public void setOnActivationListener(OnActivationListener listener) { + mActivationListener = listener; + } + + public void reset() { + mActive = false; + } + + public void updateDisplay(Display display) { + Point outSize = new Point(0,0); + display.getSize(outSize); + mDisplayWidth = outSize.x; + mDisplayHeight = outSize.y; + if (DEBUG) { + Slog.d(TAG, "new display: " + mDisplayWidth + "," + mDisplayHeight); + } + } + + public boolean start(MotionEvent motionEvent, int positions) { + final int x = (int) motionEvent.getX(); + final float fx = motionEvent.getX() / mDisplayWidth; + final int y = (int) motionEvent.getY(); + final float fy = motionEvent.getY() / mDisplayHeight; + + if ((positions & PiePosition.LEFT.FLAG) != 0) { + if (x < mTriggerThickness && fy > 0.1f && fy < 0.9f) { + startWithPosition(motionEvent, PiePosition.LEFT); + return true; + } + } + if ((positions & PiePosition.BOTTOM.FLAG) != 0) { + if (y > mDisplayHeight - mTriggerThickness && fx > 0.1f && fx < 0.9f) { + startWithPosition(motionEvent, PiePosition.BOTTOM); + return true; + } + } + if ((positions & PiePosition.RIGHT.FLAG) != 0) { + if (x > mDisplayWidth - mTriggerThickness && fy > 0.1f && fy < 0.9f) { + startWithPosition(motionEvent, PiePosition.RIGHT); + return true; + } + } + if ((positions & PiePosition.TOP.FLAG) != 0) { + if (y < mTriggerThickness && fx > 0.1f && fx < 0.9f) { + startWithPosition(motionEvent, PiePosition.TOP); + return true; + } + } + return false; + } + + private void startWithPosition(MotionEvent motionEvent, PiePosition position) { + if (DEBUG) { + Slog.d(TAG, "start tracking from " + position.name()); + } + + mDownTime = motionEvent.getDownTime(); + this.mPosition = position; + mInitialX = (int) motionEvent.getX(); + mInitialY = (int) motionEvent.getY(); + switch (position) { + case LEFT: + mGracePeriod = (int) (mTriggerDistance / 3.0f); + mOffTake = mInitialX - PIXEL_SWIPE_OFFTAKE_SLOP; + break; + case BOTTOM: + mOffTake = mInitialY + PIXEL_SWIPE_OFFTAKE_SLOP; + break; + case RIGHT: + mGracePeriod = mDisplayWidth - (int) (mTriggerDistance / 3.0f); + mOffTake = mInitialX + PIXEL_SWIPE_OFFTAKE_SLOP; + break; + case TOP: + mOffTake = mInitialY - PIXEL_SWIPE_OFFTAKE_SLOP; + break; + } + mActive = true; + } + + public boolean move(MotionEvent motionEvent) { + if (!mActive || motionEvent.getEventTime() - mDownTime > TRIGGER_TIME_MS) { + Slog.d(TAG, "pie gesture timeout: " + (motionEvent.getEventTime() - mDownTime)); + mActive = false; + return false; + } + + final int x = (int) motionEvent.getX(); + final int y = (int) motionEvent.getY(); + final int deltaX = x - mInitialX; + final int deltaY = y - mInitialY; + + if (DEBUG) { + Slog.d(TAG, "move at " + x + "," + y + " " + deltaX + "," + deltaY); + } + + boolean loaded = false; + switch (mPosition) { + case LEFT: + if (x < mGracePeriod) { + mInitialY = y; + } + if (deltaY < mPerpendicularDistance && deltaY > -mPerpendicularDistance + && x >= mOffTake) { + if (deltaX < mTriggerDistance) { + mOffTake = x - PIXEL_SWIPE_OFFTAKE_SLOP; + return true; + } + loaded = true; + } + break; + case BOTTOM: + if (deltaX < mPerpendicularDistance && deltaX > -mPerpendicularDistance + && y <= mOffTake) { + if (deltaY > -mTriggerDistance) { + mOffTake = y + PIXEL_SWIPE_OFFTAKE_SLOP; + return true; + } + loaded = true; + } + break; + case RIGHT: + if (x > mGracePeriod) { + mInitialY = y; + } + if (deltaY < mPerpendicularDistance && deltaY > -mPerpendicularDistance + && x <= mOffTake) { + if (deltaX > -mTriggerDistance) { + mOffTake = x + PIXEL_SWIPE_OFFTAKE_SLOP; + return true; + } + loaded = true; + } + break; + case TOP: + if (deltaX < mPerpendicularDistance && deltaX > -mPerpendicularDistance + && y >= mOffTake) { + if (deltaY < mTriggerDistance) { + mOffTake = y - PIXEL_SWIPE_OFFTAKE_SLOP; + return true; + } + loaded = true; + } + break; + } + mActive = false; + if (loaded && mActivationListener != null) { + if (DEBUG) { + Slog.d(TAG, "activate at " + x + "," + y + " " + mPosition + " within " + + (SystemClock.uptimeMillis() - mDownTime) + "ms"); + } + mActivationListener.onActivation(motionEvent, x, y, mPosition); + } + return loaded; + } +} \ No newline at end of file diff --git a/services/java/com/android/server/pie/PieInputFilter.java b/services/java/com/android/server/pie/PieInputFilter.java new file mode 100644 index 0000000000000..00cfc14f81d8b --- /dev/null +++ b/services/java/com/android/server/pie/PieInputFilter.java @@ -0,0 +1,510 @@ +/* + * 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.pie; + +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.pie.PiePosition; +import com.android.server.pie.PieGestureTracker.OnActivationListener; + +import java.io.PrintWriter; + +/** + * A simple input filter, that listens for pie activation gestures in the motion event input + * stream. + *
+ * 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 pie 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 is encountered and the state is reset to LISTEN. + *
+ * If you are reading this within Java Doc, you are doing something wrong ;) + */ +public class PieInputFilter 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 = "PieInputFilter"; + 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 = true; + + 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; + } + private State mState = State.LISTEN; // guarded by mLock + private PieGestureTracker mTracker; // guarded by mLock + private volatile int mPositions; // 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 PieInputFilter(Context context, Handler handler) { + mHandler = handler; + + final Resources res = context.getResources(); + mTracker = new PieGestureTracker(res.getDimensionPixelSize(R.dimen.pie_trigger_thickness), + res.getDimensionPixelSize(R.dimen.pie_trigger_distance), + res.getDimensionPixelSize(R.dimen.pie_perpendicular_distance)); + mTracker.setOnActivationListener(new OnActivationListener() { + public void onActivation(MotionEvent event, int touchX, int touchY, PiePosition position) { + mHandler.obtainMessage(PieService.MSG_PIE_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 + public boolean unlockFilter() { + synchronized (mLock) { + if (mState == State.LOCKED) { + mState = State.SYNTHESIZE; + 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 + || !(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); + 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(); + sendSynthesizedMotionEvent(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 show up the pie + // in #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) { + mState = State.LISTEN; + mSyntheticDownTime = -1; + } + sendInputEvent(motionEvent, policyFlags); + 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; + } + sendMotionEventWithOffset(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 sendMotionEventWithOffset(MotionEvent event, int policyFlags, + long downTime, long offset) { + final int pointerCount = event.getPointerCount(); + PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); + PointerProperties[] properties = getTempPointerPropertiesWithMinSize(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[] getTempPointerCoordsWithMinSize(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[] getTempPointerPropertiesWithMinSize(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 sendSynthesizedMotionEvent(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, "Pie 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, "Pie 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"); + } + } +} diff --git a/services/java/com/android/server/pie/PieService.java b/services/java/com/android/server/pie/PieService.java new file mode 100644 index 0000000000000..52cfff44c96ad --- /dev/null +++ b/services/java/com/android/server/pie/PieService.java @@ -0,0 +1,469 @@ +/* + * 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.pie; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.os.Binder; +import android.os.IBinder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.RemoteException; +import android.service.pie.IPieActivationListener; +import android.service.pie.IPieHostCallback; +import android.service.pie.IPieService; +import android.util.Slog; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.util.pie.PiePosition; +import com.android.server.input.InputManagerService; +import com.android.server.wm.WindowManagerService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * A system service to track and handle pie activations gestures. This service interacts with + * the {@link InputManagerService} to do all the dirty work for pie controls: + *