From 9f11bd11a1389bb98d54f6e6114028f4e39f90e1 Mon Sep 17 00:00:00 2001 From: Jens Doll Date: Wed, 17 Apr 2013 17:32:04 +0200 Subject: [PATCH] Pie controls: Introducing a pie delivery service To make pie controls more reliable, it is neccessary to detect trigger actions directly from the input stream. This commit introduces a new system service, that filters all input events in front of the input dispatcher to detect pie activations. This commit introduces: * A new system server local API in the input manager service to register secondary input filters. These filters are behind the default accessibility filter but before the input event dispatching of the android framework. * A new system service, that binds to the new API to listen for pie activation gestures. * A non-public manager class providing access to the newly created pie service. The service manager name of the service is "pieservice". The non-public AIDL interface of the service is IPieService.aidl. To register a new pie activation listener the INJECT_INPUT permission is needed. The service state can be dumped by the "dumpsys pieservice" command. Note: This commit only introduces the pie service. There is another commit, that binds the actual pie controls to the pie service. Patch Set #1: * The pie service is currently disabled by default and needs to be enabled by device overlays (see config.xml / config_allowPieService). Patch Set #2: * Activation fixes * Debug dump improvements Patch Set #4: * Added systrace support (TRACE_INPUT_TAG) * Switch default to enable service on all devices. * Moved Position to com.internal.android.utils.pie.* * Some more code rearrangements Patch Set #5: * Rebase Patch Set #6: * Cover more corner cases on PieInputFilter * Adjust gesture time out Patch Set #7: * Do not send events that are from the past * Recycle all events Patch Set #8: * Handle binder died events in PieService correctly Patch Set #10: * Simplified locking * SYSTEM_UI_FLAG_HIDE_NAVIGATION support * Fixed ADW Lauchner bug Change-Id: I6a4a4635bed420e800a3230457ee690131116a11 --- Android.mk | 3 + .../service/pie/IPieActivationListener.aidl | 14 + .../android/service/pie/IPieHostCallback.aidl | 15 + .../java/android/service/pie/IPieService.aidl | 20 + core/java/android/service/pie/PieManager.java | 193 +++++++ core/java/android/service/pie/package.html | 5 + .../internal/util/pie/PiePosition.java | 42 ++ core/res/res/values/config.xml | 3 + core/res/res/values/dimens.xml | 11 + core/res/res/values/symbols.xml | 6 + .../java/com/android/server/SystemServer.java | 21 + .../server/input/InputManagerService.java | 136 ++++- .../android/server/pie/PieGestureTracker.java | 221 ++++++++ .../android/server/pie/PieInputFilter.java | 510 ++++++++++++++++++ .../com/android/server/pie/PieService.java | 469 ++++++++++++++++ .../server/wm/WindowManagerService.java | 33 ++ 16 files changed, 1675 insertions(+), 27 deletions(-) create mode 100644 core/java/android/service/pie/IPieActivationListener.aidl create mode 100644 core/java/android/service/pie/IPieHostCallback.aidl create mode 100644 core/java/android/service/pie/IPieService.aidl create mode 100644 core/java/android/service/pie/PieManager.java create mode 100644 core/java/android/service/pie/package.html create mode 100644 core/java/com/android/internal/util/pie/PiePosition.java create mode 100644 services/java/com/android/server/pie/PieGestureTracker.java create mode 100644 services/java/com/android/server/pie/PieInputFilter.java create mode 100644 services/java/com/android/server/pie/PieService.java 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< -1 + + true diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 784a2cd7bc1ca..800d0d1744512 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -349,4 +349,15 @@ 10dp + + 10dp + + + 15dp + + + 6dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f0b00ff8c3711..f0f83a0dd45d2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1993,4 +1993,10 @@ + + + + + + diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b2ea6351bb1e7..1b2b65d3c87ce 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -62,6 +62,7 @@ import com.android.server.input.InputManagerService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.NetworkStatsService; +import com.android.server.pie.PieService; import com.android.server.pm.Installer; import com.android.server.pm.PackageManagerService; import com.android.server.pm.UserManagerService; @@ -383,6 +384,7 @@ public void run() { TextServicesManagerService tsms = null; LockSettingsService lockSettings = null; DreamManagerService dreamy = null; + PieService pieService = null; // Bring up services needed for UI. if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) { @@ -818,6 +820,17 @@ public void run() { } catch (Throwable e) { Slog.e(TAG, "Failure starting AssetRedirectionManager Service", e); } + + if (context.getResources().getBoolean( + com.android.internal.R.bool.config_allowPieService)) { + try { + Slog.i(TAG, "Pie Delivery Service"); + pieService = new PieService(context, wm, inputManager); + ServiceManager.addService("pieservice", pieService); + } catch (Throwable e) { + Slog.e(TAG, "Failure starting Pie Delivery Service Service", e); + } + } } // make sure the ADB_ENABLED setting value matches the secure property value @@ -910,6 +923,14 @@ public void run() { reportWtf("making Display Manager Service ready", e); } + if (pieService != null) { + try { + pieService.systemReady(); + } catch (Throwable e) { + reportWtf("making Pie Delivery Service ready", e); + } + } + IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_APP_LAUNCH_FAILURE); filter.addAction(Intent.ACTION_APP_LAUNCH_FAILURE_RESET); diff --git a/services/java/com/android/server/input/InputManagerService.java b/services/java/com/android/server/input/InputManagerService.java index 9921b5c8d6d99..aa3b28360a7dc 100644 --- a/services/java/com/android/server/input/InputManagerService.java +++ b/services/java/com/android/server/input/InputManagerService.java @@ -97,6 +97,7 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog.Monitor, DisplayManagerService.InputManagerFuncs { static final String TAG = "InputManager"; static final boolean DEBUG = false; + static final boolean DEBUG_FILTER = false; private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml"; @@ -143,8 +144,9 @@ public class InputManagerService extends IInputManager.Stub // State for the currently installed input filter. final Object mInputFilterLock = new Object(); - IInputFilter mInputFilter; // guarded by mInputFilterLock - InputFilterHost mInputFilterHost; // guarded by mInputFilterLock + ChainedInputFilterHost mInputFilterHost; // guarded by mInputFilterLock + ArrayList mInputFilterChain = + new ArrayList(); // guarded by mInputFilterLock private static native int nativeInit(InputManagerService service, Context context, MessageQueue messageQueue); @@ -475,34 +477,77 @@ public void unregisterInputChannel(InputChannel inputChannel) { */ public void setInputFilter(IInputFilter filter) { synchronized (mInputFilterLock) { - final IInputFilter oldFilter = mInputFilter; + final IInputFilter oldFilter = mInputFilterHost != null + ? mInputFilterHost.mInputFilter : null; if (oldFilter == filter) { return; // nothing to do } if (oldFilter != null) { - mInputFilter = null; mInputFilterHost.disconnectLocked(); + mInputFilterChain.remove(mInputFilterHost); mInputFilterHost = null; - try { - oldFilter.uninstall(); - } catch (RemoteException re) { - /* ignore */ - } } if (filter != null) { - mInputFilter = filter; - mInputFilterHost = new InputFilterHost(); - try { - filter.install(mInputFilterHost); - } catch (RemoteException re) { - /* ignore */ + ChainedInputFilterHost head = mInputFilterChain.isEmpty() ? null : + mInputFilterChain.get(0); + mInputFilterHost = new ChainedInputFilterHost(filter, head); + mInputFilterHost.connectLocked(); + mInputFilterChain.add(0, mInputFilterHost); + } + + nativeSetInputFilterEnabled(mPtr, !mInputFilterChain.isEmpty()); + } + } + + /** + * Registers a secondary input filter. These filters are always behind the "original" + * input filter. This ensures that all input events will be filtered by the + * {@code AccessibilityManagerService} first. + *

