Merge "Adding APIs for an accessibility service to intercept key events." into jb-mr2-dev
diff --git a/api/current.txt b/api/current.txt
index cc465e2..2f79c9b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2075,6 +2075,7 @@
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.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 void onServiceConnected();
public void onSetConnectionId(int connectionId);
public boolean onGesture(int gestureId);
+ public boolean onKeyEvent(KeyEvent event);
}
private int mConnectionId;
@@ -413,6 +415,32 @@
}
/**
+ * 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 boolean onGesture(int gestureId) {
return AccessibilityService.this.onGesture(gestureId);
}
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ return AccessibilityService.this.onKeyEvent(event);
+ }
});
}
@@ -554,11 +587,14 @@
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 @@
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 @@
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 @@
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 @@
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.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 @@
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.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 @@
private final Choreographer mChoreographer;
- private int mCurrentDeviceId;
+ private int mCurrentTouchDeviceId;
private boolean mInstalled;
@@ -98,6 +99,8 @@
private boolean mHoverEventSequenceStarted;
+ private boolean mKeyEventSequenceStarted;
+
AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
super(context.getMainLooper());
mContext = context;
@@ -133,11 +136,21 @@
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 @@
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 @@
} 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 @@
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 @@
}
}
+ 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.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 @@
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 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
} else {
if (mHasInputFilter) {
mHasInputFilter = false;
- mInputFilter.setEnabledFeatures(0);
+ mInputFilter.reset();
inputFilter = null;
setInputFilter = true;
}
@@ -1446,6 +1486,7 @@
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 @@
}
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 @@
}
}
+ 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 @@
* 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 @@
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 @@
return false;
}
UserState userState = getUserStateLocked(mUserId);
+ mKeyEventDispatcher.flush();
if (!mIsAutomation) {
mContext.unbindService(this);
} else {
@@ -1718,6 +1774,11 @@
}
@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 void binderDied() {
synchronized (mLock) {
+ mKeyEventDispatcher.flush();
UserState userState = getUserStateLocked(mUserId);
// The death recipient is unregistered in removeServiceLocked
removeServiceLocked(this, userState);
@@ -2141,12 +2203,12 @@
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 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 @@
}
}
+ private void notifyKeyEventInternal(KeyEvent event, int policyFlags) {
+ mKeyEventDispatcher.notifyKeyEvent(event, policyFlags);
+ }
+
private void notifyClearAccessibilityNodeInfoCacheInternal() {
IAccessibilityServiceClient listener = mServiceInterface;
if (listener != null) {
@@ -2339,6 +2412,177 @@
}
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 @@
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.
*