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);