+ * 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: + *

  • Installing an input filter and listen for pie activation gestures
  • + *
  • Removing those gestures from the input stream
  • + *
  • Transferring touch focus to the pie controls when shown
  • + */ +public class PieService extends IPieService.Stub { + public static final String TAG = "PieService"; + public static final boolean DEBUG = false; + public static final boolean DEBUG_INPUT = false; + + public static final int MSG_PIE_ACTIVATION = 32023; + public static final int MSG_PIE_DEACTIVATION = 32024; + public static final int MSG_UPDATE_POSITIONS = 32025; + + private final Context mContext; + private final InputManagerService mInputManager; + private final WindowManagerService mWindowManager; + + private HandlerThread mHandlerThread = new HandlerThread("Pie"); + + // Lock for thread, handler, mInputFilter, activations and listener related variables + private final Object mLock = new Object(); + private Handler mHandler; + private PieInputFilter mInputFilter; + + private int mGlobalPositions = 0; + private boolean mIsMonitoring = false; + + private final class PieActivationListenerRecord extends IPieHostCallback.Stub implements DeathRecipient { + private boolean mActive; + + public PieActivationListenerRecord(IPieActivationListener listener) { + this.listener = listener; + this.positions = 0; + } + + public void binderDied() { + removeListenerRecord(this); + } + + public void updatePositions(int positions) { + this.positions = positions; + } + + public boolean eligibleForActivation(int positionFlag) { + return (positions & positionFlag) != 0; + } + + public boolean notifyPieActivation(int touchX, int touchY, PiePosition position) { + if ((positions & position.FLAG) != 0) { + try { + listener.onPieActivation(touchX, touchY, position.INDEX, 0); + mActive = true; + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify process, assuming it died.", e); + binderDied(); + } + } + return mActive; + } + + // called through Binder + public boolean gainTouchFocus(IBinder windowToken) { + if (DEBUG) { + Slog.d(TAG, "Gain touch focus for " + windowToken); + } + if (mActive) { + return mInputFilter.unlockFilter(); + } + return false; + } + + // called through Binder + public void restoreListenerState() throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "Restore listener state"); + } + if (mActive) { + mWindowManager.resetStatusBarVisibilityMask(); + mInputFilter.unlockFilter(); + mActive = false; + synchronized (mLock) { + mActiveRecord = null; + mHandler.obtainMessage(MSG_PIE_DEACTIVATION, mGlobalPositions, 0).sendToTarget(); + } + } + } + + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); + pw.print("mPositions=0x" + Integer.toHexString(positions)); + pw.println(" mActive=" + mActive); + pw.print(prefix); + pw.println("mBinder=" + listener); + } + + public int positions; + public final IPieActivationListener listener; + } + private final List mPieActivationListener = + new ArrayList(); + private PieActivationListenerRecord mActiveRecord = null; + // end of lock guarded variables + + private DisplayObserver mDisplayObserver; + + // called by system server + public PieService(Context context, WindowManagerService windowManager, InputManagerService inputManager) { + mContext = context; + mInputManager = inputManager; + mWindowManager = windowManager; + } + + // called by system server + public void systemReady() { + if (DEBUG) Slog.d(TAG, "Starting the pie gesture capture thread ..."); + + mHandlerThread.start(); + mHandler = new H(mHandlerThread.getLooper()); + mHandler.post(new Runnable() { + @Override + public void run() { + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_FOREGROUND); + android.os.Process.setCanSelfBackground(false); + } + }); + mDisplayObserver = new DisplayObserver(mContext, mHandler); + // check if anyone registered during startup + mHandler.obtainMessage(MSG_UPDATE_POSITIONS, mGlobalPositions, 0).sendToTarget(); + updateMonitoring(); + } + + private void enforceMonitoringLocked() { + if (DEBUG) { + Slog.d(TAG, "Attempting to start monitoring input events ..."); + } + if (mInputFilter == null) { + mInputFilter = new PieInputFilter(mContext, mHandler); + mInputManager.registerSecondaryInputFilter(mInputFilter); + } + mDisplayObserver.observe(); + } + + private void shutdownMonitoringLocked() { + if (DEBUG) { + Slog.d(TAG, "Shutting down monitoring input events ..."); + } + mDisplayObserver.unobserve(); + if (mInputFilter != null) { + mInputManager.unregisterSecondaryInputFilter(mInputFilter); + mInputFilter = null; + } + } + + private void updateMonitoring() { + synchronized(mLock) { + if (!mIsMonitoring && mGlobalPositions != 0) { + enforceMonitoringLocked(); + } else if (mIsMonitoring && mGlobalPositions == 0) { + shutdownMonitoringLocked(); + } + mIsMonitoring = mGlobalPositions != 0; + } + } + + // called through Binder + public IPieHostCallback registerPieActivationListener(IPieActivationListener listener) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.INJECT_EVENTS) + != PackageManager.PERMISSION_GRANTED) { + Slog.w(TAG, "Permission Denial: can't register from from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); + return null; + } + + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + PieActivationListenerRecord record = null; + synchronized(mLock) { + record = findListenerRecordLocked(listener.asBinder()); + if (record == null) { + record = new PieActivationListenerRecord(listener); + mPieActivationListener.add(record); + } + } + return record; + } + + // called through Binder + public void updatePieActivationListener(IBinder listener, int positionFlags) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + synchronized(mLock) { + PieActivationListenerRecord record = findListenerRecordLocked(listener); + if (record == null) { + Slog.w(TAG, "Unknown listener on update listener. Register first?"); + throw new IllegalStateException("listener not registered"); + } + record.updatePositions(positionFlags); + updatePositionsLocked(); + if (mActiveRecord == null && mHandler != null) { + mHandler.obtainMessage(MSG_UPDATE_POSITIONS, mGlobalPositions, 0).sendToTarget(); + } + } + updateMonitoring(); + } + + private PieActivationListenerRecord findListenerRecordLocked(IBinder listener) { + for (PieActivationListenerRecord record : mPieActivationListener) { + if (record.listener.asBinder().equals(listener)) { + return record; + } + } + return null; + } + + private void updatePositionsLocked() { + mGlobalPositions = 0; + for (PieActivationListenerRecord temp : mPieActivationListener) { + mGlobalPositions |= temp.positions; + } + } + + private void removeListenerRecord(PieActivationListenerRecord record) { + synchronized(mLock) { + mPieActivationListener.remove(record); + updatePositionsLocked(); + // check if the record was the active one + if (record == mActiveRecord) { + mHandler.obtainMessage(MSG_PIE_DEACTIVATION, mGlobalPositions, 0).sendToTarget(); + } + } + updateMonitoring(); + } + + // called by handler thread + private boolean propagateActivation(int touchX, int touchY, PiePosition position) { + if (mActiveRecord != null) { + Slog.w(TAG, "Handing activition while another activition is still in progress"); + } + if (!mWindowManager.updateStatusBarVisibilityMask(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)) { + return false; + } + synchronized(mLock) { + PieActivationListenerRecord target = null; + for (PieActivationListenerRecord record : mPieActivationListener) { + if (record.eligibleForActivation(position.FLAG)) { + target = record; + break; + } + } + // NOTE: We can do this here because the #onPieActivation() is a oneway + // Binder call. This means we do not block with holding the mListenerLock!!! + // If this ever change, this needs to be adjusted and if you don't know what + // this means, you should probably not mess around with this code, anyway. + if (target != null && target.notifyPieActivation(touchX, touchY, position)) { + mActiveRecord = target; + } + } + if (mActiveRecord != null) { + mWindowManager.reevaluateStatusBarVisibility(); + } else { + mWindowManager.resetStatusBarVisibilityMask(); + } + return mActiveRecord != null; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + // let's log all exceptions we do not know about. + if (!(e instanceof IllegalArgumentException || e instanceof IllegalStateException)) { + Slog.e(TAG, "PieService crashed: ", e); + } + throw e; + } + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump PieService from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("PIE SERVICE (dumpsys pieservice)\n"); + synchronized(mLock) { + pw.println(" mIsMonitoring=" + mIsMonitoring); + pw.println(" mInputFilter=" + mInputFilter); + if (mInputFilter != null) { + mInputFilter.dump(pw, " "); + } + pw.println(" mGlobalPositions=0x" + Integer.toHexString(mGlobalPositions)); + int i = 0; + for (PieActivationListenerRecord record : mPieActivationListener) { + if (record == mActiveRecord) break; + i++; + } + pw.println(" mActiveRecord=" + (mActiveRecord != null ? ("#" + i) : "null")); + i = 0; + for (PieActivationListenerRecord record : mPieActivationListener) { + pw.println(" Listener #" + i + ":"); + record.dump(pw, " "); + i++; + } + } + } + + private final class H extends Handler { + public H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message m) { + switch (m.what) { + case MSG_PIE_ACTIVATION: + if (DEBUG) { + Slog.d(TAG, "Activating pie on " + m.obj.toString()); + } + // Since input filter runs asynchronously to us, double activation may happen + // theoretically. Take the safe route here. + removeMessages(MSG_PIE_ACTIVATION); + if (propagateActivation(m.arg1, m.arg2, (PiePosition) m.obj)) { + // switch off all positions for the time of activation + updatePositionsHandler(0); + } + break; + case MSG_PIE_DEACTIVATION: + if (DEBUG) { + Slog.d(TAG, "Deactivating pie with positions 0x" + Integer.toHexString(m.arg1)); + } + // switch back on the positions we need + updatePositionsHandler(m.arg1); + break; + case MSG_UPDATE_POSITIONS: + if (DEBUG) { + Slog.d(TAG, "Updating positions 0x" + Integer.toHexString(m.arg1)); + } + updatePositionsHandler(m.arg1); + } + } + + private void updatePositionsHandler(int positions) { + synchronized (mLock) { + if (mInputFilter != null) { + mInputFilter.updatePositions(positions); + } + } + } + } + + private final class DisplayObserver implements DisplayListener { + private final Handler mHandler; + private final DisplayManager mDisplayManager; + + private final Display mDefaultDisplay; + private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo(); + + public DisplayObserver(Context context, Handler handler) { + mHandler = handler; + mDisplayManager = (DisplayManager) context.getSystemService( + Context.DISPLAY_SERVICE); + final WindowManager windowManager = (WindowManager) context.getSystemService( + Context.WINDOW_SERVICE); + + mDefaultDisplay = windowManager.getDefaultDisplay(); + updateDisplayInfo(); + } + + private void updateDisplayInfo() { + if (DEBUG) { + Slog.d(TAG, "Updating display information ..."); + } + if (mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) { + synchronized (mLock) { + if (mInputFilter != null) { + mInputFilter.updateDisplay(mDefaultDisplay, mDefaultDisplayInfo); + } + } + } else { + Slog.e(TAG, "Default display is not valid."); + } + } + + public void observe() { + mDisplayManager.registerDisplayListener(this, mHandler); + updateDisplayInfo(); + } + + public void unobserve() { + mDisplayManager.unregisterDisplayListener(this); + } + + @Override + public void onDisplayAdded(int displayId) { + /* do noting */ + } + + @Override + public void onDisplayRemoved(int displayId) { + /* do nothing */ + } + + @Override + public void onDisplayChanged(int displayId) { + updateDisplayInfo(); + } + } +} diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index 4133bbd9463e9..a385a7abf508f 100644 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -487,6 +487,12 @@ public void onReceive(Context context, Intent intent) { int mLastStatusBarVisibility = 0; + /** + * Mask used to control the visibility of the status and navigation bar for short periods + * of time. (e.g. during pie controls) + */ + int mStatusBarVisibilityMask = 0; + // State while inside of layoutAndPlaceSurfacesLocked(). boolean mFocusMayChange; @@ -10394,6 +10400,7 @@ public void statusBarVisibilityChanged(int visibility) { synchronized (mWindowMap) { mLastStatusBarVisibility = visibility; visibility = mPolicy.adjustSystemUiVisibilityLw(visibility); + visibility &= ~mStatusBarVisibilityMask; updateStatusBarVisibilityLocked(visibility); } } @@ -10432,6 +10439,7 @@ void updateStatusBarVisibilityLocked(int visibility) { public void reevaluateStatusBarVisibility() { synchronized (mWindowMap) { int visibility = mPolicy.adjustSystemUiVisibilityLw(mLastStatusBarVisibility); + visibility &= ~mStatusBarVisibilityMask; updateStatusBarVisibilityLocked(visibility); performLayoutAndPlaceSurfacesLocked(); } @@ -10526,6 +10534,31 @@ public void updateDisplayMetrics() { Binder.restoreCallingIdentity(origId); } + /** + * Tries to set the status bar visibilty mask. This will fail if the mask was set already. + * + * @param mask specifies the positive mask. E.g. all bit that should be masked out are set. + */ + public boolean updateStatusBarVisibilityMask(int mask) { + boolean result = false; + synchronized(mWindowMap) { + if (mStatusBarVisibilityMask == 0) { + mStatusBarVisibilityMask = mask; + result = true; + } + } + return result; + } + + /** + * Call this, only if {@link #updateStatusBarVisibilityMask(int)} returned {@code true}. + */ + public void resetStatusBarVisibilityMask() { + synchronized(mWindowMap) { + mStatusBarVisibilityMask = 0; + } + } + void dumpPolicyLocked(PrintWriter pw, String[] args, boolean dumpAll) { pw.println("WINDOW MANAGER POLICY STATE (dumpsys window policy)"); mPolicy.dump(" ", pw, args);