Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c3464941 authored by Phil Weaver's avatar Phil Weaver
Browse files

Dispatch a11y events in separate thread.

Moves the IPCs into a separate thread, where they should affect
jank a lot less.

Bug: 30183085
Change-Id: Ib76159d158e7a867e76cdd5c8ea3a318949fcc5b
parent 4ecc9d2d
Loading
Loading
Loading
Loading
+125 −39
Original line number Diff line number Diff line
@@ -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,14 +303,7 @@ public final class AccessibilityManager {
     * their descendants.
     */
    public void sendAccessibilityEvent(AccessibilityEvent event) {
        final IAccessibilityManager service;
        final int userId;
        synchronized (mLock) {
            service = getServiceLocked();
            if (service == null) {
                return;
            }
            if (!mIsEnabled) {
        if (!isEnabled()) {
            Looper myLooper = Looper.myLooper();
            if (myLooper == Looper.getMainLooper()) {
                throw new IllegalStateException(
@@ -318,26 +317,18 @@ public final class AccessibilityManager {
                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");

        getEventDispatchThread().scheduleEvent(event);
    }
        } catch (RemoteException re) {
            Log.e(LOG_TAG, "Error during sending " + event + " ", re);
        } finally {
            if (doRecycle) {
                event.recycle();

    private EventDispatchThread getEventDispatchThread() {
        synchronized (mLock) {
            if (mEventDispatchThread == null) {
                mEventDispatchThread = new EventDispatchThread(mService, mUserId);
                mEventDispatchThread.start();
            }
            return mEventDispatchThread;
        }
    }

@@ -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();
            }
        }
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -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);

+31 −15
Original line number Diff line number Diff line
@@ -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 (resolvedUserId == mCurrentUserId) {
                if (mSecurityPolicy.canDispatchAccessibilityEventLocked(event)) {
                mSecurityPolicy.updateActiveAndAccessibilityFocusedWindowLocked(event.getWindowId(),
                        event.getSourceNodeId(), event.getEventType(), event.getAction());
                    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,
                    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
+0 −8
Original line number Diff line number Diff line
@@ -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