From fd24f601f82ec2bef7be00758228f19e3ec20283 Mon Sep 17 00:00:00 2001 From: Sally Date: Tue, 28 Feb 2023 01:41:08 +0000 Subject: [PATCH] Support separating A11yManagers by device id An AccessibilityManagerClient is now associated with a device id. Usually this is the default device (phone), but if a proxy is registered for a display that belongs to a virtual device, a client will be assosiated with that device's id, and updates will happen based on state held by the ProxyManager. When broadcasting to Clients, check the device id. Clients are "moved" back to the default device and updated with the current user state if 1. the virtual device is closed or 2.a proxy is unregistered and there are no more active proxies belonging to that device. onProxyChanged behaves like onClientChanged/onUserStateChanged to update the clients and some system state (like updating window tracking). All IAccessibilityManagerClient methods (except setUiContrast) are called by ProxyManager. Additional changes: - Remove onProxyChanged/onClientChanged in ProxyManager#registerProxy. Inform apps of a new proxy after the proxy's installed and enabled service infos have propagated (in ProxyA11yServiceConnection). - Call A11yInputFilter from the main thread, since it's not thread safe. - Add more dump logging See video in the comments Bug: 254545943 Test: atest AccessibilityDisplayProxyTest Change-Id: If4be657d9fe7a3cdba1437a1f2433b3fab5193b6 --- ...bstractAccessibilityServiceConnection.java | 10 +- .../AccessibilityManagerService.java | 292 ++++++-- .../ProxyAccessibilityServiceConnection.java | 59 +- .../server/accessibility/ProxyManager.java | 694 ++++++++++++++++-- ...oxyAccessibilityServiceConnectionTest.java | 5 +- 5 files changed, 900 insertions(+), 160 deletions(-) diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index e159f18809c40..7463061256b76 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -271,6 +271,14 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ */ void onClientChangeLocked(boolean serviceInfoChanged); + /** + * Called back to notify the system the proxy client for a device has changed. + * + * Changes include if the proxy is unregistered, if its service info list has changed, or if + * its focus appearance has changed. + */ + void onProxyChanged(int deviceId); + int getCurrentUserIdLocked(); Pair getWindowTransformationMatrixAndMagnificationSpec( @@ -315,8 +323,6 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ void attachAccessibilityOverlayToDisplay(int displayId, SurfaceControl sc); - void setCurrentUserFocusAppearance(int strokeWidth, int color); - } public AbstractAccessibilityServiceConnection(Context context, ComponentName componentName, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 5417009f92d45..51325e72204d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -25,6 +25,9 @@ import static android.accessibilityservice.AccessibilityTrace.FLAGS_PACKAGE_BROA import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCAST_RECEIVER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION; import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL; +import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; +import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; +import static android.content.Context.DEVICE_ID_DEFAULT; import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; @@ -189,7 +192,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub AccessibilityUserState.ServiceInfoChangeListener, AccessibilityWindowManager.AccessibilityEventSender, AccessibilitySecurityPolicy.AccessibilityUserManager, - SystemActionPerformer.SystemActionsChangedListener { + SystemActionPerformer.SystemActionsChangedListener, ProxyManager.SystemSupport{ private static final boolean DEBUG = false; @@ -471,7 +474,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub new MagnificationScaleProvider(mContext)); mMagnificationProcessor = new MagnificationProcessor(mMagnificationController); mCaptioningManagerImpl = new CaptioningManagerImpl(mContext); - mProxyManager = new ProxyManager(mLock, mA11yWindowManager, mContext); + mProxyManager = new ProxyManager(mLock, mA11yWindowManager, mContext, mMainHandler, + mUiAutomationManager, this); mFlashNotificationsController = new FlashNotificationsController(mContext); init(); } @@ -862,6 +866,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub }; mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, mMainHandler, Context.RECEIVER_EXPORTED); + + final BroadcastReceiver virtualDeviceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int deviceId = intent.getIntExtra( + EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT); + mProxyManager.clearConnections(deviceId); + } + }; + + final IntentFilter virtualDeviceFilter = new IntentFilter(ACTION_VIRTUAL_DEVICE_REMOVED); + mContext.registerReceiver(virtualDeviceReceiver, virtualDeviceFilter, + Context.RECEIVER_NOT_EXPORTED); } /** @@ -940,21 +957,42 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final int resolvedUserId = mSecurityPolicy .resolveCallingUserIdEnforcingPermissionsLocked(userId); + AccessibilityUserState userState = getUserStateLocked(resolvedUserId); + // Support a process moving from the default device to a single virtual + // device. + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + Client client = new Client(callback, Binder.getCallingUid(), userState, deviceId); // If the client is from a process that runs across users such as // the system UI or the system we add it to the global state that // is shared across users. - AccessibilityUserState userState = getUserStateLocked(resolvedUserId); - Client client = new Client(callback, Binder.getCallingUid(), userState); if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) { + if (mProxyManager.isProxyedDeviceId(deviceId)) { + if (DEBUG) { + Slog.v(LOG_TAG, "Added global client for proxy-ed pid: " + + Binder.getCallingPid() + " for device id " + deviceId + + " with package names " + Arrays.toString(client.mPackageNames)); + } + return IntPair.of(mProxyManager.getStateLocked(deviceId, + mUiAutomationManager.isUiAutomationRunningLocked()), + client.mLastSentRelevantEventTypes); + } mGlobalClients.register(callback, client); if (DEBUG) { Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid()); } - return IntPair.of( - combineUserStateAndProxyState(getClientStateLocked(userState), - mProxyManager.getStateLocked()), - client.mLastSentRelevantEventTypes); } else { + // If the display belongs to a proxy connections + if (mProxyManager.isProxyedDeviceId(deviceId)) { + if (DEBUG) { + Slog.v(LOG_TAG, "Added user client for proxy-ed pid: " + + Binder.getCallingPid() + " for device id " + deviceId + + " with package names " + Arrays.toString(client.mPackageNames)); + } + return IntPair.of(mProxyManager.getStateLocked(deviceId, + mUiAutomationManager.isUiAutomationRunningLocked()), + client.mLastSentRelevantEventTypes); + } userState.mUserClients.register(callback, client); // If this client is not for the current user we do not // return a state since it is not for the foreground user. @@ -963,12 +1001,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Slog.i(LOG_TAG, "Added user client for pid:" + Binder.getCallingPid() + " and userId:" + mCurrentUserId); } - return IntPair.of( - (resolvedUserId == mCurrentUserId) ? combineUserStateAndProxyState( - getClientStateLocked(userState), mProxyManager.getStateLocked()) - : 0, - client.mLastSentRelevantEventTypes); } + return IntPair.of( + (resolvedUserId == mCurrentUserId) ? getClientStateLocked(userState) : 0, + client.mLastSentRelevantEventTypes); } } @@ -1094,7 +1130,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private void dispatchAccessibilityEventLocked(AccessibilityEvent event) { - if (mProxyManager.isProxyed(event.getDisplayId())) { + if (mProxyManager.isProxyedDisplay(event.getDisplayId())) { mProxyManager.sendAccessibilityEventLocked(event); } else { notifyAccessibilityServicesDelayedLocked(event, false); @@ -1160,6 +1196,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final int resolvedUserId; final List serviceInfos; synchronized (mLock) { + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + return mProxyManager.getInstalledAndEnabledServiceInfosLocked( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK, deviceId); + } // We treat calls from a profile as if made by its parent as profiles // share the accessibility state of the parent. The call below // performs the current profile parent resolution. @@ -1195,6 +1237,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } synchronized (mLock) { + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + return mProxyManager.getInstalledAndEnabledServiceInfosLocked(feedbackType, + deviceId); + } // We treat calls from a profile as if made by its parent as profiles // share the accessibility state of the parent. The call below // performs the current profile parent resolution. @@ -1239,19 +1287,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (resolvedUserId != mCurrentUserId) { return; } - List services = - getUserStateLocked(resolvedUserId).mBoundServices; - int numServices = services.size() + mProxyManager.getNumProxysLocked(); - interfacesToInterrupt = new ArrayList<>(numServices); - for (int i = 0; i < services.size(); i++) { - AccessibilityServiceConnection service = services.get(i); - IBinder a11yServiceBinder = service.mService; - IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface; - if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) { - interfacesToInterrupt.add(a11yServiceInterface); + + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + interfacesToInterrupt = new ArrayList<>(); + mProxyManager.addServiceInterfacesLocked(interfacesToInterrupt, deviceId); + } else { + List services = + getUserStateLocked(resolvedUserId).mBoundServices; + interfacesToInterrupt = new ArrayList<>(services.size()); + for (int i = 0; i < services.size(); i++) { + AccessibilityServiceConnection service = services.get(i); + IBinder a11yServiceBinder = service.mService; + IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface; + if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) { + interfacesToInterrupt.add(a11yServiceInterface); + } } } - mProxyManager.addServiceInterfacesLocked(interfacesToInterrupt); } for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) { try { @@ -1827,7 +1881,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return result; } - private void notifyClearAccessibilityCacheLocked() { + @Override + public void notifyClearAccessibilityCacheLocked() { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { AccessibilityServiceConnection service = state.mBoundServices.get(i); @@ -2031,18 +2086,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mMainHandler.post(() -> { broadcastToClients(userState, ignoreRemoteException(client -> { int relevantEventTypes; - boolean changed = false; synchronized (mLock) { relevantEventTypes = computeRelevantEventTypesLocked(userState, client); - - if (client.mLastSentRelevantEventTypes != relevantEventTypes) { - client.mLastSentRelevantEventTypes = relevantEventTypes; - changed = true; + if (!mProxyManager.isProxyedDeviceId(client.mDeviceId)) { + if (client.mLastSentRelevantEventTypes != relevantEventTypes) { + client.mLastSentRelevantEventTypes = relevantEventTypes; + client.mCallback.setRelevantEventTypes(relevantEventTypes); + } } } - if (changed) { - client.mCallback.setRelevantEventTypes(relevantEventTypes); - } })); }); } @@ -2062,7 +2114,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mUiAutomationManager.getServiceInfo(), client) ? mUiAutomationManager.getRelevantEventTypes() : 0; - relevantEventTypes |= mProxyManager.getRelevantEventTypesLocked(); return relevantEventTypes; } @@ -2116,7 +2167,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private static boolean isClientInPackageAllowlist( + static boolean isClientInPackageAllowlist( @Nullable AccessibilityServiceInfo serviceInfo, Client client) { if (serviceInfo == null) return false; @@ -2309,24 +2360,20 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub updateAccessibilityEnabledSettingLocked(userState); } - private int combineUserStateAndProxyState(int userState, int proxyState) { - return userState | proxyState; + void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState) { + scheduleUpdateClientsIfNeededLocked(userState, false); } - void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState) { + void scheduleUpdateClientsIfNeededLocked(AccessibilityUserState userState, + boolean forceUpdate) { final int clientState = getClientStateLocked(userState); - final int proxyState = mProxyManager.getStateLocked(); - if ((userState.getLastSentClientStateLocked() != clientState - || mProxyManager.getLastSentStateLocked() != proxyState) + if (((userState.getLastSentClientStateLocked() != clientState || forceUpdate)) && (mGlobalClients.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, combineUserStateAndProxyState(clientState, proxyState), + this, clientState, userState.mUserId)); } } @@ -2346,8 +2393,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mTraceManager.logTrace(LOG_TAG + ".sendStateToClients", FLAGS_ACCESSIBILITY_MANAGER_CLIENT, "clientState=" + clientState); } - clients.broadcast(ignoreRemoteException( - client -> client.setState(clientState))); + clients.broadcastForEachCookie(ignoreRemoteException( + client -> { + Client managerClient = ((Client) client); + if (!mProxyManager.isProxyedDeviceId(managerClient.mDeviceId)) { + managerClient.mCallback.setState(clientState); + } + })); } private void scheduleNotifyClientsOfServicesStateChangeLocked( @@ -2370,8 +2422,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mTraceManager.logTrace(LOG_TAG + ".notifyClientsOfServicesStateChange", FLAGS_ACCESSIBILITY_MANAGER_CLIENT, "uiTimeout=" + uiTimeout); } - clients.broadcast(ignoreRemoteException( - client -> client.notifyServicesStateChanged(uiTimeout))); + + clients.broadcastForEachCookie(ignoreRemoteException( + client -> { + Client managerClient = ((Client) client); + if (!mProxyManager.isProxyedDeviceId(managerClient.mDeviceId)) { + managerClient.mCallback.notifyServicesStateChanged(uiTimeout); + } + })); } private void scheduleUpdateInputFilter(AccessibilityUserState userState) { @@ -2444,7 +2502,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } inputFilter = mInputFilter; setInputFilter = true; - mProxyManager.setAccessibilityInputFilter(mInputFilter); } mInputFilter.setUserAndEnabledFeatures(userState.mUserId, flags); mInputFilter.setCombinedGenericMotionEventSources( @@ -2477,6 +2534,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub "inputFilter=" + inputFilter); } mWindowManagerService.setInputFilter(inputFilter); + mProxyManager.setAccessibilityInputFilter(inputFilter); } } @@ -2541,6 +2599,20 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * @param userState the new user state */ private void onUserStateChangedLocked(AccessibilityUserState userState) { + onUserStateChangedLocked(userState, false); + } + + /** + * Called when any property of the user state has changed. + * + * @param userState the new user state + * @param forceUpdate whether to force an update of the app Clients. + */ + private void onUserStateChangedLocked(AccessibilityUserState userState, boolean forceUpdate) { + if (DEBUG) { + Slog.v(LOG_TAG, "onUserStateChangedLocked for user " + userState.mUserId + " with " + + "forceUpdate: " + forceUpdate); + } // TODO: Remove this hack mInitialized = true; updateLegacyCapabilitiesLocked(userState); @@ -2553,7 +2625,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub scheduleUpdateFingerprintGestureHandling(userState); scheduleUpdateInputFilter(userState); updateRelevantEventsLocked(userState); - scheduleUpdateClientsIfNeededLocked(userState); + scheduleUpdateClientsIfNeededLocked(userState, forceUpdate); updateAccessibilityShortcutKeyTargetsLocked(userState); updateAccessibilityButtonTargetsLocked(userState); // Update the capabilities before the mode because we will check the current mode is @@ -2600,7 +2672,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (display != null) { if (observingWindows) { mA11yWindowManager.startTrackingWindows(display.getDisplayId(), - mProxyManager.isProxyed(display.getDisplayId())); + mProxyManager.isProxyedDisplay(display.getDisplayId())); } else { mA11yWindowManager.stopTrackingWindows(display.getDisplayId()); } @@ -2867,6 +2939,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_INTERACTIVE_UI_TIMEOUT_MS, 0, userState.mUserId); + + mProxyManager.updateTimeoutsIfNeeded(nonInteractiveUiTimeout, interactiveUiTimeout); if (nonInteractiveUiTimeout != userState.getUserNonInteractiveUiTimeoutLocked() || interactiveUiTimeout != userState.getUserInteractiveUiTimeoutLocked()) { userState.setUserNonInteractiveUiTimeoutLocked(nonInteractiveUiTimeout); @@ -3632,8 +3706,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } synchronized(mLock) { - final AccessibilityUserState userState = getCurrentUserStateLocked(); - return getRecommendedTimeoutMillisLocked(userState); + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + return mProxyManager.getRecommendedTimeoutMillisLocked(deviceId); + } else { + final AccessibilityUserState userState = getCurrentUserStateLocked(); + return getRecommendedTimeoutMillisLocked(userState); + } } } @@ -3712,6 +3792,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mTraceManager.logTrace(LOG_TAG + ".getFocusStrokeWidth", FLAGS_ACCESSIBILITY_MANAGER); } synchronized (mLock) { + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + return mProxyManager.getFocusStrokeWidthLocked(deviceId); + } final AccessibilityUserState userState = getCurrentUserStateLocked(); return userState.getFocusStrokeWidthLocked(); @@ -3728,6 +3813,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mTraceManager.logTrace(LOG_TAG + ".getFocusColor", FLAGS_ACCESSIBILITY_MANAGER); } synchronized (mLock) { + final int deviceId = mProxyManager.getFirstDeviceIdForUidLocked( + Binder.getCallingUid()); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + return mProxyManager.getFocusColorLocked(deviceId); + } final AccessibilityUserState userState = getCurrentUserStateLocked(); return userState.getFocusColorLocked(); @@ -3814,9 +3904,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub throw new IllegalArgumentException("The display " + displayId + " does not exist or is" + " not tracked by accessibility."); } - if (mProxyManager.isProxyed(displayId)) { + if (mProxyManager.isProxyedDisplay(displayId)) { throw new IllegalArgumentException("The display " + displayId + " is already being" - + "proxy-ed"); + + " proxy-ed"); } final long identity = Binder.clearCallingIdentity(); @@ -3847,7 +3937,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } boolean isDisplayProxyed(int displayId) { - return mProxyManager.isProxyed(displayId); + return mProxyManager.isProxyedDisplay(displayId); } @Override @@ -3995,13 +4085,71 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public void onClientChangeLocked(boolean serviceInfoChanged) { + onClientChangeLocked(serviceInfoChanged, false); + } + + /** + * Called when the state of a service or proxy has changed + * @param serviceInfoChanged if the service info has changed + * @param forceUpdate whether to force an update of state for app clients + */ + public void onClientChangeLocked(boolean serviceInfoChanged, boolean forceUpdate) { AccessibilityUserState userState = getUserStateLocked(mCurrentUserId); - onUserStateChangedLocked(userState); + onUserStateChangedLocked(userState, forceUpdate); if (serviceInfoChanged) { scheduleNotifyClientsOfServicesStateChangeLocked(userState); } } + + @Override + public void onProxyChanged(int deviceId) { + mProxyManager.onProxyChanged(deviceId); + } + + /** + * Removes the device from tracking. This will reset any AccessibilityManagerClients to be + * associated with the default user id. + */ + @Override + public void removeDeviceIdLocked(int deviceId) { + resetClientsLocked(deviceId, getCurrentUserStateLocked().mUserClients); + resetClientsLocked(deviceId, mGlobalClients); + // Force an update of A11yManagers if the state was previously a proxy state and needs to be + // returned to the default device state. + onClientChangeLocked(true, true); + } + + private void resetClientsLocked(int deviceId, + RemoteCallbackList clients) { + if (clients == null || clients.getRegisteredCallbackCount() == 0) { + return; + } + synchronized (mLock) { + for (int i = 0; i < clients.getRegisteredCallbackCount(); i++) { + final Client appClient = ((Client) clients.getRegisteredCallbackCookie(i)); + if (appClient.mDeviceId == deviceId) { + appClient.mDeviceId = DEVICE_ID_DEFAULT; + } + } + } + } + + @Override + public void updateWindowsForAccessibilityCallbackLocked() { + updateWindowsForAccessibilityCallbackLocked(getUserStateLocked(mCurrentUserId)); + } + + @Override + public RemoteCallbackList getGlobalClientsLocked() { + return mGlobalClients; + } + + @Override + public RemoteCallbackList getCurrentUserClientsLocked() { + return getCurrentUserState().mUserClients; + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, @@ -4313,13 +4461,22 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final IAccessibilityManagerClient mCallback; final String[] mPackageNames; int mLastSentRelevantEventTypes; + int mUid; + int mDeviceId = DEVICE_ID_DEFAULT; private Client(IAccessibilityManagerClient callback, int clientUid, - AccessibilityUserState userState) { + AccessibilityUserState userState, int deviceId) { mCallback = callback; mPackageNames = mPackageManager.getPackagesForUid(clientUid); + mUid = clientUid; + mDeviceId = deviceId; synchronized (mLock) { - mLastSentRelevantEventTypes = computeRelevantEventTypesLocked(userState, this); + if (mProxyManager.isProxyedDeviceId(deviceId)) { + mLastSentRelevantEventTypes = + mProxyManager.computeRelevantEventTypesLocked(this); + } else { + mLastSentRelevantEventTypes = computeRelevantEventTypesLocked(userState, this); + } } } } @@ -4805,8 +4962,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.post(() -> { broadcastToClients(userState, ignoreRemoteException(client -> { - client.mCallback.setFocusAppearance(userState.getFocusStrokeWidthLocked(), - userState.getFocusColorLocked()); + if (!mProxyManager.isProxyedDeviceId(client.mDeviceId)) { + client.mCallback.setFocusAppearance(userState.getFocusStrokeWidthLocked(), + userState.getFocusColorLocked()); + } })); }); @@ -5041,11 +5200,4 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub transaction.apply(); transaction.close(); } - - @Override - public void setCurrentUserFocusAppearance(int strokeWidth, int color) { - synchronized (mLock) { - getCurrentUserStateLocked().setFocusAppearanceLocked(strokeWidth, color); - } - } } diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index b19a502547ab1..ab01fc324ed2e 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -67,6 +67,7 @@ import java.util.Set; public class ProxyAccessibilityServiceConnection extends AccessibilityServiceConnection { private static final String LOG_TAG = "ProxyAccessibilityServiceConnection"; + private int mDeviceId; private int mDisplayId; private List mInstalledAndEnabledServices; @@ -75,6 +76,9 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon /** The color of the focus rectangle */ private int mFocusColor; + private int mInteractiveTimeout; + private int mNonInteractiveTimeout; + ProxyAccessibilityServiceConnection( Context context, ComponentName componentName, @@ -83,7 +87,7 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon AccessibilitySecurityPolicy securityPolicy, SystemSupport systemSupport, AccessibilityTrace trace, WindowManagerInternal windowManagerInternal, - AccessibilityWindowManager awm, int displayId) { + AccessibilityWindowManager awm, int displayId, int deviceId) { super(/* userState= */null, context, componentName, accessibilityServiceInfo, id, mainHandler, lock, securityPolicy, systemSupport, trace, windowManagerInternal, /* systemActionPerformer= */ null, awm, /* activityTaskManagerService= */ null); @@ -93,6 +97,14 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon R.dimen.accessibility_focus_highlight_stroke_width); mFocusColor = mContext.getResources().getColor( R.color.accessibility_focus_highlight_color); + mDeviceId = deviceId; + } + + int getDisplayId() { + return mDisplayId; + } + int getDeviceId() { + return mDeviceId; } /** @@ -155,6 +167,8 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon proxyInfo.setAccessibilityTool(isAccessibilityTool); proxyInfo.setInteractiveUiTimeoutMillis(interactiveUiTimeout); proxyInfo.setNonInteractiveUiTimeoutMillis(nonInteractiveUiTimeout); + mInteractiveTimeout = interactiveUiTimeout; + mNonInteractiveTimeout = nonInteractiveUiTimeout; // If any one service info doesn't set package names, i.e. if it's interested in all // apps, the proxy shouldn't filter by package name even if some infos specify this. @@ -167,7 +181,7 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon // Update connection with mAccessibilityServiceInfo values. setDynamicallyConfigurableProperties(proxyInfo); // Notify manager service. - mSystemSupport.onClientChangeLocked(true); + mSystemSupport.onProxyChanged(mDeviceId); } } finally { Binder.restoreCallingIdentity(identity); @@ -235,12 +249,7 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon mFocusStrokeWidth = strokeWidth; mFocusColor = color; - // Sets the appearance data in the A11yUserState for now, since the A11yManagers are not - // separated. - // TODO(254545943): Separate proxy and non-proxy states so the focus appearance on the - // phone is not affected by the appearance of a proxy-ed app. - mSystemSupport.setCurrentUserFocusAppearance(mFocusStrokeWidth, mFocusColor); - mSystemSupport.onClientChangeLocked(false); + mSystemSupport.onProxyChanged(mDeviceId); } } @@ -572,17 +581,51 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon throw new UnsupportedOperationException("setAnimationScale is not supported"); } + public int getInteractiveTimeout() { + return mInteractiveTimeout; + } + + public int getNonInteractiveTimeout() { + return mNonInteractiveTimeout; + } + + /** + * Returns true if a timeout was updated. + */ + public boolean updateTimeouts(int nonInteractiveUiTimeout, int interactiveUiTimeout) { + final int newInteractiveUiTimeout = interactiveUiTimeout != 0 + ? interactiveUiTimeout + : mAccessibilityServiceInfo.getInteractiveUiTimeoutMillis(); + final int newNonInteractiveUiTimeout = nonInteractiveUiTimeout != 0 + ? nonInteractiveUiTimeout + : mAccessibilityServiceInfo.getNonInteractiveUiTimeoutMillis(); + boolean updated = false; + + if (mInteractiveTimeout != newInteractiveUiTimeout) { + mInteractiveTimeout = newInteractiveUiTimeout; + updated = true; + } + if (mNonInteractiveTimeout != newNonInteractiveUiTimeout) { + mNonInteractiveTimeout = newNonInteractiveUiTimeout; + updated = true; + } + return updated; + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; synchronized (mLock) { pw.append("Proxy[displayId=" + mDisplayId); + pw.append(", deviceId=" + mDeviceId); pw.append(", feedbackType" + AccessibilityServiceInfo.feedbackTypeToString(mFeedbackType)); pw.append(", capabilities=" + mAccessibilityServiceInfo.getCapabilities()); pw.append(", eventTypes=" + AccessibilityEvent.eventTypeToString(mEventTypes)); pw.append(", notificationTimeout=" + mNotificationTimeout); + pw.append(", nonInteractiveUiTimeout=").append(String.valueOf(mNonInteractiveTimeout)); + pw.append(", interactiveUiTimeout=").append(String.valueOf(mInteractiveTimeout)); pw.append(", focusStrokeWidth=").append(String.valueOf(mFocusStrokeWidth)); pw.append(", focusColor=").append(String.valueOf(mFocusColor)); pw.append(", installedAndEnabledServiceCount=").append(String.valueOf( diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java index e258de16caf52..d417197e1419c 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java @@ -14,26 +14,45 @@ * limitations under the License. */ package com.android.server.accessibility; + +import static android.content.Context.DEVICE_ID_DEFAULT; +import static android.content.Context.DEVICE_ID_INVALID; + +import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; + import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityTrace; import android.accessibilityservice.IAccessibilityServiceClient; +import android.annotation.NonNull; +import android.companion.virtual.VirtualDeviceManager; import android.content.ComponentName; import android.content.Context; import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.IAccessibilityManagerClient; +import com.android.internal.util.IntPair; +import com.android.server.LocalServices; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; /** * Manages proxy connections. @@ -53,26 +72,72 @@ public class ProxyManager { static final String PROXY_COMPONENT_PACKAGE_NAME = "ProxyPackage"; static final String PROXY_COMPONENT_CLASS_NAME = "ProxyClass"; + // AMS#mLock private final Object mLock; private final Context mContext; + private final Handler mMainHandler; - // 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 final UiAutomationManager mUiAutomationManager; - private SparseArray mProxyA11yServiceConnections = + // Device Id -> state. Used to determine if we should notify AccessibilityManager clients of + // updates. + private final SparseIntArray mLastStates = new SparseIntArray(); + + // Each display id entry in a SparseArray represents a proxy a11y user. + private final SparseArray mProxyA11yServiceConnections = new SparseArray<>(); - private AccessibilityWindowManager mA11yWindowManager; + private final AccessibilityWindowManager mA11yWindowManager; private AccessibilityInputFilter mA11yInputFilter; - ProxyManager(Object lock, AccessibilityWindowManager awm, Context context) { + private VirtualDeviceManagerInternal mLocalVdm; + + private final SystemSupport mSystemSupport; + + /** + * Callbacks into AccessibilityManagerService. + */ + public interface SystemSupport { + /** + * Removes the device id from tracking. + */ + void removeDeviceIdLocked(int deviceId); + + /** + * Updates the windows tracking for the current user. + */ + void updateWindowsForAccessibilityCallbackLocked(); + + /** + * Clears all caches. + */ + void notifyClearAccessibilityCacheLocked(); + + /** + * Gets the clients for all users. + */ + @NonNull + RemoteCallbackList getGlobalClientsLocked(); + + /** + * Gets the clients for the current user. + */ + @NonNull + RemoteCallbackList getCurrentUserClientsLocked(); + } + + ProxyManager(Object lock, AccessibilityWindowManager awm, + Context context, Handler mainHandler, UiAutomationManager uiAutomationManager, + SystemSupport systemSupport) { mLock = lock; mA11yWindowManager = awm; mContext = context; + mMainHandler = mainHandler; + mUiAutomationManager = uiAutomationManager; + mSystemSupport = systemSupport; + mLocalVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); } /** @@ -89,6 +154,12 @@ public class ProxyManager { Slog.v(LOG_TAG, "Register proxy for display id: " + displayId); } + VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class); + if (vdm == null) { + return; + } + final int deviceId = vdm.getDeviceIdForDisplayId(displayId); + // 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(); @@ -101,7 +172,7 @@ public class ProxyManager { new ProxyAccessibilityServiceConnection(context, info.getComponentName(), info, id, mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal, - mA11yWindowManager, displayId); + mA11yWindowManager, displayId, deviceId); synchronized (mLock) { mProxyA11yServiceConnections.put(displayId, connection); @@ -113,20 +184,16 @@ public class ProxyManager { @Override public void binderDied() { client.asBinder().unlinkToDeath(this, 0); - clearConnection(displayId); + clearConnectionAndUpdateState(displayId); } }; client.asBinder().linkToDeath(deathRecipient, 0); - // Notify apps that the service state has changed. - // A11yManager#A11yServicesStateChangeListener - synchronized (mLock) { - connection.mSystemSupport.onClientChangeLocked(true); - } - - if (mA11yInputFilter != null) { - mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId); - } + mMainHandler.post(() -> { + if (mA11yInputFilter != null) { + mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId); + } + }); connection.initializeServiceInterface(client); } @@ -134,38 +201,101 @@ public class ProxyManager { * Unregister the proxy based on display id. */ public boolean unregisterProxy(int displayId) { - return clearConnection(displayId); - } - - private boolean clearConnection(int displayId) { - boolean removed = false; - synchronized (mLock) { - if (mProxyA11yServiceConnections.contains(displayId)) { - mProxyA11yServiceConnections.remove(displayId); - removed = true; - if (DEBUG) { - Slog.v(LOG_TAG, "Unregister proxy for display id " + displayId); - } - } - } - if (removed) { - mA11yWindowManager.stopTrackingDisplayProxy(displayId); - if (mA11yInputFilter != null) { - final DisplayManager displayManager = (DisplayManager) - mContext.getSystemService(Context.DISPLAY_SERVICE); - final Display proxyDisplay = displayManager.getDisplay(displayId); - if (proxyDisplay != null) { - mA11yInputFilter.enableFeaturesForDisplayIfInstalled(proxyDisplay); - } - } - } - return removed; + return clearConnectionAndUpdateState(displayId); } /** - * Checks if a display id is being proxy-ed. + * Clears all proxy connections belonging to {@code deviceId}. */ - public boolean isProxyed(int displayId) { + public void clearConnections(int deviceId) { + final IntArray displaysToClear = new IntArray(); + synchronized (mLock) { + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy != null && proxy.getDeviceId() == deviceId) { + displaysToClear.add(proxy.getDisplayId()); + } + } + } + for (int i = 0; i < displaysToClear.size(); i++) { + clearConnectionAndUpdateState(displaysToClear.get(i)); + } + } + + /** + * Removes the system connection of an AccessibilityDisplayProxy. + * + * This will: + *
    + *
  • Reset Clients to belong to the default device if appropriate. + *
  • Stop identifying the display's a11y windows as belonging to a proxy. + *
  • Re-enable any input filters for the display. + *
  • Notify AMS that a proxy has been removed. + *
