diff options
author | Svetoslav <svetoslavganov@google.com> | 2013-04-09 12:58:41 -0700 |
---|---|---|
committer | Svetoslav <svetoslavganov@google.com> | 2013-04-11 16:00:08 -0700 |
commit | c4fccd183f1bb47a027bb303af5e65bec2f68b1b (patch) | |
tree | 1a5534c102b1f22f84e3126a719b0f1c0e135845 | |
parent | dbf500aaafd0889aa3ac9bf0fb2b2be4e0c3ebbf (diff) | |
download | frameworks_base-c4fccd183f1bb47a027bb303af5e65bec2f68b1b.zip frameworks_base-c4fccd183f1bb47a027bb303af5e65bec2f68b1b.tar.gz frameworks_base-c4fccd183f1bb47a027bb303af5e65bec2f68b1b.tar.bz2 |
Adding APIs for an accessibility service to intercept key events.
Now that we have gestures which are detected by the system and
interpreted by an accessibility service, there is an inconsistent
behavior between using the gestures and the keyboard. Some devices
have both. Therefore, an accessibility service should be able to
interpret keys in addition to gestures to provide consistent user
experience. Now an accessibility service can expose shortcuts for
each gestural action.
This change adds APIs for an accessibility service to observe and
intercept at will key events before they are dispatched to the
rest of the system. The service can return true or false from its
onKeyEvent to either consume the event or to let it be delivered
to the rest of the system. However, the service will *not* be
able to inject key events or modify the observed ones.
Previous ideas of allowing the service to say it "tracks" the event
so the latter is not delivered to the system until a subsequent
event is either "handled" or "not handled" will not work. If the
service tracks a key but no other key is pressed essentially this
key is not delivered to the app and at potentially much later point
this stashed event will be delivered in maybe a completely different
context.The correct way of implementing shortcuts is a combination
of modifier keys plus some other key/key sequence. Key events already
contain information about which modifier keys are down as well as
the service can track them as well.
bug:8088812
Change-Id: I81ba9a7de9f19ca6662661f27fdc852323e38c00
8 files changed, 410 insertions, 174 deletions
diff --git a/api/current.txt b/api/current.txt index 1e1f01c..58391af 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2074,6 +2074,7 @@ package android.accessibilityservice { method public final android.os.IBinder onBind(android.content.Intent); method protected boolean onGesture(int); method public abstract void onInterrupt(); + method protected boolean onKeyEvent(android.view.KeyEvent); method protected void onServiceConnected(); method public final boolean performGlobalAction(int); method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 811b92a..31de98d 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -24,6 +24,7 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.view.KeyEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; @@ -348,6 +349,7 @@ public abstract class AccessibilityService extends Service { public void onServiceConnected(); public void onSetConnectionId(int connectionId); public boolean onGesture(int gestureId); + public boolean onKeyEvent(KeyEvent event); } private int mConnectionId; @@ -413,6 +415,32 @@ public abstract class AccessibilityService extends Service { } /** + * Callback that allows an accessibility service to observe the key events + * before they are passed to the rest of the system. This means that the events + * are first delivered here before they are passed to the device policy, the + * input method, or applications. + * <p> + * <strong>Note:</strong> It is important that key events are handled in such + * a way that the event stream that would be passed to the rest of the system + * is well-formed. For example, handling the down event but not the up event + * and vice versa would generate an inconsistent event stream. + * </p> + * <p> + * <strong>Note:</strong> The key events delivered in this method are copies + * and modifying them will have no effect on the events that will be passed + * to the system. This method is intended to perform purely filtering + * functionality. + * <p> + * + * @param event The event to be processed. + * @return If true then the event will be consumed and not delivered to + * applications, otherwise it will be delivered as usual. + */ + protected boolean onKeyEvent(KeyEvent event) { + return false; + } + + /** * Gets the root node in the currently active window if this service * can retrieve window content. * @@ -535,6 +563,11 @@ public abstract class AccessibilityService extends Service { public boolean onGesture(int gestureId) { return AccessibilityService.this.onGesture(gestureId); } + + @Override + public boolean onKeyEvent(KeyEvent event) { + return AccessibilityService.this.onKeyEvent(event); + } }); } @@ -554,11 +587,14 @@ public abstract class AccessibilityService extends Service { private static final int DO_ON_ACCESSIBILITY_EVENT = 30; private static final int DO_ON_GESTURE = 40; private static final int DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 50; + private static final int DO_ON_KEY_EVENT = 60; private final HandlerCaller mCaller; private final Callbacks mCallback; + private int mConnectionId; + public IAccessibilityServiceClientWrapper(Context context, Looper looper, Callbacks callback) { mCallback = callback; @@ -591,41 +627,65 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } + @Override + public void onKeyEvent(KeyEvent event, int sequence) { + Message message = mCaller.obtainMessageIO(DO_ON_KEY_EVENT, sequence, event); + mCaller.sendMessage(message); + } + public void executeMessage(Message message) { switch (message.what) { - case DO_ON_ACCESSIBILITY_EVENT : + case DO_ON_ACCESSIBILITY_EVENT: { AccessibilityEvent event = (AccessibilityEvent) message.obj; if (event != null) { AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event); mCallback.onAccessibilityEvent(event); event.recycle(); } - return; - case DO_ON_INTERRUPT : + } return; + case DO_ON_INTERRUPT: { mCallback.onInterrupt(); - return; - case DO_SET_SET_CONNECTION : - final int connectionId = message.arg1; + } return; + case DO_SET_SET_CONNECTION: { + mConnectionId = message.arg1; IAccessibilityServiceConnection connection = (IAccessibilityServiceConnection) message.obj; if (connection != null) { - AccessibilityInteractionClient.getInstance().addConnection(connectionId, + AccessibilityInteractionClient.getInstance().addConnection(mConnectionId, connection); - mCallback.onSetConnectionId(connectionId); + mCallback.onSetConnectionId(mConnectionId); mCallback.onServiceConnected(); } else { - AccessibilityInteractionClient.getInstance().removeConnection(connectionId); + AccessibilityInteractionClient.getInstance().removeConnection(mConnectionId); AccessibilityInteractionClient.getInstance().clearCache(); mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } - return; - case DO_ON_GESTURE : + } return; + case DO_ON_GESTURE: { final int gestureId = message.arg1; mCallback.onGesture(gestureId); - return; - case DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: + } return; + case DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { AccessibilityInteractionClient.getInstance().clearCache(); - return; + } return; + case DO_ON_KEY_EVENT: { + KeyEvent event = (KeyEvent) message.obj; + try { + IAccessibilityServiceConnection connection = AccessibilityInteractionClient + .getInstance().getConnection(mConnectionId); + if (connection != null) { + final boolean result = mCallback.onKeyEvent(event); + final int sequence = message.arg1; + try { + connection.setOnKeyEventResult(result, sequence); + } catch (RemoteException re) { + /* ignore */ + } + } + } finally { + event.recycle(); + } + } return; default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 5d684e3..c5e3d43a 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -18,6 +18,7 @@ package android.accessibilityservice; import android.accessibilityservice.IAccessibilityServiceConnection; import android.view.accessibility.AccessibilityEvent; +import android.view.KeyEvent; /** * Top-level interface to an accessibility service component. @@ -35,4 +36,6 @@ import android.view.accessibility.AccessibilityEvent; void onGesture(int gesture); void clearAccessibilityNodeInfoCache(); + + void onKeyEvent(in KeyEvent event, int sequence); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7a29f35..3df06b5 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -31,144 +31,31 @@ interface IAccessibilityServiceConnection { void setServiceInfo(in AccessibilityServiceInfo info); - /** - * Finds an {@link android.view.accessibility.AccessibilityNodeInfo} by accessibility id. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param flags Additional flags. - * @param threadId The id of the calling thread. - * @return Whether the call succeeded. - */ boolean findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, long threadId); - /** - * Finds {@link android.view.accessibility.AccessibilityNodeInfo}s by View text. - * The match is case insensitive containment. The search is performed in the window - * whose id is specified and starts from the node whose accessibility id is specified. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param text The searched text. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param threadId The id of the calling thread. - * @return Whether the call succeeded. - */ boolean findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - /** - * Finds an {@link android.view.accessibility.AccessibilityNodeInfo} by View id. The search - * is performed in the window whose id is specified and starts from the node whose - * accessibility id is specified. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param viewId The fully qualified resource name of the view id to find. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param threadId The id of the calling thread. - * @return Whether the call succeeded. - */ boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, long accessibilityNodeId, String viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - /** - * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the specified - * focus type. The search is performed in the window whose id is specified and starts from - * the node whose accessibility id is specified. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param focusType The type of focus to find. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param threadId The id of the calling thread. - * @return Whether the call succeeded. - */ boolean findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - /** - * Finds an {@link android.view.accessibility.AccessibilityNodeInfo} to take accessibility - * focus in the given direction. The search is performed in the window whose id is - * specified and starts from the node whose accessibility id is specified. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param direction The direction in which to search for focusable. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param threadId The id of the calling thread. - * @return Whether the call succeeded. - */ boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - /** - * Performs an accessibility action on an - * {@link android.view.accessibility.AccessibilityNodeInfo}. - * - * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. - * @param accessibilityNodeId A unique view id or virtual descendant id from - * where to start the search. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} - * to start from the root. - * @param action The action to perform. - * @param arguments Optional action arguments. - * @param interactionId The id of the interaction for matching with the callback result. - * @param callback Callback which to receive the result. - * @param threadId The id of the calling thread. - * @return Whether the action was performed. - */ boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, in Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - /** - * @return The associated accessibility service info. - */ AccessibilityServiceInfo getServiceInfo(); - /** - * Performs a global action, such as going home, going back, etc. - * - * @param action The action to perform. - * @return Whether the action was performed. - */ boolean performGlobalAction(int action); + + oneway void setOnKeyEventResult(boolean handled, int sequence); } diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 7d02342..d9799b6 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -31,6 +31,7 @@ import android.os.SystemClock; import android.util.Log; import android.view.Display; import android.view.InputEvent; +import android.view.KeyEvent; import android.view.Surface; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; @@ -693,6 +694,11 @@ public final class UiAutomation { listener.onAccessibilityEvent(AccessibilityEvent.obtain(event)); } } + + @Override + public boolean onKeyEvent(KeyEvent event) { + return false; + } }); } } diff --git a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java index 179db12..0d8a571 100644 --- a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -25,6 +25,7 @@ import android.view.Display; import android.view.InputDevice; import android.view.InputEvent; import android.view.InputFilter; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.WindowManagerPolicy; import android.view.accessibility.AccessibilityEvent; @@ -80,7 +81,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private final Choreographer mChoreographer; - private int mCurrentDeviceId; + private int mCurrentTouchDeviceId; private boolean mInstalled; @@ -98,6 +99,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private boolean mHoverEventSequenceStarted; + private boolean mKeyEventSequenceStarted; + AccessibilityInputFilter(Context context, AccessibilityManagerService service) { super(context.getMainLooper()); mContext = context; @@ -133,11 +136,21 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo Slog.d(TAG, "Received event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); } - if (mEventHandler == null) { + if (event instanceof MotionEvent + && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { + MotionEvent motionEvent = (MotionEvent) event; + onMotionEvent(motionEvent, policyFlags); + } else if (event instanceof KeyEvent + && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) { + KeyEvent keyEvent = (KeyEvent) event; + onKeyEvent(keyEvent, policyFlags); + } else { super.onInputEvent(event, policyFlags); - return; } - if (event.getSource() != InputDevice.SOURCE_TOUCHSCREEN) { + } + + private void onMotionEvent(MotionEvent event, int policyFlags) { + if (mEventHandler == null) { super.onInputEvent(event, policyFlags); return; } @@ -149,26 +162,25 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo return; } final int deviceId = event.getDeviceId(); - if (mCurrentDeviceId != deviceId) { + if (mCurrentTouchDeviceId != deviceId) { + mCurrentTouchDeviceId = deviceId; mMotionEventSequenceStarted = false; mHoverEventSequenceStarted = false; mEventHandler.clear(); - mCurrentDeviceId = deviceId; } - if (mCurrentDeviceId < 0) { + if (mCurrentTouchDeviceId < 0) { super.onInputEvent(event, policyFlags); return; } // We do not handle scroll events. - MotionEvent motionEvent = (MotionEvent) event; - if (motionEvent.getActionMasked() == MotionEvent.ACTION_SCROLL) { + if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { super.onInputEvent(event, policyFlags); return; } // Wait for a down touch event to start processing. - if (motionEvent.isTouchEvent()) { + if (event.isTouchEvent()) { if (!mMotionEventSequenceStarted) { - if (motionEvent.getActionMasked() != MotionEvent.ACTION_DOWN) { + if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { return; } mMotionEventSequenceStarted = true; @@ -176,7 +188,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo } else { // Wait for an enter hover event to start processing. if (!mHoverEventSequenceStarted) { - if (motionEvent.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) { + if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) { return; } mHoverEventSequenceStarted = true; @@ -185,6 +197,22 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo batchMotionEvent((MotionEvent) event, policyFlags); } + private void onKeyEvent(KeyEvent event, int policyFlags) { + if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { + mKeyEventSequenceStarted = false; + super.onInputEvent(event, policyFlags); + return; + } + // Wait for a down key event to start processing. + if (!mKeyEventSequenceStarted) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + mKeyEventSequenceStarted = true; + } + mAms.notifyKeyEvent(event, policyFlags); + } + private void scheduleProcessBatchedEvents() { mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mProcessBatchedEventsRunnable, null); @@ -286,6 +314,13 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo } } + void reset() { + setEnabledFeatures(0); + mKeyEventSequenceStarted = false; + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + } + private void enableFeatures() { mMotionEventSequenceStarted = false; mHoverEventSequenceStarted = false; diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index 527e891..110c4da 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -61,16 +61,20 @@ import android.os.UserManager; import android.provider.Settings; import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; +import android.util.Pools.Pool; +import android.util.Pools.SimplePool; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.IWindow; import android.view.IWindowManager; import android.view.InputDevice; +import android.view.InputEventConsistencyVerifier; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MagnificationSpec; import android.view.WindowManager; +import android.view.WindowManagerPolicy; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; @@ -132,6 +136,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private static final int OWN_PROCESS_ID = android.os.Process.myPid(); + private static final int MAX_POOL_SIZE = 10; + private static int sIdCounter = 0; private static int sNextWindowId; @@ -140,6 +146,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private final Object mLock = new Object(); + private final Pool<PendingEvent> mPendingEventPool = + new SimplePool<PendingEvent>(MAX_POOL_SIZE); + private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); @@ -633,6 +642,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + boolean notifyKeyEvent(KeyEvent event, int policyFlags) { + synchronized (mLock) { + KeyEvent localClone = KeyEvent.obtain(event); + boolean handled = notifyKeyEventLocked(localClone, policyFlags, false); + if (!handled) { + handled = notifyKeyEventLocked(localClone, policyFlags, true); + } + return handled; + } + } + /** * Gets the bounds of the accessibility focus in the active window. * @@ -798,6 +818,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { return false; } + private boolean notifyKeyEventLocked(KeyEvent event, int policyFlags, boolean isDefault) { + // TODO: Now we are giving the key events to the last enabled + // service that can handle them which is the last one + // in our list since we write the last enabled as the + // last record in the enabled services setting. Ideally, + // the user should make the call which service handles + // key events. However, only one service should handle + // key events to avoid user frustration when different + // behavior is observed from different combinations of + // enabled accessibility services. + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + if (service.mIsDefault == isDefault) { + service.notifyKeyEvent(event, policyFlags); + return true; + } + } + return false; + } + private void notifyClearAccessibilityNodeInfoCacheLocked() { UserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { @@ -1119,8 +1160,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { boolean setInputFilter = false; AccessibilityInputFilter inputFilter = null; synchronized (mLock) { - if ((userState.mIsAccessibilityEnabled && userState.mIsTouchExplorationEnabled) - || userState.mIsDisplayMagnificationEnabled) { + if (userState.mIsAccessibilityEnabled) { if (!mHasInputFilter) { mHasInputFilter = true; if (mInputFilter == null) { @@ -1141,7 +1181,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } else { if (mHasInputFilter) { mHasInputFilter = false; - mInputFilter.setEnabledFeatures(0); + mInputFilter.reset(); inputFilter = null; setInputFilter = true; } @@ -1446,6 +1486,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public static final int MSG_ANNOUNCE_NEW_USER_IF_NEEDED = 5; public static final int MSG_UPDATE_INPUT_FILTER = 6; public static final int MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG = 7; + public static final int MSG_SEND_KEY_EVENT_TO_INPUT_FILTER = 8; public MainHandler(Looper looper) { super(looper); @@ -1464,6 +1505,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } event.recycle(); } break; + case MSG_SEND_KEY_EVENT_TO_INPUT_FILTER: { + KeyEvent event = (KeyEvent) msg.obj; + final int policyFlags = msg.arg1; + synchronized (mLock) { + if (mHasInputFilter && mInputFilter != null) { + mInputFilter.sendInputEvent(event, policyFlags); + } + } + event.recycle(); + } break; case MSG_SEND_STATE_TO_CLIENTS: { final int clientState = msg.arg1; final int userId = msg.arg2; @@ -1536,6 +1587,22 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private PendingEvent obtainPendingEventLocked(KeyEvent event, int policyFlags, int sequence) { + PendingEvent pendingEvent = mPendingEventPool.acquire(); + if (pendingEvent == null) { + pendingEvent = new PendingEvent(); + } + pendingEvent.event = event; + pendingEvent.policyFlags = policyFlags; + pendingEvent.sequence = sequence; + return pendingEvent; + } + + private void recyclePendingEventLocked(PendingEvent pendingEvent) { + pendingEvent.clear(); + mPendingEventPool.release(pendingEvent); + } + /** * This class represents an accessibility service. It stores all per service * data required for the service management, provides API for starting/stopping the @@ -1545,12 +1612,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { * connection for the service. */ class Service extends IAccessibilityServiceConnection.Stub - implements ServiceConnection, DeathRecipient { - - // We pick the MSBs to avoid collision since accessibility event types are - // used as message types allowing us to remove messages per event type. - private static final int MSG_ON_GESTURE = 0x80000000; - private static final int MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 0x40000000; + implements ServiceConnection, DeathRecipient {; final int mUserId; @@ -1594,29 +1656,22 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final SparseArray<AccessibilityEvent> mPendingEvents = new SparseArray<AccessibilityEvent>(); - /** - * Handler for delayed event dispatch. - */ - public Handler mHandler = new Handler(mMainHandler.getLooper()) { + final KeyEventDispatcher mKeyEventDispatcher = new KeyEventDispatcher(); + + // Handler only for dispatching accessibility events since we use event + // types as message types allowing us to remove messages per event type. + public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) { @Override public void handleMessage(Message message) { - final int type = message.what; - switch (type) { - case MSG_ON_GESTURE: { - final int gestureId = message.arg1; - notifyGestureInternal(gestureId); - } break; - case MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { - notifyClearAccessibilityNodeInfoCacheInternal(); - } break; - default: { - final int eventType = type; - notifyAccessibilityEventInternal(eventType); - } break; - } + final int eventType = message.what; + notifyAccessibilityEventInternal(eventType); } }; + // Handler for scheduling method invocations on the main thread. + public InvocationHandler mInvocationHandler = new InvocationHandler( + mMainHandler.getLooper()); + public Service(int userId, ComponentName componentName, AccessibilityServiceInfo accessibilityServiceInfo) { mUserId = userId; @@ -1703,6 +1758,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { return false; } UserState userState = getUserStateLocked(mUserId); + mKeyEventDispatcher.flush(); if (!mIsAutomation) { mContext.unbindService(this); } else { @@ -1718,6 +1774,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } @Override + public void setOnKeyEventResult(boolean handled, int sequence) { + mKeyEventDispatcher.setOnKeyEventResult(handled, sequence); + } + + @Override public AccessibilityServiceInfo getServiceInfo() { synchronized (mLock) { return mAccessibilityServiceInfo; @@ -2109,6 +2170,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public void binderDied() { synchronized (mLock) { + mKeyEventDispatcher.flush(); UserState userState = getUserStateLocked(mUserId); // The death recipient is unregistered in removeServiceLocked removeServiceLocked(this, userState); @@ -2141,12 +2203,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final int what = eventType; if (oldEvent != null) { - mHandler.removeMessages(what); + mEventDispatchHandler.removeMessages(what); oldEvent.recycle(); } - Message message = mHandler.obtainMessage(what); - mHandler.sendMessageDelayed(message, mNotificationTimeout); + Message message = mEventDispatchHandler.obtainMessage(what); + mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); } } @@ -2211,11 +2273,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } public void notifyGesture(int gestureId) { - mHandler.obtainMessage(MSG_ON_GESTURE, gestureId, 0).sendToTarget(); + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, + gestureId, 0).sendToTarget(); + } + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_KEY_EVENT, + policyFlags, 0, event).sendToTarget(); } public void notifyClearAccessibilityNodeInfoCache() { - mHandler.sendEmptyMessage(MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); + mInvocationHandler.sendEmptyMessage( + InvocationHandler.MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); } private void notifyGestureInternal(int gestureId) { @@ -2230,6 +2299,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private void notifyKeyEventInternal(KeyEvent event, int policyFlags) { + mKeyEventDispatcher.notifyKeyEvent(event, policyFlags); + } + private void notifyClearAccessibilityNodeInfoCacheInternal() { IAccessibilityServiceClient listener = mServiceInterface; if (listener != null) { @@ -2339,6 +2412,177 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } return null; } + + private final class InvocationHandler extends Handler { + + public static final int MSG_ON_GESTURE = 1; + public static final int MSG_ON_KEY_EVENT = 2; + public static final int MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 3; + public static final int MSG_ON_KEY_EVENT_TIMEOUT = 4; + + public InvocationHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_ON_GESTURE: { + final int gestureId = message.arg1; + notifyGestureInternal(gestureId); + } break; + case MSG_ON_KEY_EVENT: { + KeyEvent event = (KeyEvent) message.obj; + final int policyFlags = message.arg1; + notifyKeyEventInternal(event, policyFlags); + } break; + case MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { + notifyClearAccessibilityNodeInfoCacheInternal(); + } break; + case MSG_ON_KEY_EVENT_TIMEOUT: { + PendingEvent eventState = (PendingEvent) message.obj; + setOnKeyEventResult(false, eventState.sequence); + } break; + default: { + throw new IllegalArgumentException("Unknown message: " + type); + } + } + } + } + + private final class KeyEventDispatcher { + + private static final long ON_KEY_EVENT_TIMEOUT_MILLIS = 500; + + private PendingEvent mPendingEvents; + + private final InputEventConsistencyVerifier mSentEventsVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() + ? new InputEventConsistencyVerifier( + this, 0, KeyEventDispatcher.class.getSimpleName()) : null; + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + final PendingEvent pendingEvent; + + synchronized (mLock) { + pendingEvent = addPendingEventLocked(event, policyFlags); + } + + Message message = mInvocationHandler.obtainMessage( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, pendingEvent); + mInvocationHandler.sendMessageDelayed(message, ON_KEY_EVENT_TIMEOUT_MILLIS); + + try { + // Accessibility services are exclusively not in the system + // process, therefore no need to clone the motion event to + // prevent tampering. It will be cloned in the IPC call. + mServiceInterface.onKeyEvent(pendingEvent.event, pendingEvent.sequence); + } catch (RemoteException re) { + setOnKeyEventResult(false, pendingEvent.sequence); + } + } + + public void setOnKeyEventResult(boolean handled, int sequence) { + synchronized (mLock) { + PendingEvent pendingEvent = removePendingEventLocked(sequence); + if (pendingEvent != null) { + mInvocationHandler.removeMessages( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + pendingEvent.handled = handled; + finishPendingEventLocked(pendingEvent); + } + } + } + + public void flush() { + synchronized (mLock) { + cancelAllPendingEventsLocked(); + mSentEventsVerifier.reset(); + } + } + + private PendingEvent addPendingEventLocked(KeyEvent event, int policyFlags) { + final int sequence = event.getSequenceNumber(); + PendingEvent pendingEvent = obtainPendingEventLocked(event, policyFlags, sequence); + pendingEvent.next = mPendingEvents; + mPendingEvents = pendingEvent; + return pendingEvent; + } + + private PendingEvent removePendingEventLocked(int sequence) { + PendingEvent previous = null; + PendingEvent current = mPendingEvents; + + while (current != null) { + if (current.sequence == sequence) { + if (previous != null) { + previous.next = current.next; + } else { + mPendingEvents = current.next; + } + current.next = null; + return current; + } + previous = current; + current = current.next; + } + return null; + } + + private void finishPendingEventLocked(PendingEvent pendingEvent) { + if (!pendingEvent.handled) { + sendKeyEventToInputFilter(pendingEvent.event, pendingEvent.policyFlags); + } + // Nullify the event since we do not want it to be + // recycled yet. It will be sent to the input filter. + pendingEvent.event = null; + recyclePendingEventLocked(pendingEvent); + } + + private void sendKeyEventToInputFilter(KeyEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(LOG_TAG, "Injecting event: " + event); + } + if (mSentEventsVerifier != null) { + mSentEventsVerifier.onKeyEvent(event, 0); + } + policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; + mMainHandler.obtainMessage(MainHandler.MSG_SEND_KEY_EVENT_TO_INPUT_FILTER, + policyFlags, 0, event).sendToTarget(); + } + + private void cancelAllPendingEventsLocked() { + while (mPendingEvents != null) { + PendingEvent pendingEvent = removePendingEventLocked(mPendingEvents.sequence); + pendingEvent.handled = false; + mInvocationHandler.removeMessages(InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + finishPendingEventLocked(pendingEvent); + } + } + } + } + + private static final class PendingEvent { + PendingEvent next; + + KeyEvent event; + int policyFlags; + int sequence; + boolean handled; + + public void clear() { + if (event != null) { + event.recycle(); + event = null; + } + next = null; + policyFlags = 0; + sequence = 0; + handled = false; + } } final class SecurityPolicy { diff --git a/services/java/com/android/server/accessibility/EventStreamTransformation.java b/services/java/com/android/server/accessibility/EventStreamTransformation.java index 3289a15..8c93e7b 100644 --- a/services/java/com/android/server/accessibility/EventStreamTransformation.java +++ b/services/java/com/android/server/accessibility/EventStreamTransformation.java @@ -57,7 +57,7 @@ import android.view.accessibility.AccessibilityEvent; interface EventStreamTransformation { /** - * Receives motion event. Passed are the event transformed by previous + * Receives a motion event. Passed are the event transformed by previous * transformations and the raw event to which no transformations have * been applied. * |