Loading core/java/android/view/accessibility/IAccessibilityManager.aidl +1 −1 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ interface IAccessibilityManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl surfaceControl); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE)") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.STATUS_BAR_SERVICE,android.Manifest.permission.MANAGE_ACCESSIBILITY})") oneway void notifyQuickSettingsTilesChanged(int userId, in List<ComponentName> tileComponentNames); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") Loading services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +121 −19 Original line number Diff line number Diff line Loading @@ -1706,20 +1706,101 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override @RequiresPermission(Manifest.permission.STATUS_BAR_SERVICE) @RequiresPermission(allOf = { Manifest.permission.STATUS_BAR_SERVICE, Manifest.permission.MANAGE_ACCESSIBILITY }) public void notifyQuickSettingsTilesChanged( @UserIdInt int userId, List<ComponentName> tileComponentNames) { mSecurityPolicy.enforceCallingPermission( @UserIdInt int userId, @NonNull List<ComponentName> tileComponentNames) { if (!android.view.accessibility.Flags.a11yQsShortcut()) { return; } mContext.enforceCallingPermission( Manifest.permission.STATUS_BAR_SERVICE, /* function= */ "notifyQuickSettingsTilesChanged"); mContext.enforceCallingPermission( Manifest.permission.MANAGE_ACCESSIBILITY, /* function= */ "notifyQuickSettingsTilesChanged"); if (DEBUG) { Slog.d(LOG_TAG, TextUtils.formatSimple( "notifyQuickSettingsTilesChanged userId: %d, tileComponentNames: %s", userId, tileComponentNames)); // TODO (b/314843909): in the follow up cl } final Set<ComponentName> newTileComponentNames = new ArraySet<>(tileComponentNames); final Set<ComponentName> addedTiles; final Set<ComponentName> removedTiles; final Map<ComponentName, AccessibilityServiceInfo> tileServiceToA11yServiceInfo; final Map<ComponentName, ComponentName> a11yFeatureToTileService; // update in-memory copy of QS_TILES in AccessibilityManager // update Settings.Secure.ACCESSIBILITY_QS_TARGETS and its in-memory copy // show full device control warning if needed (b/314850435) synchronized (mLock) { AccessibilityUserState userState = getUserStateLocked(userId); tileServiceToA11yServiceInfo = userState.getTileServiceToA11yServiceInfoMapLocked(); a11yFeatureToTileService = userState.getA11yFeatureToTileService(); ArraySet<ComponentName> currentTiles = userState.getA11yQsTilesInQsPanel(); // Find newly added tiles addedTiles = newTileComponentNames .stream() .filter(tileComponentName -> !currentTiles.contains(tileComponentName)) .collect(Collectors.toSet()); // Find newly removed tiles removedTiles = currentTiles .stream() .filter(tileComponentName -> !newTileComponentNames.contains(tileComponentName)) .collect(Collectors.toSet()); if (addedTiles.isEmpty() && removedTiles.isEmpty()) { return; } userState.updateA11yTilesInQsPanelLocked(newTileComponentNames); } List<String> a11yFeaturesToEnable = new ArrayList<>(); List<String> a11yFeaturesToRemove = new ArrayList<>(); // Find the framework features to configure the qs shortcut on/off for (Map.Entry<ComponentName, ComponentName> frameworkFeatureWithTile : ShortcutConstants.A11Y_FEATURE_TO_FRAMEWORK_TILE.entrySet()) { String a11yFeature = frameworkFeatureWithTile.getKey().flattenToString(); ComponentName tile = frameworkFeatureWithTile.getValue(); if (addedTiles.contains(tile)) { a11yFeaturesToEnable.add(a11yFeature); } else if (removedTiles.contains(tile)) { a11yFeaturesToRemove.add(a11yFeature); } } // Find the accessibility service/activity to configure the qs shortcut on/off for (Map.Entry<ComponentName, ComponentName> a11yFeatureWithTileService : a11yFeatureToTileService.entrySet()) { String a11yFeature = a11yFeatureWithTileService.getKey().flattenToString(); ComponentName tileService = a11yFeatureWithTileService.getValue(); if (addedTiles.contains(tileService)) { AccessibilityServiceInfo serviceInfo = tileServiceToA11yServiceInfo.getOrDefault( tileService, null); if (serviceInfo != null && isAccessibilityServiceWarningRequired(serviceInfo)) { // TODO(b/314850435): show full device control warning if needed after // SysUI QS Panel can update live continue; } a11yFeaturesToEnable.add(a11yFeature); } else if (removedTiles.contains(tileService)) { a11yFeaturesToRemove.add(a11yFeature); } } // Turn on/off a11y qs shortcut for the a11y features based on the change in QS Panel if (!a11yFeaturesToEnable.isEmpty()) { enableShortcutForTargets(/* enable= */ true, UserShortcutType.QUICK_SETTINGS, a11yFeaturesToEnable, userId); } if (!a11yFeaturesToRemove.isEmpty()) { enableShortcutForTargets(/* enable= */ false, UserShortcutType.QUICK_SETTINGS, a11yFeaturesToRemove, userId); } } /** Loading Loading @@ -3661,18 +3742,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /** * Update the Settings.Secure.ACCESSIBILITY_QS_TARGETS so that it only contains valid content, * and a side loaded service can't spoof the package name of the default service. * <p> * 1. Remove the target if the target is no longer installed on the device <br/> * 2. Add the target if the target is enabled and the target's tile is in the QS Panel <br/> * </p> */ private void updateAccessibilityQsTargetsLocked(AccessibilityUserState userState) { final Set<String> targets = userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS); final int lastSize = targets.size(); if (lastSize == 0) { if (!android.view.accessibility.Flags.a11yQsShortcut()) { return; } final Set<String> targets = userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS); // Removes the targets that are no longer installed on the device. boolean somethingChanged = targets.removeIf( name -> !userState.isShortcutTargetInstalledLocked(name)); // Add the target if the a11y service is enabled and the tile exist in QS panel Set<ComponentName> enabledServices = userState.getEnabledServicesLocked(); Map<ComponentName, ComponentName> a11yFeatureToTileService = userState.getA11yFeatureToTileService(); Set<ComponentName> currentA11yTilesInQsPanel = userState.getA11yQsTilesInQsPanel(); for (ComponentName enabledService : enabledServices) { ComponentName tileService = a11yFeatureToTileService.getOrDefault(enabledService, null); if (tileService != null && currentA11yTilesInQsPanel.contains(tileService)) { somethingChanged |= targets.add(enabledService.flattenToString()); } } if (!somethingChanged) { return; } Loading Loading @@ -3700,14 +3798,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = List.of( final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = new ArrayList<>(3); shortcutTypeAndShortcutSetting.add( new Pair<>(ACCESSIBILITY_SHORTCUT_KEY, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)); shortcutTypeAndShortcutSetting.add( new Pair<>(ACCESSIBILITY_BUTTON, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS)); if (android.view.accessibility.Flags.a11yQsShortcut()) { shortcutTypeAndShortcutSetting.add( new Pair<>(UserShortcutType.QUICK_SETTINGS, Settings.Secure.ACCESSIBILITY_QS_TARGETS) ); Settings.Secure.ACCESSIBILITY_QS_TARGETS)); } final ComponentName serviceName = service.getComponentName(); for (Pair<Integer, String> shortcutTypePair : shortcutTypeAndShortcutSetting) { Loading services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +50 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; /** * Class that hold states and settings per user and share between Loading Loading @@ -104,6 +106,15 @@ class AccessibilityUserState { final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>(); private final ArraySet<String> mAccessibilityQsTargets = new ArraySet<>(); /** * The QuickSettings tiles in the QS Panel. This can be different from * {@link #mAccessibilityQsTargets} in that {@link #mA11yTilesInQsPanel} stores the * TileService's or the a11y framework tile component names (e.g. * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the * A11y Feature's component names. */ private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>(); private final ServiceInfoChangeListener mServiceInfoChangeListener; private ComponentName mServiceChangingSoftKeyboardMode; Loading Loading @@ -566,7 +577,9 @@ class AccessibilityUserState { pw.println("}"); pw.append(" button target:{").append(mTargetAssignedToAccessibilityButton); pw.println("}"); pw.append(" qs shortcut targets:" + mAccessibilityQsTargets); pw.append(" qs shortcut targets:").append(mAccessibilityQsTargets.toString()); pw.println(); pw.append(" a11y tiles in QS panel:").append(mA11yTilesInQsPanel.toString()); pw.println(); pw.append(" Bound services:{"); final int serviceCount = mBoundServices.size(); Loading Loading @@ -1100,10 +1113,46 @@ class AccessibilityUserState { return new ArraySet<>(mAccessibilityQsTargets); } public void updateA11yTilesInQsPanelLocked(Set<ComponentName> componentNames) { mA11yTilesInQsPanel.clear(); mA11yTilesInQsPanel.addAll(componentNames); } /** * Returns a copy of the a11y tiles that are in the QuickSettings panel */ public ArraySet<ComponentName> getA11yQsTilesInQsPanel() { return new ArraySet<>(mA11yTilesInQsPanel); } /** * Returns a map of AccessibilityService or AccessibilityShortcut to its provided TileService */ public Map<ComponentName, ComponentName> getA11yFeatureToTileService() { Map<ComponentName, ComponentName> featureToTileServiceMap = new ArrayMap<>(); featureToTileServiceMap.putAll(mA11yServiceToTileService); featureToTileServiceMap.putAll(mA11yActivityToTileService); return featureToTileServiceMap; } /** * Returns a map of TileService's componentName to the AccessibilityServiceInfo it ties to. */ public Map<ComponentName, AccessibilityServiceInfo> getTileServiceToA11yServiceInfoMapLocked() { Map<ComponentName, AccessibilityServiceInfo> tileServiceToA11yServiceInfoMap = new ArrayMap<>(); Map<ComponentName, AccessibilityServiceInfo> a11yServiceToServiceInfoMap = mInstalledServices.stream().collect( Collectors.toMap( AccessibilityServiceInfo::getComponentName, Function.identity())); for (Map.Entry<ComponentName, ComponentName> serviceToTile : mA11yServiceToTileService.entrySet()) { if (a11yServiceToServiceInfoMap.containsKey(serviceToTile.getKey())) { tileServiceToA11yServiceInfoMap.put(serviceToTile.getValue(), a11yServiceToServiceInfoMap.get(serviceToTile.getKey())); } } return tileServiceToA11yServiceInfoMap; } } services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +152 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,7 @@ import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.TestUtils; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.common.ShortcutConstants.FloatingMenuSize; import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; Loading Loading @@ -1318,6 +1319,152 @@ public class AccessibilityManagerServiceTest { } } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_statusBarServiceNotGranted_throwsException() { mTestableContext.getTestablePermissions().setPermission( Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_DENIED); mockManageAccessibilityGranted(mTestableContext); assertThrows(SecurityException.class, () -> mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME))); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_manageAccessibilityNotGranted_throwsException() { mockStatusBarServiceGranted(mTestableContext); mTestableContext.getTestablePermissions().setPermission( Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_DENIED); assertThrows(SecurityException.class, () -> mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME))); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); List<ComponentName> tiles = List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel() ).containsExactlyElementsIn(tiles); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_sameQsTiles_noUpdateToA11yTilesInQsPanel() { notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel(); List<ComponentName> tiles = mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel().stream().toList(); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel() ).containsExactlyElementsIn(tiles); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningRequired_qsShortcutRemainDisabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); setupShortcutTargetServices(); ComponentName tile = new ComponentName( TARGET_ALWAYS_ON_A11Y_SERVICE.getPackageName(), TARGET_ALWAYS_ON_A11Y_SERVICE_TILE_CLASS); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of(tile) ); assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()).doesNotContain(tile); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningNotRequired_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); setupShortcutTargetServices(); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mAccessibilityButtonTargets.clear(); userState.mAccessibilityButtonTargets.add(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()); ComponentName tile = new ComponentName( TARGET_ALWAYS_ON_A11Y_SERVICE.getPackageName(), TARGET_ALWAYS_ON_A11Y_SERVICE_TILE_CLASS); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of(tile) ); assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()) .contains(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); List<ComponentName> tiles = List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTargets() ).containsExactlyElementsIn(List.of( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(), AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()) ); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_removeFrameworkTile_qsShortcutDisabled() { notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled(); Set<ComponentName> qsTiles = mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel(); qsTiles.remove(AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, qsTiles.stream().toList() ); assertThat( mA11yms.getCurrentUserState().getA11yQsTargets() ).doesNotContain( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString()); } private static AccessibilityServiceInfo mockAccessibilityServiceInfo( ComponentName componentName) { return mockAccessibilityServiceInfo( Loading Loading @@ -1367,6 +1514,11 @@ public class AccessibilityManagerServiceTest { PackageManager.PERMISSION_GRANTED); } private void mockStatusBarServiceGranted(TestableContext context) { context.getTestablePermissions().setPermission(Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_GRANTED); } private void assertStartActivityWithExpectedComponentName(Context mockContext, String componentName) { verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(), Loading services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,8 @@ import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_E import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; Loading @@ -45,6 +47,8 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.graphics.Color; import android.platform.test.annotations.RequiresFlagsDisabled; Loading @@ -59,6 +63,7 @@ import android.view.Display; import androidx.test.InstrumentationRegistry; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.After; Loading @@ -68,6 +73,9 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Map; import java.util.Set; /** Tests for AccessibilityUserState */ public class AccessibilityUserStateTest { Loading Loading @@ -431,7 +439,70 @@ public class AccessibilityUserStateTest { assertEquals(focusStrokeWidthValue, mUserState.getFocusStrokeWidthLocked()); assertEquals(focusColorValue, mUserState.getFocusColorLocked()); } @Test public void updateA11yQsTargetLocked_valueUpdated() { Set<String> newTargets = Set.of( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(), AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString() ); mUserState.updateA11yQsTargetLocked(newTargets); assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets); } @Test public void getA11yQsTargets_returnsCopiedData() { updateA11yQsTargetLocked_valueUpdated(); Set<String> targets = mUserState.getA11yQsTargets(); targets.clear(); assertThat(mUserState.getA11yQsTargets()).isNotEmpty(); } @Test public void updateA11yTilesInQsPanelLocked_valueUpdated() { Set<ComponentName> newTargets = Set.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mUserState.updateA11yTilesInQsPanelLocked(newTargets); assertThat(mUserState.getA11yQsTilesInQsPanel()).isEqualTo(newTargets); } @Test public void getA11yQsTilesInQsPanel_returnsCopiedData() { updateA11yTilesInQsPanelLocked_valueUpdated(); Set<ComponentName> targets = mUserState.getA11yQsTilesInQsPanel(); targets.clear(); assertThat(mUserState.getA11yQsTilesInQsPanel()).isNotEmpty(); } @Test public void getTileServiceToA11yServiceInfoMapLocked() { final ComponentName tileComponent = new ComponentName(COMPONENT_NAME.getPackageName(), "FakeTileService"); ServiceInfo serviceInfo = new ServiceInfo(); serviceInfo.packageName = tileComponent.getPackageName(); serviceInfo.name = COMPONENT_NAME.getClassName(); ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.serviceInfo = serviceInfo; when(mMockServiceInfo.getTileServiceName()).thenReturn(tileComponent.getClassName()); when(mMockServiceInfo.getResolveInfo()).thenReturn(resolveInfo); mUserState.mInstalledServices.add(mMockServiceInfo); mUserState.updateTileServiceMapForAccessibilityServiceLocked(); Map<ComponentName, AccessibilityServiceInfo> actual = mUserState.getTileServiceToA11yServiceInfoMapLocked(); assertThat(actual).containsExactly(tileComponent, mMockServiceInfo); } private int getSecureIntForUser(String key, int userId) { Loading Loading
core/java/android/view/accessibility/IAccessibilityManager.aidl +1 −1 Original line number Diff line number Diff line Loading @@ -142,7 +142,7 @@ interface IAccessibilityManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl surfaceControl); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE)") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.STATUS_BAR_SERVICE,android.Manifest.permission.MANAGE_ACCESSIBILITY})") oneway void notifyQuickSettingsTilesChanged(int userId, in List<ComponentName> tileComponentNames); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") Loading
services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +121 −19 Original line number Diff line number Diff line Loading @@ -1706,20 +1706,101 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override @RequiresPermission(Manifest.permission.STATUS_BAR_SERVICE) @RequiresPermission(allOf = { Manifest.permission.STATUS_BAR_SERVICE, Manifest.permission.MANAGE_ACCESSIBILITY }) public void notifyQuickSettingsTilesChanged( @UserIdInt int userId, List<ComponentName> tileComponentNames) { mSecurityPolicy.enforceCallingPermission( @UserIdInt int userId, @NonNull List<ComponentName> tileComponentNames) { if (!android.view.accessibility.Flags.a11yQsShortcut()) { return; } mContext.enforceCallingPermission( Manifest.permission.STATUS_BAR_SERVICE, /* function= */ "notifyQuickSettingsTilesChanged"); mContext.enforceCallingPermission( Manifest.permission.MANAGE_ACCESSIBILITY, /* function= */ "notifyQuickSettingsTilesChanged"); if (DEBUG) { Slog.d(LOG_TAG, TextUtils.formatSimple( "notifyQuickSettingsTilesChanged userId: %d, tileComponentNames: %s", userId, tileComponentNames)); // TODO (b/314843909): in the follow up cl } final Set<ComponentName> newTileComponentNames = new ArraySet<>(tileComponentNames); final Set<ComponentName> addedTiles; final Set<ComponentName> removedTiles; final Map<ComponentName, AccessibilityServiceInfo> tileServiceToA11yServiceInfo; final Map<ComponentName, ComponentName> a11yFeatureToTileService; // update in-memory copy of QS_TILES in AccessibilityManager // update Settings.Secure.ACCESSIBILITY_QS_TARGETS and its in-memory copy // show full device control warning if needed (b/314850435) synchronized (mLock) { AccessibilityUserState userState = getUserStateLocked(userId); tileServiceToA11yServiceInfo = userState.getTileServiceToA11yServiceInfoMapLocked(); a11yFeatureToTileService = userState.getA11yFeatureToTileService(); ArraySet<ComponentName> currentTiles = userState.getA11yQsTilesInQsPanel(); // Find newly added tiles addedTiles = newTileComponentNames .stream() .filter(tileComponentName -> !currentTiles.contains(tileComponentName)) .collect(Collectors.toSet()); // Find newly removed tiles removedTiles = currentTiles .stream() .filter(tileComponentName -> !newTileComponentNames.contains(tileComponentName)) .collect(Collectors.toSet()); if (addedTiles.isEmpty() && removedTiles.isEmpty()) { return; } userState.updateA11yTilesInQsPanelLocked(newTileComponentNames); } List<String> a11yFeaturesToEnable = new ArrayList<>(); List<String> a11yFeaturesToRemove = new ArrayList<>(); // Find the framework features to configure the qs shortcut on/off for (Map.Entry<ComponentName, ComponentName> frameworkFeatureWithTile : ShortcutConstants.A11Y_FEATURE_TO_FRAMEWORK_TILE.entrySet()) { String a11yFeature = frameworkFeatureWithTile.getKey().flattenToString(); ComponentName tile = frameworkFeatureWithTile.getValue(); if (addedTiles.contains(tile)) { a11yFeaturesToEnable.add(a11yFeature); } else if (removedTiles.contains(tile)) { a11yFeaturesToRemove.add(a11yFeature); } } // Find the accessibility service/activity to configure the qs shortcut on/off for (Map.Entry<ComponentName, ComponentName> a11yFeatureWithTileService : a11yFeatureToTileService.entrySet()) { String a11yFeature = a11yFeatureWithTileService.getKey().flattenToString(); ComponentName tileService = a11yFeatureWithTileService.getValue(); if (addedTiles.contains(tileService)) { AccessibilityServiceInfo serviceInfo = tileServiceToA11yServiceInfo.getOrDefault( tileService, null); if (serviceInfo != null && isAccessibilityServiceWarningRequired(serviceInfo)) { // TODO(b/314850435): show full device control warning if needed after // SysUI QS Panel can update live continue; } a11yFeaturesToEnable.add(a11yFeature); } else if (removedTiles.contains(tileService)) { a11yFeaturesToRemove.add(a11yFeature); } } // Turn on/off a11y qs shortcut for the a11y features based on the change in QS Panel if (!a11yFeaturesToEnable.isEmpty()) { enableShortcutForTargets(/* enable= */ true, UserShortcutType.QUICK_SETTINGS, a11yFeaturesToEnable, userId); } if (!a11yFeaturesToRemove.isEmpty()) { enableShortcutForTargets(/* enable= */ false, UserShortcutType.QUICK_SETTINGS, a11yFeaturesToRemove, userId); } } /** Loading Loading @@ -3661,18 +3742,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /** * Update the Settings.Secure.ACCESSIBILITY_QS_TARGETS so that it only contains valid content, * and a side loaded service can't spoof the package name of the default service. * <p> * 1. Remove the target if the target is no longer installed on the device <br/> * 2. Add the target if the target is enabled and the target's tile is in the QS Panel <br/> * </p> */ private void updateAccessibilityQsTargetsLocked(AccessibilityUserState userState) { final Set<String> targets = userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS); final int lastSize = targets.size(); if (lastSize == 0) { if (!android.view.accessibility.Flags.a11yQsShortcut()) { return; } final Set<String> targets = userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS); // Removes the targets that are no longer installed on the device. boolean somethingChanged = targets.removeIf( name -> !userState.isShortcutTargetInstalledLocked(name)); // Add the target if the a11y service is enabled and the tile exist in QS panel Set<ComponentName> enabledServices = userState.getEnabledServicesLocked(); Map<ComponentName, ComponentName> a11yFeatureToTileService = userState.getA11yFeatureToTileService(); Set<ComponentName> currentA11yTilesInQsPanel = userState.getA11yQsTilesInQsPanel(); for (ComponentName enabledService : enabledServices) { ComponentName tileService = a11yFeatureToTileService.getOrDefault(enabledService, null); if (tileService != null && currentA11yTilesInQsPanel.contains(tileService)) { somethingChanged |= targets.add(enabledService.flattenToString()); } } if (!somethingChanged) { return; } Loading Loading @@ -3700,14 +3798,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return; } final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = List.of( final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = new ArrayList<>(3); shortcutTypeAndShortcutSetting.add( new Pair<>(ACCESSIBILITY_SHORTCUT_KEY, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)); shortcutTypeAndShortcutSetting.add( new Pair<>(ACCESSIBILITY_BUTTON, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS)); if (android.view.accessibility.Flags.a11yQsShortcut()) { shortcutTypeAndShortcutSetting.add( new Pair<>(UserShortcutType.QUICK_SETTINGS, Settings.Secure.ACCESSIBILITY_QS_TARGETS) ); Settings.Secure.ACCESSIBILITY_QS_TARGETS)); } final ComponentName serviceName = service.getComponentName(); for (Pair<Integer, String> shortcutTypePair : shortcutTypeAndShortcutSetting) { Loading
services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +50 −1 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; /** * Class that hold states and settings per user and share between Loading Loading @@ -104,6 +106,15 @@ class AccessibilityUserState { final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>(); private final ArraySet<String> mAccessibilityQsTargets = new ArraySet<>(); /** * The QuickSettings tiles in the QS Panel. This can be different from * {@link #mAccessibilityQsTargets} in that {@link #mA11yTilesInQsPanel} stores the * TileService's or the a11y framework tile component names (e.g. * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the * A11y Feature's component names. */ private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>(); private final ServiceInfoChangeListener mServiceInfoChangeListener; private ComponentName mServiceChangingSoftKeyboardMode; Loading Loading @@ -566,7 +577,9 @@ class AccessibilityUserState { pw.println("}"); pw.append(" button target:{").append(mTargetAssignedToAccessibilityButton); pw.println("}"); pw.append(" qs shortcut targets:" + mAccessibilityQsTargets); pw.append(" qs shortcut targets:").append(mAccessibilityQsTargets.toString()); pw.println(); pw.append(" a11y tiles in QS panel:").append(mA11yTilesInQsPanel.toString()); pw.println(); pw.append(" Bound services:{"); final int serviceCount = mBoundServices.size(); Loading Loading @@ -1100,10 +1113,46 @@ class AccessibilityUserState { return new ArraySet<>(mAccessibilityQsTargets); } public void updateA11yTilesInQsPanelLocked(Set<ComponentName> componentNames) { mA11yTilesInQsPanel.clear(); mA11yTilesInQsPanel.addAll(componentNames); } /** * Returns a copy of the a11y tiles that are in the QuickSettings panel */ public ArraySet<ComponentName> getA11yQsTilesInQsPanel() { return new ArraySet<>(mA11yTilesInQsPanel); } /** * Returns a map of AccessibilityService or AccessibilityShortcut to its provided TileService */ public Map<ComponentName, ComponentName> getA11yFeatureToTileService() { Map<ComponentName, ComponentName> featureToTileServiceMap = new ArrayMap<>(); featureToTileServiceMap.putAll(mA11yServiceToTileService); featureToTileServiceMap.putAll(mA11yActivityToTileService); return featureToTileServiceMap; } /** * Returns a map of TileService's componentName to the AccessibilityServiceInfo it ties to. */ public Map<ComponentName, AccessibilityServiceInfo> getTileServiceToA11yServiceInfoMapLocked() { Map<ComponentName, AccessibilityServiceInfo> tileServiceToA11yServiceInfoMap = new ArrayMap<>(); Map<ComponentName, AccessibilityServiceInfo> a11yServiceToServiceInfoMap = mInstalledServices.stream().collect( Collectors.toMap( AccessibilityServiceInfo::getComponentName, Function.identity())); for (Map.Entry<ComponentName, ComponentName> serviceToTile : mA11yServiceToTileService.entrySet()) { if (a11yServiceToServiceInfoMap.containsKey(serviceToTile.getKey())) { tileServiceToA11yServiceInfoMap.put(serviceToTile.getValue(), a11yServiceToServiceInfoMap.get(serviceToTile.getKey())); } } return tileServiceToA11yServiceInfoMap; } }
services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +152 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,7 @@ import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.TestUtils; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.common.ShortcutConstants.FloatingMenuSize; import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; Loading Loading @@ -1318,6 +1319,152 @@ public class AccessibilityManagerServiceTest { } } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_statusBarServiceNotGranted_throwsException() { mTestableContext.getTestablePermissions().setPermission( Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_DENIED); mockManageAccessibilityGranted(mTestableContext); assertThrows(SecurityException.class, () -> mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME))); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_manageAccessibilityNotGranted_throwsException() { mockStatusBarServiceGranted(mTestableContext); mTestableContext.getTestablePermissions().setPermission( Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_DENIED); assertThrows(SecurityException.class, () -> mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME))); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); List<ComponentName> tiles = List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel() ).containsExactlyElementsIn(tiles); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_sameQsTiles_noUpdateToA11yTilesInQsPanel() { notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel(); List<ComponentName> tiles = mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel().stream().toList(); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel() ).containsExactlyElementsIn(tiles); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningRequired_qsShortcutRemainDisabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); setupShortcutTargetServices(); ComponentName tile = new ComponentName( TARGET_ALWAYS_ON_A11Y_SERVICE.getPackageName(), TARGET_ALWAYS_ON_A11Y_SERVICE_TILE_CLASS); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of(tile) ); assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()).doesNotContain(tile); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningNotRequired_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); setupShortcutTargetServices(); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mAccessibilityButtonTargets.clear(); userState.mAccessibilityButtonTargets.add(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()); ComponentName tile = new ComponentName( TARGET_ALWAYS_ON_A11Y_SERVICE.getPackageName(), TARGET_ALWAYS_ON_A11Y_SERVICE_TILE_CLASS); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, List.of(tile) ); assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()) .contains(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); List<ComponentName> tiles = List.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, tiles ); assertThat( mA11yms.getCurrentUserState().getA11yQsTargets() ).containsExactlyElementsIn(List.of( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(), AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()) ); } @Test @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_removeFrameworkTile_qsShortcutDisabled() { notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled(); Set<ComponentName> qsTiles = mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel(); qsTiles.remove(AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME); mA11yms.notifyQuickSettingsTilesChanged( mA11yms.getCurrentUserState().mUserId, qsTiles.stream().toList() ); assertThat( mA11yms.getCurrentUserState().getA11yQsTargets() ).doesNotContain( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString()); } private static AccessibilityServiceInfo mockAccessibilityServiceInfo( ComponentName componentName) { return mockAccessibilityServiceInfo( Loading Loading @@ -1367,6 +1514,11 @@ public class AccessibilityManagerServiceTest { PackageManager.PERMISSION_GRANTED); } private void mockStatusBarServiceGranted(TestableContext context) { context.getTestablePermissions().setPermission(Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_GRANTED); } private void assertStartActivityWithExpectedComponentName(Context mockContext, String componentName) { verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(), Loading
services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +71 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,8 @@ import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_E import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; Loading @@ -45,6 +47,8 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.graphics.Color; import android.platform.test.annotations.RequiresFlagsDisabled; Loading @@ -59,6 +63,7 @@ import android.view.Display; import androidx.test.InstrumentationRegistry; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.After; Loading @@ -68,6 +73,9 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Map; import java.util.Set; /** Tests for AccessibilityUserState */ public class AccessibilityUserStateTest { Loading Loading @@ -431,7 +439,70 @@ public class AccessibilityUserStateTest { assertEquals(focusStrokeWidthValue, mUserState.getFocusStrokeWidthLocked()); assertEquals(focusColorValue, mUserState.getFocusColorLocked()); } @Test public void updateA11yQsTargetLocked_valueUpdated() { Set<String> newTargets = Set.of( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(), AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString() ); mUserState.updateA11yQsTargetLocked(newTargets); assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets); } @Test public void getA11yQsTargets_returnsCopiedData() { updateA11yQsTargetLocked_valueUpdated(); Set<String> targets = mUserState.getA11yQsTargets(); targets.clear(); assertThat(mUserState.getA11yQsTargets()).isNotEmpty(); } @Test public void updateA11yTilesInQsPanelLocked_valueUpdated() { Set<ComponentName> newTargets = Set.of( AccessibilityShortcutController.DALTONIZER_TILE_COMPONENT_NAME, AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME ); mUserState.updateA11yTilesInQsPanelLocked(newTargets); assertThat(mUserState.getA11yQsTilesInQsPanel()).isEqualTo(newTargets); } @Test public void getA11yQsTilesInQsPanel_returnsCopiedData() { updateA11yTilesInQsPanelLocked_valueUpdated(); Set<ComponentName> targets = mUserState.getA11yQsTilesInQsPanel(); targets.clear(); assertThat(mUserState.getA11yQsTilesInQsPanel()).isNotEmpty(); } @Test public void getTileServiceToA11yServiceInfoMapLocked() { final ComponentName tileComponent = new ComponentName(COMPONENT_NAME.getPackageName(), "FakeTileService"); ServiceInfo serviceInfo = new ServiceInfo(); serviceInfo.packageName = tileComponent.getPackageName(); serviceInfo.name = COMPONENT_NAME.getClassName(); ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.serviceInfo = serviceInfo; when(mMockServiceInfo.getTileServiceName()).thenReturn(tileComponent.getClassName()); when(mMockServiceInfo.getResolveInfo()).thenReturn(resolveInfo); mUserState.mInstalledServices.add(mMockServiceInfo); mUserState.updateTileServiceMapForAccessibilityServiceLocked(); Map<ComponentName, AccessibilityServiceInfo> actual = mUserState.getTileServiceToA11yServiceInfoMapLocked(); assertThat(actual).containsExactly(tileComponent, mMockServiceInfo); } private int getSecureIntForUser(String key, int userId) { Loading