From 111ef0b93234d62ef1ae6c22646eea29436bed15 Mon Sep 17 00:00:00 2001 From: sallyyuen Date: Thu, 3 Nov 2022 13:57:59 -0700 Subject: [PATCH] Expose more SystemAPI methods on AccessibilityDisplayProxy These are: - onProxyConnected (equivalent to onServiceConnected) - onAccessibilityEvent - onInterrupt - getWindows - set/getInstalledAndEnabledServices Connect the proxy client (IAccessibilityServiceClient) to a proxy service connection (ProxyAbstractAccessibilityServiceConnection) in ProxyManager, so the A11yDisplayProxy can access the UI. AccessibilityServices run on their own thread. To avoid using the app's main thread, the app that registers the A11yDisplayProxy can call it on a different thread. Add a note in the proxy class. Future TODOs (b/254545943): The a11y state returned to A11yManager includes state of all proxies and AccessibilityServices. Separation will require AccessibiilityManager to go from being a process singleton to a per-display instance. Input and a11y focus are singletons and may shift between a proxy display and a display on the device. Separation will require future changes. ProxyManager behaves more like UiAutomationManager, and centralizes proxy management. Separation into different user states will require future refactoring. Bug: 241429275 Test: atest AccessibilityDisplayProxyTest, AccessibilityManagerServiceTest, manual test app that registers/unregisters A11yDisplayProxy Change-Id: I830c6613c2c2f8abcc2343c5110f4e7b7494fb0e --- core/api/system-current.txt | 6 + .../AccessibilityDisplayProxy.java | 163 ++++++++++++++- .../AccessibilityInteractionClient.java | 18 +- .../AccessibilityManagerTest.java | 16 +- .../AccessibilityManagerService.java | 39 ++-- .../AccessibilitySecurityPolicy.java | 2 +- .../ProxyAccessibilityServiceConnection.java | 21 +- .../server/accessibility/ProxyManager.java | 185 +++++++++++++++++- .../AccessibilityManagerServiceTest.java | 44 ++++- 9 files changed, 443 insertions(+), 51 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 352b4f98edd31..4f30e8ab4835c 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -15928,6 +15928,12 @@ package android.view.accessibility { public abstract class AccessibilityDisplayProxy { ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List); method public int getDisplayId(); + method @NonNull public final java.util.List getInstalledAndEnabledServices(); + method @NonNull public java.util.List getWindows(); + method public void interrupt(); + method public void onAccessibilityEvent(@NonNull android.view.accessibility.AccessibilityEvent); + method public void onProxyConnected(); + method public void setInstalledAndEnabledServices(@NonNull java.util.List); } public final class AccessibilityManager { diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java index 85f5056e41160..44b6deb3015fc 100644 --- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -20,13 +20,18 @@ import android.accessibilityservice.AccessibilityGestureEvent; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IAccessibilityServiceConnection; import android.accessibilityservice.MagnificationConfig; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.ResolveInfo; import android.graphics.Region; import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.EditorInfo; @@ -34,14 +39,15 @@ import android.view.inputmethod.EditorInfo; import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback; import com.android.internal.inputmethod.RemoteAccessibilityInputConnection; +import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; /** * Allows a privileged app - an app with MANAGE_ACCESSIBILITY permission and SystemAPI access - to * interact with the windows in the display that this proxy represents. Proxying the default display - * or a display that is not tracked will throw an exception. Only the real user has access to global - * clients like SystemUI. + * or a display that is not tracked by accessibility, such as private displays, will throw an + * exception. Only the real user has access to global clients like SystemUI. * *

