diff options
4 files changed, 160 insertions, 63 deletions
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 2dfa8cdd3db9..44f6facd88f5 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -21,6 +21,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; import android.os.Binder; import android.os.Handler; @@ -91,6 +92,9 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_DELAY_DEFAULT = 600; + /** @hide */ + public static final int MAX_A11Y_EVENTS_PER_SERVICE_CALL = 20; + static final Object sInstanceSync = new Object(); private static AccessibilityManager sInstance; @@ -99,6 +103,8 @@ public final class AccessibilityManager { private IAccessibilityManager mService; + private EventDispatchThread mEventDispatchThread; + final int mUserId; final Handler mHandler; @@ -170,7 +176,7 @@ public final class AccessibilityManager { private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { public void setState(int state) { - // We do not want to change this immediately as the applicatoin may + // We do not want to change this immediately as the application may // have already checked that accessibility is on and fired an event, // that is now propagating up the view tree, Hence, if accessibility // is now off an exception will be thrown. We want to have the exception @@ -297,47 +303,32 @@ public final class AccessibilityManager { * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { + if (!isEnabled()) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); return; } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); - return; - } - } - userId = mUserId; } - boolean doRecycle = false; - try { - event.setEventTime(SystemClock.uptimeMillis()); - // it is possible that this manager is in the same process as the service but - // client using it is called through Binder from another process. Example: MMS - // app adds a SMS notification and the NotificationManagerService calls this method - long identityToken = Binder.clearCallingIdentity(); - doRecycle = service.sendAccessibilityEvent(event, userId); - Binder.restoreCallingIdentity(identityToken); - if (DEBUG) { - Log.i(LOG_TAG, event + " sent"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error during sending " + event + " ", re); - } finally { - if (doRecycle) { - event.recycle(); + event.setEventTime(SystemClock.uptimeMillis()); + + getEventDispatchThread().scheduleEvent(event); + } + + private EventDispatchThread getEventDispatchThread() { + synchronized (mLock) { + if (mEventDispatchThread == null) { + mEventDispatchThread = new EventDispatchThread(mService, mUserId); + mEventDispatchThread.start(); } + return mEventDispatchThread; } } @@ -620,7 +611,7 @@ public final class AccessibilityManager { } } - private IAccessibilityManager getServiceLocked() { + private IAccessibilityManager getServiceLocked() { if (mService == null) { tryConnectToServiceLocked(null); } @@ -722,4 +713,99 @@ public final class AccessibilityManager { } } } + + private static class EventDispatchThread extends Thread { + // Second lock used to keep UI thread performant. Never try to grab mLock when holding + // this one, or the UI thread will block in send AccessibilityEvent. + private final Object mEventQueueLock = new Object(); + + // Two lists to hold events. The app thread fills one while we empty the other. + private final ArrayList<AccessibilityEvent> mEventLists0 = + new ArrayList<>(MAX_A11Y_EVENTS_PER_SERVICE_CALL); + private final ArrayList<AccessibilityEvent> mEventLists1 = + new ArrayList<>(MAX_A11Y_EVENTS_PER_SERVICE_CALL); + + private boolean mPingPongListToggle; + + private final IAccessibilityManager mService; + + private final int mUserId; + + EventDispatchThread(IAccessibilityManager service, int userId) { + mService = service; + mUserId = userId; + } + + @Override + public void run() { + while (true) { + ArrayList<AccessibilityEvent> listBeingDrained; + synchronized (mEventQueueLock) { + ArrayList<AccessibilityEvent> listBeingFilled = getListBeingFilledLocked(); + if (listBeingFilled.isEmpty()) { + try { + mEventQueueLock.wait(); + } catch (InterruptedException e) { + // Treat as a notify + } + } + // Swap buffers + mPingPongListToggle = !mPingPongListToggle; + listBeingDrained = listBeingFilled; + } + dispatchEvents(listBeingDrained); + } + } + + public void scheduleEvent(AccessibilityEvent event) { + synchronized (mEventQueueLock) { + getListBeingFilledLocked().add(event); + mEventQueueLock.notifyAll(); + } + } + + private ArrayList<AccessibilityEvent> getListBeingFilledLocked() { + return (mPingPongListToggle) ? mEventLists0 : mEventLists1; + } + + private void dispatchEvents(ArrayList<AccessibilityEvent> events) { + int eventListCapacityLowerBound = events.size(); + while (events.size() > 0) { + // We don't want to consume extra memory if an app sends a lot of events in a + // one-off event. Cap the list length at double the max events per call. + // We'll end up with extra GC for apps that send huge numbers of events, but + // sending that many events will lead to bad performance in any case. + if ((eventListCapacityLowerBound > 2 * MAX_A11Y_EVENTS_PER_SERVICE_CALL) + && (events.size() <= 2 * MAX_A11Y_EVENTS_PER_SERVICE_CALL)) { + events.trimToSize(); + eventListCapacityLowerBound = events.size(); + } + // We only expect this loop to run once, as the app shouldn't be sending + // huge numbers of events. + // The clear in the called method will remove the sent events + dispatchOneBatchOfEvents(events.subList(0, + Math.min(events.size(), MAX_A11Y_EVENTS_PER_SERVICE_CALL))); + } + } + + private void dispatchOneBatchOfEvents(List<AccessibilityEvent> events) { + if (events.isEmpty()) { + return; + } + long identityToken = Binder.clearCallingIdentity(); + try { + mService.sendAccessibilityEvents(new ParceledListSlice<>(events), + mUserId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error sending multiple events"); + } + Binder.restoreCallingIdentity(identityToken); + if (DEBUG) { + Log.i(LOG_TAG, events.size() + " events sent"); + } + for (int i = events.size() - 1; i >= 0; i--) { + events.remove(i).recycle(); + } + } + } } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 7f44bac8bc6f..aa9cb39062f9 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -21,6 +21,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceConnection; import android.accessibilityservice.IAccessibilityServiceClient; import android.content.ComponentName; +import android.content.pm.ParceledListSlice; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnection; @@ -37,7 +38,9 @@ interface IAccessibilityManager { int addClient(IAccessibilityManagerClient client, int userId); - boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent, int userId); + void sendAccessibilityEvent(in AccessibilityEvent uiEvent, int userId); + + void sendAccessibilityEvents(in ParceledListSlice events, int userId); List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 695ea606a90b..b1fbcde6c727 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -451,7 +451,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } @Override - public boolean sendAccessibilityEvent(AccessibilityEvent event, int userId) { + public void sendAccessibilityEvent(AccessibilityEvent event, int userId) { synchronized (mLock) { // We treat calls from a profile as if made by its parent as profiles // share the accessibility state of the parent. The call below @@ -459,23 +459,39 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final int resolvedUserId = mSecurityPolicy .resolveCallingUserIdEnforcingPermissionsLocked(userId); // This method does nothing for a background user. - if (resolvedUserId != mCurrentUserId) { - return true; // yes, recycle the event - } - if (mSecurityPolicy.canDispatchAccessibilityEventLocked(event)) { - mSecurityPolicy.updateActiveAndAccessibilityFocusedWindowLocked(event.getWindowId(), - event.getSourceNodeId(), event.getEventType(), event.getAction()); - mSecurityPolicy.updateEventSourceLocked(event); - notifyAccessibilityServicesDelayedLocked(event, false); - notifyAccessibilityServicesDelayedLocked(event, true); - } - if (mHasInputFilter && mInputFilter != null) { - mMainHandler.obtainMessage(MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER, - AccessibilityEvent.obtain(event)).sendToTarget(); + if (resolvedUserId == mCurrentUserId) { + if (mSecurityPolicy.canDispatchAccessibilityEventLocked(event)) { + mSecurityPolicy.updateActiveAndAccessibilityFocusedWindowLocked( + event.getWindowId(), event.getSourceNodeId(), + event.getEventType(), event.getAction()); + mSecurityPolicy.updateEventSourceLocked(event); + notifyAccessibilityServicesDelayedLocked(event, false); + notifyAccessibilityServicesDelayedLocked(event, true); + } + if (mHasInputFilter && mInputFilter != null) { + mMainHandler.obtainMessage( + MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER, + AccessibilityEvent.obtain(event)).sendToTarget(); + } } + } + if (OWN_PROCESS_ID != Binder.getCallingPid()) { event.recycle(); } - return (OWN_PROCESS_ID != Binder.getCallingPid()); + } + + @Override + public void sendAccessibilityEvents(ParceledListSlice events, int userId) { + List<AccessibilityEvent> a11yEvents = events.getList(); + // Grab the lock once for the entire batch + synchronized (mLock) { + int numEventsToProcess = Math.min(a11yEvents.size(), + AccessibilityManager.MAX_A11Y_EVENTS_PER_SERVICE_CALL); + for (int i = 0; i < numEventsToProcess; i++) { + AccessibilityEvent event = a11yEvents.get(i); + sendAccessibilityEvent(event, userId); + } + } } @Override diff --git a/services/tests/servicestests/src/com/android/server/AccessibilityManagerTest.java b/services/tests/servicestests/src/com/android/server/AccessibilityManagerTest.java index 026a2adc1f96..6e3e6c6278a9 100644 --- a/services/tests/servicestests/src/com/android/server/AccessibilityManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/AccessibilityManagerTest.java @@ -131,18 +131,10 @@ public class AccessibilityManagerTest extends AndroidTestCase { public void testSendAccessibilityEvent_AccessibilityEnabled() throws Exception { AccessibilityEvent sentEvent = AccessibilityEvent.obtain(); - when(mMockService.sendAccessibilityEvent(eq(sentEvent), anyInt())) - .thenReturn(true /* should recycle event object */) - .thenReturn(false /* should not recycle event object */); - AccessibilityManager manager = createManager(true); manager.sendAccessibilityEvent(sentEvent); assertSame("The event should be recycled.", sentEvent, AccessibilityEvent.obtain()); - - manager.sendAccessibilityEvent(sentEvent); - - assertNotSame("The event should not be recycled.", sentEvent, AccessibilityEvent.obtain()); } @MediumTest |