+ * + * @param displayId the display id of the connection to be cleared. + * @return whether the proxy was removed. + */ + private boolean clearConnectionAndUpdateState(int displayId) { + boolean removedFromConnections = false; + int deviceId = DEVICE_ID_INVALID; + synchronized (mLock) { + if (mProxyA11yServiceConnections.contains(displayId)) { + deviceId = mProxyA11yServiceConnections.get(displayId).getDeviceId(); + mProxyA11yServiceConnections.remove(displayId); + removedFromConnections = true; + } + } + + if (removedFromConnections) { + updateStateForRemovedDisplay(displayId, deviceId); + } + + if (DEBUG) { + Slog.v(LOG_TAG, "Unregistered proxy for display id " + displayId + ": " + + removedFromConnections); + } + return removedFromConnections; + } + + /** + * When the connection is removed from tracking in ProxyManager, propagate changes to other a11y + * system components like the input filter and IAccessibilityManagerClients. + */ + public void updateStateForRemovedDisplay(int displayId, int deviceId) { + mA11yWindowManager.stopTrackingDisplayProxy(displayId); + // A11yInputFilter isn't thread-safe, so post on the system thread. + mMainHandler.post( + () -> { + if (mA11yInputFilter != null) { + final DisplayManager displayManager = (DisplayManager) + mContext.getSystemService(Context.DISPLAY_SERVICE); + final Display proxyDisplay = displayManager.getDisplay(displayId); + if (proxyDisplay != null) { + // A11yInputFilter isn't thread-safe, so post on the system thread. + mA11yInputFilter.enableFeaturesForDisplayIfInstalled(proxyDisplay); + } + } + }); + // If there isn't an existing proxy for the device id, reset clients. Resetting + // will usually happen, since in most cases there will only be one proxy for a + // device. + if (!isProxyedDeviceId(deviceId)) { + synchronized (mLock) { + mSystemSupport.removeDeviceIdLocked(deviceId); + mLastStates.delete(deviceId); + } + } else { + // Update with the states of the remaining proxies. + onProxyChanged(deviceId); + } + } + + /** + * Returns {@code true} if {@code displayId} is being proxy-ed. + */ + public boolean isProxyedDisplay(int displayId) { synchronized (mLock) { final boolean tracked = mProxyA11yServiceConnections.contains(displayId); if (DEBUG) { @@ -175,6 +305,23 @@ public class ProxyManager { } } + /** + * Returns {@code true} if {@code deviceId} is being proxy-ed. + */ + public boolean isProxyedDeviceId(int deviceId) { + if (deviceId == DEVICE_ID_DEFAULT && deviceId == DEVICE_ID_INVALID) { + return false; + } + boolean isTrackingDeviceId; + synchronized (mLock) { + isTrackingDeviceId = getFirstProxyForDeviceIdLocked(deviceId) != null; + } + if (DEBUG) { + Slog.v(LOG_TAG, "Tracking device " + deviceId + " : " + isTrackingDeviceId); + } + return isTrackingDeviceId; + } + /** * Sends AccessibilityEvents to a proxy given the event's displayId. */ @@ -213,15 +360,37 @@ public class ProxyManager { /** * If there is at least one proxy, accessibility is enabled. */ - public int getStateLocked() { + public int getStateLocked(int deviceId, boolean automationRunning) { int clientState = 0; - final boolean a11yEnabled = mProxyA11yServiceConnections.size() > 0; - if (a11yEnabled) { + if (automationRunning) { clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED; } for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { final ProxyAccessibilityServiceConnection proxy = mProxyA11yServiceConnections.valueAt(i); + if (proxy != null && proxy.getDeviceId() == deviceId) { + // Combine proxy states. + clientState |= getStateForDisplayIdLocked(proxy); + } + } + + if (DEBUG) { + Slog.v(LOG_TAG, "For device id " + deviceId + " a11y is enabled: " + + ((clientState & AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED) != 0)); + Slog.v(LOG_TAG, "For device id " + deviceId + " touch exploration is enabled: " + + ((clientState & AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED) + != 0)); + } + return clientState; + } + + /** + * If there is at least one proxy, accessibility is enabled. + */ + public int getStateForDisplayIdLocked(ProxyAccessibilityServiceConnection proxy) { + int clientState = 0; + if (proxy != null) { + clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED; if (proxy.mRequestTouchExplorationMode) { clientState |= AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED; } @@ -235,61 +404,396 @@ public class ProxyManager { != 0)); } return clientState; - // TODO(b/254545943): When A11yManager is separated, include support for other properties. } /** - * Gets the last state. + * Gets the last state for a device. */ - public int getLastSentStateLocked() { - return mLastState; + public int getLastSentStateLocked(int deviceId) { + return mLastStates.get(deviceId, 0); } /** - * Sets the last state. + * Sets the last state for a device. */ - public void setLastStateLocked(int proxyState) { - mLastState = proxyState; + public void setLastStateLocked(int deviceId, int proxyState) { + mLastStates.put(deviceId, proxyState); } /** - * Returns the relevant event types of every proxy. - * TODO(254545943): When A11yManager is separated, return based on the A11yManager display. + * Updates the relevant event types of the app clients that are shown on a display owned by the + * specified device. + * + * A client belongs to a device id, so event types (and other state) is determined by the device + * id. In most cases, a device owns a single display. But if multiple displays may belong to one + * Virtual Device, the app clients will get the aggregated event types for all proxy-ed displays + * belonging to a VirtualDevice. */ - public int getRelevantEventTypesLocked() { + public void updateRelevantEventTypesLocked(int deviceId) { + if (!isProxyedDeviceId(deviceId)) { + return; + } + mMainHandler.post(() -> { + synchronized (mLock) { + broadcastToClientsLocked(ignoreRemoteException(client -> { + int relevantEventTypes; + if (client.mDeviceId == deviceId) { + relevantEventTypes = computeRelevantEventTypesLocked(client); + if (client.mLastSentRelevantEventTypes != relevantEventTypes) { + client.mLastSentRelevantEventTypes = relevantEventTypes; + client.mCallback.setRelevantEventTypes(relevantEventTypes); + } + } + })); + } + }); + } + + /** + * Returns the relevant event types for a Client. + */ + int computeRelevantEventTypesLocked(AccessibilityManagerService.Client client) { int relevantEventTypes = 0; for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { - ProxyAccessibilityServiceConnection proxy = + final ProxyAccessibilityServiceConnection proxy = mProxyA11yServiceConnections.valueAt(i); - relevantEventTypes |= proxy.getRelevantEventTypes(); + if (proxy != null && proxy.getDeviceId() == client.mDeviceId) { + relevantEventTypes |= proxy.getRelevantEventTypes(); + relevantEventTypes |= AccessibilityManagerService.isClientInPackageAllowlist( + mUiAutomationManager.getServiceInfo(), client) + ? mUiAutomationManager.getRelevantEventTypes() + : 0; + } } if (DEBUG) { - Slog.v(LOG_TAG, "Relevant event types for all proxies: " - + AccessibilityEvent.eventTypeToString(relevantEventTypes)); + Slog.v(LOG_TAG, "Relevant event types for device id " + client.mDeviceId + + ": " + AccessibilityEvent.eventTypeToString(relevantEventTypes)); } return relevantEventTypes; } - /** - * Gets the number of current proxy connections. - * @return - */ - public int getNumProxysLocked() { - return mProxyA11yServiceConnections.size(); - } - /** * Adds the service interfaces to a list. - * @param interfaces + * @param interfaces the list to add to. + * @param deviceId the device id of the interested app client. */ - public void addServiceInterfacesLocked(List interfaces) { + public void addServiceInterfacesLocked(@NonNull List interfaces, + int deviceId) { 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); + if (proxy != null && proxy.getDeviceId() == deviceId) { + final IBinder proxyBinder = proxy.mService; + final IAccessibilityServiceClient proxyInterface = proxy.mServiceInterface; + if ((proxyBinder != null) && (proxyInterface != null)) { + interfaces.add(proxyInterface); + } + } + } + } + + /** + * Gets the list of installed and enabled services for a device id. + * + * Note: Multiple display proxies may belong to the same device. + */ + public List getInstalledAndEnabledServiceInfosLocked(int feedbackType, + int deviceId) { + List serviceInfos = new ArrayList<>(); + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy != null && proxy.getDeviceId() == deviceId) { + // Return all proxy infos for ALL mask. + if (feedbackType == AccessibilityServiceInfo.FEEDBACK_ALL_MASK) { + serviceInfos.addAll(proxy.getInstalledAndEnabledServices()); + } else if ((proxy.mFeedbackType & feedbackType) != 0) { + List proxyInfos = + proxy.getInstalledAndEnabledServices(); + // Iterate through each info in the proxy. + for (AccessibilityServiceInfo info : proxyInfos) { + if ((info.feedbackType & feedbackType) != 0) { + serviceInfos.add(info); + } + } + } + } + } + return serviceInfos; + } + + /** + * Handles proxy changes. + * + *

+ * Changes include if the proxy is unregistered, its service info list has + * changed, or its focus appearance has changed. + *

+ * Some responses may include updating app clients. A client belongs to a device id, so state is + * determined by the device id. In most cases, a device owns a single display. But if multiple + * displays belong to one Virtual Device, the app clients will get a difference in + * behavior depending on what is being updated. + * + * The following state methods are updated for AccessibilityManager clients belonging to a + * proxied device: + *

    + *
  • A11yManager#setRelevantEventTypes - The combined event types of all proxies belonging to + * a device id. + *
  • A11yManager#setState - The combined states of all proxies belonging to a device id. + *
  • A11yManager#notifyServicesStateChanged(timeout) - The highest of all proxies belonging + * to a device id. + *
  • A11yManager#setFocusAppearance - The appearance of the most recently updated display id + * belonging to the device. + *
+ * This is similar to onUserStateChangeLocked and onClientChangeLocked, but does not require an + * A11yUserState and only checks proxy-relevant settings. + */ + public void onProxyChanged(int deviceId) { + if (DEBUG) { + Slog.v(LOG_TAG, "onProxyChanged called for deviceId: " + deviceId); + } + //The following state updates are excluded: + // - Input-related state + // - Primary-device / hardware-specific state + synchronized (mLock) { + // A proxy may be registered after the client has been initialized in #addClient. + // For example, a user does not turn on accessibility until after the app has launched. + // Or the process was started with a default id context and should shift to a device. + // Update device ids of the clients if necessary. + updateDeviceIdsIfNeededLocked(deviceId); + // Start tracking of all displays if necessary. + mSystemSupport.updateWindowsForAccessibilityCallbackLocked(); + // Calls A11yManager#setRelevantEventTypes (test these) + updateRelevantEventTypesLocked(deviceId); + // Calls A11yManager#setState + scheduleUpdateProxyClientsIfNeededLocked(deviceId); + //Calls A11yManager#notifyServicesStateChanged(timeout) + scheduleNotifyProxyClientsOfServicesStateChangeLocked(deviceId); + // Calls A11yManager#setFocusAppearance + updateFocusAppearanceLocked(deviceId); + mSystemSupport.notifyClearAccessibilityCacheLocked(); + } + } + + /** + * Updates the states of the app AccessibilityManagers. + */ + public void scheduleUpdateProxyClientsIfNeededLocked(int deviceId) { + final int proxyState = getStateLocked(deviceId, + mUiAutomationManager.isUiAutomationRunningLocked()); + if (DEBUG) { + Slog.v(LOG_TAG, "State for device id " + deviceId + " is " + proxyState); + Slog.v(LOG_TAG, "Last state for device id " + deviceId + " is " + + getLastSentStateLocked(deviceId)); + } + if ((getLastSentStateLocked(deviceId)) != proxyState) { + setLastStateLocked(deviceId, proxyState); + mMainHandler.post(() -> { + synchronized (mLock) { + broadcastToClientsLocked(ignoreRemoteException(client -> { + if (client.mDeviceId == deviceId) { + client.mCallback.setState(proxyState); + } + })); + } + }); + } + } + + /** + * Notifies AccessibilityManager of services state changes, which includes changes to the + * list of service infos and timeouts. + * + * @see AccessibilityManager.AccessibilityServicesStateChangeListener + */ + public void scheduleNotifyProxyClientsOfServicesStateChangeLocked(int deviceId) { + if (DEBUG) { + Slog.v(LOG_TAG, "Notify services state change at device id " + deviceId); + } + mMainHandler.post(()-> { + broadcastToClientsLocked(ignoreRemoteException(client -> { + if (client.mDeviceId == deviceId) { + synchronized (mLock) { + client.mCallback.notifyServicesStateChanged( + getRecommendedTimeoutMillisLocked(deviceId)); + } + } + })); + }); + } + + /** + * Updates the focus appearance of AccessibilityManagerClients. + */ + public void updateFocusAppearanceLocked(int deviceId) { + if (DEBUG) { + Slog.v(LOG_TAG, "Update proxy focus appearance at device id " + deviceId); + } + // Reasonably assume that all proxies belonging to a virtual device should have the + // same focus appearance, and if they should be different these should belong to different + // virtual devices. + final ProxyAccessibilityServiceConnection proxy = getFirstProxyForDeviceIdLocked(deviceId); + if (proxy != null) { + mMainHandler.post(()-> { + broadcastToClientsLocked(ignoreRemoteException(client -> { + if (client.mDeviceId == proxy.getDeviceId()) { + client.mCallback.setFocusAppearance( + proxy.getFocusStrokeWidthLocked(), + proxy.getFocusColorLocked()); + } + })); + }); + } + } + + private ProxyAccessibilityServiceConnection getFirstProxyForDeviceIdLocked(int deviceId) { + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy != null && proxy.getDeviceId() == deviceId) { + return proxy; + } + } + return null; + } + + private void broadcastToClientsLocked( + @NonNull Consumer clientAction) { + final RemoteCallbackList userClients = + mSystemSupport.getCurrentUserClientsLocked(); + final RemoteCallbackList globalClients = + mSystemSupport.getGlobalClientsLocked(); + userClients.broadcastForEachCookie(clientAction); + globalClients.broadcastForEachCookie(clientAction); + } + + /** + * Updates the timeout and notifies app clients. + * + * For real users, timeouts are tracked in A11yUserState. For proxies, timeouts are in the + * service connection. The value in user state is preferred, but if this value is 0 the service + * info value is used. + * + * This follows the pattern in readUserRecommendedUiTimeoutSettingsLocked. + * + * TODO(b/250929565): ProxyUserState or similar should hold the timeouts + */ + public void updateTimeoutsIfNeeded(int nonInteractiveUiTimeout, int interactiveUiTimeout) { + synchronized (mLock) { + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy != null) { + if (proxy.updateTimeouts(nonInteractiveUiTimeout, interactiveUiTimeout)) { + scheduleNotifyProxyClientsOfServicesStateChangeLocked(proxy.getDeviceId()); + } + } + } + } + } + + /** + * Gets the recommended timeout belonging to a Virtual Device. + * + * This is the highest of all display proxies belonging to the virtual device. + */ + public long getRecommendedTimeoutMillisLocked(int deviceId) { + int combinedInteractiveTimeout = 0; + int combinedNonInteractiveTimeout = 0; + for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { + final ProxyAccessibilityServiceConnection proxy = + mProxyA11yServiceConnections.valueAt(i); + if (proxy != null && proxy.getDeviceId() == deviceId) { + final int proxyInteractiveUiTimeout = + (proxy != null) ? proxy.getInteractiveTimeout() : 0; + final int nonInteractiveUiTimeout = + (proxy != null) ? proxy.getNonInteractiveTimeout() : 0; + combinedInteractiveTimeout = Math.max(proxyInteractiveUiTimeout, + combinedInteractiveTimeout); + combinedNonInteractiveTimeout = Math.max(nonInteractiveUiTimeout, + combinedNonInteractiveTimeout); + } + } + return IntPair.of(combinedInteractiveTimeout, combinedNonInteractiveTimeout); + } + + /** + * Gets the first focus stroke width belonging to the device. + */ + public int getFocusStrokeWidthLocked(int deviceId) { + final ProxyAccessibilityServiceConnection proxy = getFirstProxyForDeviceIdLocked(deviceId); + if (proxy != null) { + return proxy.getFocusStrokeWidthLocked(); + } + return 0; + + } + + /** + * Gets the first focus color belonging to the device. + */ + public int getFocusColorLocked(int deviceId) { + final ProxyAccessibilityServiceConnection proxy = getFirstProxyForDeviceIdLocked(deviceId); + if (proxy != null) { + return proxy.getFocusColorLocked(); + } + return 0; + } + + /** + * Returns the first device id given a UID. + * @param callingUid the UID to check. + * @return the first matching device id, or DEVICE_ID_INVALID. + */ + public int getFirstDeviceIdForUidLocked(int callingUid) { + int firstDeviceId = DEVICE_ID_INVALID; + final VirtualDeviceManagerInternal localVdm = getLocalVdm(); + if (localVdm == null) { + return firstDeviceId; + } + final Set deviceIds = localVdm.getDeviceIdsForUid(callingUid); + for (Integer uidDeviceId : deviceIds) { + if (uidDeviceId != DEVICE_ID_DEFAULT && uidDeviceId != DEVICE_ID_INVALID) { + firstDeviceId = uidDeviceId; + break; + } + } + return firstDeviceId; + } + + /** + * Sets a Client device id if the app uid belongs to the virtual device. + */ + public void updateDeviceIdsIfNeededLocked(int deviceId) { + final RemoteCallbackList userClients = + mSystemSupport.getCurrentUserClientsLocked(); + final RemoteCallbackList globalClients = + mSystemSupport.getGlobalClientsLocked(); + + updateDeviceIdsIfNeededLocked(deviceId, userClients); + updateDeviceIdsIfNeededLocked(deviceId, globalClients); + } + + /** + * Updates the device ids of IAccessibilityManagerClients if needed. + */ + public void updateDeviceIdsIfNeededLocked(int deviceId, + @NonNull RemoteCallbackList clients) { + final VirtualDeviceManagerInternal localVdm = getLocalVdm(); + if (localVdm == null) { + return; + } + + for (int i = 0; i < clients.getRegisteredCallbackCount(); i++) { + final AccessibilityManagerService.Client client = + ((AccessibilityManagerService.Client) clients.getRegisteredCallbackCookie(i)); + if (deviceId != DEVICE_ID_DEFAULT && deviceId != DEVICE_ID_INVALID + && localVdm.getDeviceIdsForUid(client.mUid).contains(deviceId)) { + if (DEBUG) { + Slog.v(LOG_TAG, "Packages moved to device id " + deviceId + " are " + + Arrays.toString(client.mPackageNames)); + } + client.mDeviceId = deviceId; } } } @@ -306,9 +810,18 @@ public class ProxyManager { } void setAccessibilityInputFilter(AccessibilityInputFilter filter) { + if (DEBUG) { + Slog.v(LOG_TAG, "Set proxy input filter to " + filter); + } mA11yInputFilter = filter; } + VirtualDeviceManagerInternal getLocalVdm() { + if (mLocalVdm == null) { + mLocalVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); + } + return mLocalVdm; + } /** * Prints information belonging to each display that is controlled by an @@ -320,13 +833,38 @@ public class ProxyManager { pw.println("Proxy manager state:"); pw.println(" Number of proxy connections: " + mProxyA11yServiceConnections.size()); pw.println(" Registered proxy connections:"); + final RemoteCallbackList userClients = + mSystemSupport.getCurrentUserClientsLocked(); + final RemoteCallbackList globalClients = + mSystemSupport.getGlobalClientsLocked(); for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) { final ProxyAccessibilityServiceConnection proxy = mProxyA11yServiceConnections.valueAt(i); if (proxy != null) { proxy.dump(fd, pw, args); } + pw.println(); + pw.println(" User clients for proxy's virtual device id"); + printClientsForDeviceId(pw, userClients, proxy.getDeviceId()); + pw.println(); + pw.println(" Global clients for proxy's virtual device id"); + printClientsForDeviceId(pw, globalClients, proxy.getDeviceId()); + } } } -} \ No newline at end of file + + private void printClientsForDeviceId(PrintWriter pw, + RemoteCallbackList clients, int deviceId) { + if (clients != null) { + for (int j = 0; j < clients.getRegisteredCallbackCount(); j++) { + final AccessibilityManagerService.Client client = + (AccessibilityManagerService.Client) + clients.getRegisteredCallbackCookie(j); + if (client.mDeviceId == deviceId) { + pw.println(" " + Arrays.toString(client.mPackageNames) + "\n"); + } + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java index b5e0e0730a58a..dd44a79686392 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java @@ -50,6 +50,7 @@ import java.util.List; public class ProxyAccessibilityServiceConnectionTest { private static final int DISPLAY_ID = 1000; + private static final int DEVICE_ID = 2000; private static final int CONNECTION_ID = 1000; private static final ComponentName COMPONENT_NAME = new ComponentName( "com.android.server.accessibility", ".ProxyAccessibilityServiceConnectionTest"); @@ -90,7 +91,7 @@ public class ProxyAccessibilityServiceConnectionTest { mAccessibilityServiceInfo, CONNECTION_ID , new Handler( getInstrumentation().getContext().getMainLooper()), mMockLock, mMockSecurityPolicy, mMockSystemSupport, mMockA11yTrace, - mMockWindowManagerInternal, mMockA11yWindowManager, DISPLAY_ID); + mMockWindowManagerInternal, mMockA11yWindowManager, DISPLAY_ID, DEVICE_ID); } @Test @@ -101,7 +102,7 @@ public class ProxyAccessibilityServiceConnectionTest { mProxyConnection.setInstalledAndEnabledServices(infos); - verify(mMockSystemSupport).onClientChangeLocked(true); + verify(mMockSystemSupport).onProxyChanged(DEVICE_ID); } @Test