* To register and unregister a proxy, use @@ -49,7 +55,16 @@ import java.util.concurrent.Executor; * and {@link AccessibilityManager#unregisterDisplayProxy(AccessibilityDisplayProxy)}. If the app * that has registered the proxy dies, the system will remove the proxy. * - * TODO(241429275): Complete proxy impl and add additional support (if necessary) like cache methods + *

+ * Avoid using the app's main thread. Proxy methods such as {@link #getWindows} and node methods + * like {@link AccessibilityNodeInfo#getChild(int)} will happen frequently. Node methods may also + * wait on the displayed app's UI thread to obtain accurate screen data. + * + *

+ * To get a list of {@link AccessibilityServiceInfo}s that have populated {@link ComponentName}s and + * {@link ResolveInfo}s, retrieve the list using {@link #getInstalledAndEnabledServices()} after + * {@link #onProxyConnected()} has been called. + * * @hide */ @SystemApi @@ -91,7 +106,134 @@ public abstract class AccessibilityDisplayProxy { } /** - * An IAccessibilityServiceClient that handles interrupts and accessibility events. + * Handles {@link android.view.accessibility.AccessibilityEvent}s. + *

+ * AccessibilityEvents represent changes to the UI, or what parts of the node tree have changed. + * AccessibilityDisplayProxy should use these to query new UI and send appropriate feedback + * to their users. + *

+ * For example, a {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} indicates a change in windows, + * so a proxy may query {@link #getWindows} to obtain updated UI and potentially inform of a new + * window title. Or a proxy may emit an earcon on a + * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. + */ + public void onAccessibilityEvent(@NonNull AccessibilityEvent event) { + // Default no-op + } + + /** + * Handles a successful system connection after + * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)} is called. + * + *

+ * At this point, querying for UI is available and {@link AccessibilityEvent}s will begin being + * sent. An AccessibilityDisplayProxy may instantiate core infrastructure components here. + */ + public void onProxyConnected() { + // Default no-op + } + + /** + * Handles a request to interrupt the accessibility feedback. + *

+ * AccessibilityDisplayProxy should interrupt the accessibility activity occurring on its + * display. For example, a screen reader may interrupt speech. + * + * @see AccessibilityManager#interrupt() + * @see AccessibilityService#onInterrupt() + */ + public void interrupt() { + // Default no-op + } + + /** + * Gets the focus of the window specified by {@code windowInfo}. + * + * @param windowInfo the window to search + * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or + * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * @hide + * TODO(254545943): Do not expose until support for accessibility focus and/or input is in place + */ + @Nullable + public AccessibilityNodeInfo findFocus(@NonNull AccessibilityWindowInfo windowInfo, int focus) { + AccessibilityNodeInfo windowRoot = windowInfo.getRoot(); + return windowRoot != null ? windowRoot.findFocus(focus) : null; + } + + /** + * Gets the windows of the tracked display. + * + * @see AccessibilityService#getWindows() + */ + @NonNull + public List getWindows() { + return AccessibilityInteractionClient.getInstance().getWindowsOnDisplay(mConnectionId, + mDisplayId); + } + + /** + * Sets the list of {@link AccessibilityServiceInfo}s describing the services interested in the + * {@link AccessibilityDisplayProxy}'s display. + * + *

These represent a11y features and services that are installed and running. These should + * not include {@link AccessibilityService}s installed on the phone. + * + * @param installedAndEnabledServices the list of installed and running a11y services. + */ + public void setInstalledAndEnabledServices( + @NonNull List installedAndEnabledServices) { + mInstalledAndEnabledServices = installedAndEnabledServices; + sendServiceInfos(); + } + + /** + * Sets the {@link AccessibilityServiceInfo} for this service if the latter is + * properly set and there is an {@link IAccessibilityServiceConnection} to the + * AccessibilityManagerService. + */ + private void sendServiceInfos() { + IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); + if (mInstalledAndEnabledServices != null && mInstalledAndEnabledServices.size() > 0 + && connection != null) { + try { + connection.setInstalledAndEnabledServices(mInstalledAndEnabledServices); + AccessibilityInteractionClient.getInstance().clearCache(mConnectionId); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfos", re); + re.rethrowFromSystemServer(); + } + } + mInstalledAndEnabledServices = null; + } + + /** + * Gets the list of {@link AccessibilityServiceInfo}s describing the services interested in the + * {@link AccessibilityDisplayProxy}'s display. + * + * @return The {@link AccessibilityServiceInfo}s of interested services. + * @see AccessibilityServiceInfo + */ + @NonNull + public final List getInstalledAndEnabledServices() { + IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); + if (connection != null) { + try { + return connection.getInstalledAndEnabledServices(); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); + re.rethrowFromSystemServer(); + } + } + return Collections.emptyList(); + } + + /** + * An IAccessibilityServiceClient that handles interrupts, accessibility events, and system + * connection. */ private class IAccessibilityServiceClientImpl extends AccessibilityService.IAccessibilityServiceClientWrapper { @@ -100,17 +242,24 @@ public abstract class AccessibilityDisplayProxy { super(context, executor, new AccessibilityService.Callbacks() { @Override public void onAccessibilityEvent(AccessibilityEvent event) { - // TODO: call AccessiiblityProxy.onAccessibilityEvent + // TODO(254545943): Remove check when event processing is done more upstream in + // AccessibilityManagerService. + if (event.getDisplayId() == mDisplayId) { + AccessibilityDisplayProxy.this.onAccessibilityEvent(event); + } } @Override public void onInterrupt() { - // TODO: call AccessiiblityProxy.onInterrupt + AccessibilityDisplayProxy.this.interrupt(); } + @Override public void onServiceConnected() { - // TODO: send service infos and call AccessiiblityProxy.onProxyConnected + AccessibilityDisplayProxy.this.sendServiceInfos(); + AccessibilityDisplayProxy.this.onProxyConnected(); } + @Override public void init(int connectionId, IBinder windowToken) { mConnectionId = connectionId; diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index e3ffc9dcbbdec..7b1cec752cf21 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -447,14 +447,20 @@ public final class AccessibilityInteractionClient * @return The {@link AccessibilityWindowInfo} list. */ public List getWindows(int connectionId) { - final SparseArray> windows = - getWindowsOnAllDisplays(connectionId); - if (windows.size() > 0) { - return windows.valueAt(Display.DEFAULT_DISPLAY); - } - return Collections.emptyList(); + return getWindowsOnDisplay(connectionId, Display.DEFAULT_DISPLAY); } + /** + * Gets the info for all windows of the specified display. + * + * @param connectionId The id of a connection for interacting with the system. + * @return The {@link AccessibilityWindowInfo} list belonging to {@code displayId}. + */ + public List getWindowsOnDisplay(int connectionId, int displayId) { + final SparseArray> windows = + getWindowsOnAllDisplays(connectionId); + return windows.get(displayId, Collections.emptyList()); + } /** * Gets the info for all windows of all displays. * diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java index ee1e10f9009ea..7cbf3ffa509cc 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java @@ -263,10 +263,24 @@ public class AccessibilityManagerTest { } private class MyAccessibilityProxy extends AccessibilityDisplayProxy { - // TODO(241429275): Will override A11yProxy methods in the future. MyAccessibilityProxy(int displayId, @NonNull List serviceInfos) { super(displayId, Executors.newSingleThreadExecutor(), serviceInfos); } + + @Override + public void onAccessibilityEvent(@NonNull AccessibilityEvent event) { + + } + + @Override + public void onProxyConnected() { + + } + + @Override + public void interrupt() { + + } } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index e3ae03cbcdd80..2647ca3dbeb98 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -859,7 +859,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid()); } return IntPair.of( - getClientStateLocked(userState), + combineUserStateAndProxyState(getClientStateLocked(userState), + mProxyManager.getStateLocked()), client.mLastSentRelevantEventTypes); } else { userState.mUserClients.register(callback, client); @@ -871,7 +872,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub + " and userId:" + mCurrentUserId); } return IntPair.of( - (resolvedUserId == mCurrentUserId) ? getClientStateLocked(userState) : 0, + (resolvedUserId == mCurrentUserId) ? combineUserStateAndProxyState( + getClientStateLocked(userState), mProxyManager.getStateLocked()) + : 0, client.mLastSentRelevantEventTypes); } } @@ -1002,6 +1005,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub notifyAccessibilityServicesDelayedLocked(event, false); notifyAccessibilityServicesDelayedLocked(event, true); mUiAutomationManager.sendAccessibilityEventLocked(event); + mProxyManager.sendAccessibilityEvent(event); } private void sendAccessibilityEventToInputFilter(AccessibilityEvent event) { @@ -1142,9 +1146,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } List services = getUserStateLocked(resolvedUserId).mBoundServices; - int numServices = services.size(); + int numServices = services.size() + mProxyManager.getNumProxys(); interfacesToInterrupt = new ArrayList<>(numServices); - for (int i = 0; i < numServices; i++) { + for (int i = 0; i < services.size(); i++) { AccessibilityServiceConnection service = services.get(i); IBinder a11yServiceBinder = service.mService; IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface; @@ -1152,6 +1156,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub interfacesToInterrupt.add(a11yServiceInterface); } } + mProxyManager.addServiceInterfaces(interfacesToInterrupt); } for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) { try { @@ -1940,6 +1945,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUiAutomationManager.getServiceInfo(), client) ? mUiAutomationManager.getRelevantEventTypes() : 0; + relevantEventTypes |= mProxyManager.getRelevantEventTypes(); return relevantEventTypes; } @@ -2177,21 +2183,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub updateAccessibilityEnabledSettingLocked(userState); } - void scheduleUpdateClientsIfNeeded(AccessibilityUserState userState) { - synchronized (mLock) { - scheduleUpdateClientsIfNeededLocked(userState); - } + private int combineUserStateAndProxyState(int userState, int proxyState) { + return userState | proxyState; } void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState) { final int clientState = getClientStateLocked(userState); - if (userState.getLastSentClientStateLocked() != clientState + final int proxyState = mProxyManager.getStateLocked(); + if ((userState.getLastSentClientStateLocked() != clientState + || mProxyManager.getLastSentStateLocked() != proxyState) && (mGlobalClients.getRegisteredCallbackCount() > 0 - || userState.mUserClients.getRegisteredCallbackCount() > 0)) { + || userState.mUserClients.getRegisteredCallbackCount() > 0)) { userState.setLastSentClientStateLocked(clientState); + mProxyManager.setLastStateLocked(proxyState); + // Send both the user and proxy state to the app for now. + // TODO(b/250929565): Send proxy state to proxy clients mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::sendStateToAllClients, - this, clientState, userState.mUserId)); + this, combineUserStateAndProxyState(clientState, proxyState), + userState.mUserId)); } } @@ -2433,7 +2443,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // binding we do an update pass after each bind event, so we run this // code and register the callback if needed. - boolean observingWindows = mUiAutomationManager.canRetrieveInteractiveWindowsLocked(); + boolean observingWindows = mUiAutomationManager.canRetrieveInteractiveWindowsLocked() + || mProxyManager.canRetrieveInteractiveWindowsLocked(); List boundServices = userState.mBoundServices; final int boundServiceCount = boundServices.size(); for (int i = 0; !observingWindows && (i < boundServiceCount); i++) { @@ -3656,7 +3667,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub + "proxy-ed"); } - mProxyManager.registerProxy(client, displayId); + mProxyManager.registerProxy(client, displayId, mContext, + sIdCounter++, mMainHandler, mSecurityPolicy, this, getTraceManager(), + mWindowManagerService, mA11yWindowManager); return true; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java index e5e1d027dd113..ce269f5eca592 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java @@ -696,7 +696,7 @@ public class AccessibilitySecurityPolicy { final ResolveInfo resolveInfo = service.getServiceInfo().getResolveInfo(); if (resolveInfo == null) { - // For InteractionBridge and UiAutomation + // For InteractionBridge, UiAutomation, and Proxy. return true; } diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index 247f320adecd9..d7f9c12b78850 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -16,8 +16,12 @@ package com.android.server.accessibility; +import static com.android.server.accessibility.ProxyManager.PROXY_COMPONENT_CLASS_NAME; +import static com.android.server.accessibility.ProxyManager.PROXY_COMPONENT_PACKAGE_NAME; + import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityTrace; +import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.MagnificationConfig; import android.annotation.NonNull; import android.content.ComponentName; @@ -31,6 +35,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallback; +import android.os.RemoteException; import android.view.KeyEvent; import android.view.accessibility.AccessibilityDisplayProxy; import android.view.accessibility.AccessibilityNodeInfo; @@ -53,10 +58,6 @@ import java.util.Set; * TODO(241429275): Initialize this when a proxy is registered. */ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceConnection { - // Names used to populate ComponentName and ResolveInfo - private static final String PROXY_COMPONENT_PACKAGE_NAME = "ProxyPackage"; - private static final String PROXY_COMPONENT_CLASS_NAME = "ProxyClass"; - private int mDisplayId; private List mInstalledAndEnabledServices; @@ -75,6 +76,16 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon mDisplayId = displayId; } + /** + * Called when the proxy is registered. + */ + void initializeServiceInterface(IAccessibilityServiceClient serviceInterface) + throws RemoteException { + mServiceInterface = serviceInterface; + mService = serviceInterface.asBinder(); + mServiceInterface.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId)); + } + /** * Keeps mAccessibilityServiceInfo in sync with the proxy's list of AccessibilityServiceInfos. * @@ -89,7 +100,7 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon synchronized (mLock) { mInstalledAndEnabledServices = infos; final AccessibilityServiceInfo proxyInfo = mAccessibilityServiceInfo; - // Reset values. + // Reset values. mAccessibilityServiceInfo is not completely reset since it is final proxyInfo.flags = 0; proxyInfo.eventTypes = 0; proxyInfo.notificationTimeout = 0; diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java index a2ce61063aaf3..2184878b4c362 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java @@ -14,9 +14,21 @@ * limitations under the License. */ package com.android.server.accessibility; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.AccessibilityTrace; import android.accessibilityservice.IAccessibilityServiceClient; +import android.content.ComponentName; +import android.content.Context; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.SparseArray; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; -import java.util.HashSet; +import com.android.server.wm.WindowManagerInternal; + +import java.util.List; /** * Manages proxy connections. @@ -27,32 +39,185 @@ import java.util.HashSet; * TODO(241117292): Remove or cut down during simultaneous user refactoring. */ public class ProxyManager { + // Names used to populate ComponentName and ResolveInfo in connection.mA11yServiceInfo and in + // the infos of connection.setInstalledAndEnabledServices + static final String PROXY_COMPONENT_PACKAGE_NAME = "ProxyPackage"; + static final String PROXY_COMPONENT_CLASS_NAME = "ProxyClass"; + private final Object mLock; - private final HashSet mDisplayIds = new HashSet<>(); + + // Used to determine if we should notify AccessibilityManager clients of updates. + // TODO(254545943): Separate this so each display id has its own state. Currently there is no + // way to identify from AccessibilityManager which proxy state should be returned. + private int mLastState = -1; + + private SparseArray mProxyA11yServiceConnections = + new SparseArray<>(); ProxyManager(Object lock) { mLock = lock; } /** - * TODO: Create the proxy service connection. + * Creates the service connection. */ - public void registerProxy(IAccessibilityServiceClient client, int displayId) { - mDisplayIds.add(displayId); + public void registerProxy(IAccessibilityServiceClient client, int displayId, + Context context, + int id, Handler mainHandler, + AccessibilitySecurityPolicy securityPolicy, + AbstractAccessibilityServiceConnection.SystemSupport systemSupport, + AccessibilityTrace trace, + WindowManagerInternal windowManagerInternal, + AccessibilityWindowManager awm) throws RemoteException { + + // Set a default AccessibilityServiceInfo that is used before the proxy's info is + // populated. A proxy has the touch exploration and window capabilities. + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION + | AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); + final String componentClassDisplayName = PROXY_COMPONENT_CLASS_NAME + displayId; + info.setComponentName(new ComponentName(PROXY_COMPONENT_PACKAGE_NAME, + componentClassDisplayName)); + ProxyAccessibilityServiceConnection connection = + new ProxyAccessibilityServiceConnection(context, info.getComponentName(), info, + id, mainHandler, mLock, securityPolicy, systemSupport, trace, + windowManagerInternal, + awm, displayId); + + mProxyA11yServiceConnections.put(displayId, connection); + + // If the client dies, make sure to remove the connection. + IBinder.DeathRecipient deathRecipient = + new IBinder.DeathRecipient() { + @Override + public void binderDied() { + client.asBinder().unlinkToDeath(this, 0); + clearConnection(displayId); + } + }; + client.asBinder().linkToDeath(deathRecipient, 0); + // Notify apps that the service state has changed. + // A11yManager#A11yServicesStateChangeListener + connection.mSystemSupport.onClientChangeLocked(true); + + connection.initializeServiceInterface(client); } /** - * TODO: Unregister the proxy service connection based on display id. + * Unregister the proxy based on display id. */ public boolean unregisterProxy(int displayId) { - mDisplayIds.remove(displayId); - return true; + return clearConnection(displayId); + } + + private boolean clearConnection(int displayId) { + if (mProxyA11yServiceConnections.contains(displayId)) { + mProxyA11yServiceConnections.remove(displayId); + return true; + } + return false; } /** * Checks if a display id is being proxy-ed. */ public boolean isProxyed(int displayId) { - return mDisplayIds.contains(displayId); + return mProxyA11yServiceConnections.contains(displayId); } -} + + /** + * Sends AccessibilityEvents to all proxies. + * {@link android.view.accessibility.AccessibilityDisplayProxy} will filter based on display. + * TODO(b/250929565): Filtering should happen in the system, not in the proxy. + */ + public void sendAccessibilityEvent(AccessibilityEvent event) { + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + proxy.notifyAccessibilityEvent(event); + } + } + + /** + * Returns {@code true} if any proxy can retrieve windows. + * TODO(b/250929565): Retrieve per connection/user state. + */ + public boolean canRetrieveInteractiveWindowsLocked() { + boolean observingWindows = false; + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy.mRetrieveInteractiveWindows) { + observingWindows = true; + break; + } + } + return observingWindows; + } + + /** + * If there is at least one proxy, accessibility is enabled. + */ + public int getStateLocked() { + int clientState = 0; + final boolean a11yEnabled = mProxyA11yServiceConnections.size() > 0; + if (a11yEnabled) { + clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED; + } + return clientState; + // TODO(b/254545943): When A11yManager is separated, include support for other properties + // like isTouchExplorationEnabled. + } + + /** + * Gets the last state. + */ + public int getLastSentStateLocked() { + return mLastState; + } + + /** + * Sets the last state. + */ + public void setLastStateLocked(int proxyState) { + mLastState = proxyState; + } + + /** + * Returns the relevant event types of every proxy. + * TODO(254545943): When A11yManager is separated, return based on the A11yManager display. + */ + public int getRelevantEventTypes() { + int relevantEventTypes = 0; + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + relevantEventTypes |= proxy.getRelevantEventTypes(); + } + return relevantEventTypes; + } + + /** + * Gets the number of current proxy connections. + * @return + */ + public int getNumProxys() { + return mProxyA11yServiceConnections.size(); + } + + /** + * Adds the service interfaces to a list. + * @param interfaces + */ + public void addServiceInterfaces(List interfaces) { + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + final IBinder proxyBinder = proxy.mService; + final IAccessibilityServiceClient proxyInterface = proxy.mServiceInterface; + if ((proxyBinder != null) && (proxyInterface != null)) { + interfaces.add(proxyInterface); + } + } + } +} \ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 0f09252b8ca12..52a550b1bcf2f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -58,7 +58,6 @@ import android.view.Display; import android.view.DisplayAdjustments; import android.view.DisplayInfo; import android.view.WindowManager; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityWindowAttributes; import androidx.test.InstrumentationRegistry; @@ -106,8 +105,6 @@ public class AccessibilityManagerServiceTest { LABEL, DESCRIPTION, TEST_PENDING_INTENT); - private static final AccessibilityAction NEW_ACCESSIBILITY_ACTION = - new AccessibilityAction(ACTION_ID, LABEL); private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY + 1; @@ -282,10 +279,12 @@ public class AccessibilityManagerServiceTest { @Test public void testRegisterProxy() throws Exception { mA11yms.registerProxyForDisplay(mMockServiceClient, TEST_DISPLAY); - verify(mProxyManager).registerProxy(mMockServiceClient, TEST_DISPLAY); + verify(mProxyManager).registerProxy(eq(mMockServiceClient), eq(TEST_DISPLAY), + eq(mTestableContext), anyInt(), any(), eq(mMockSecurityPolicy), + eq(mA11yms), eq(mA11yms.getTraceManager()), + eq(mMockWindowManagerService), eq(mMockA11yWindowManager)); } - @SmallTest @Test public void testRegisterProxyWithoutPermission() throws Exception { @@ -296,7 +295,8 @@ public class AccessibilityManagerServiceTest { Assert.fail(); } catch (SecurityException expected) { } - verify(mProxyManager, never()).registerProxy(mMockServiceClient, TEST_DISPLAY); + verify(mProxyManager, never()).registerProxy(any(), anyInt(), any(), anyInt(), any(), any(), + any(), any(), any(), any()); } @SmallTest @@ -307,7 +307,8 @@ public class AccessibilityManagerServiceTest { Assert.fail(); } catch (IllegalArgumentException expected) { } - verify(mProxyManager, never()).registerProxy(mMockServiceClient, Display.DEFAULT_DISPLAY); + verify(mProxyManager, never()).registerProxy(any(), anyInt(), any(), anyInt(), any(), any(), + any(), any(), any(), any()); } @SmallTest @@ -318,7 +319,30 @@ public class AccessibilityManagerServiceTest { Assert.fail(); } catch (IllegalArgumentException expected) { } - verify(mProxyManager, never()).registerProxy(mMockServiceClient, Display.INVALID_DISPLAY); + verify(mProxyManager, never()).registerProxy(any(), anyInt(), any(), anyInt(), any(), any(), + any(), any(), any(), any()); + } + + @SmallTest + @Test + public void testUnRegisterProxyWithPermission() throws Exception { + mA11yms.registerProxyForDisplay(mMockServiceClient, TEST_DISPLAY); + mA11yms.unregisterProxyForDisplay(TEST_DISPLAY); + + verify(mProxyManager).unregisterProxy(TEST_DISPLAY); + } + + @SmallTest + @Test + public void testUnRegisterProxyWithoutPermission() throws Exception { + doThrow(SecurityException.class).when(mMockSecurityPolicy) + .enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY); + try { + mA11yms.unregisterProxyForDisplay(TEST_DISPLAY); + Assert.fail(); + } catch (SecurityException expected) { + } + verify(mProxyManager, never()).unregisterProxy(TEST_DISPLAY); } @SmallTest @@ -417,6 +441,8 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test public void testOnClientChange_magnificationEnabledAndCapabilityAll_requestConnection() { + when(mProxyManager.canRetrieveInteractiveWindowsLocked()).thenReturn(false); + final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); userState.mAccessibilityShortcutKeyTargets.add(MAGNIFICATION_CONTROLLER_NAME); @@ -432,6 +458,8 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test public void testOnClientChange_boundServiceCanControlMagnification_requestConnection() { + when(mProxyManager.canRetrieveInteractiveWindowsLocked()).thenReturn(false); + setupAccessibilityServiceConnection(0); when(mMockSecurityPolicy.canControlMagnification(any())).thenReturn(true);