From ad959bc4e8e708048cc99b537948e38b6a903a52 Mon Sep 17 00:00:00 2001 From: Daniel Norman Date: Wed, 11 Jan 2023 14:50:48 -0800 Subject: [PATCH] Migrates users to the AccessibilityMenu in system. The migration is performed when A11yManagerService prepares its user state for a newly-available user. The following are automatically migrated: - The enabled state - The shortcut state Bug: 261252772 Test: atest AccessibilityManagerServiceTest Test: Enable A11yMenu-in-AAS, Set up various shortcuts for A11yMenu-in-AAS, adb sync system_ext to put A11yMenu-in-System on the device, reboot and observe A11yMenu-in-System enabled by default. Change-Id: Id91b68b78a948857949fb6975988dc524e365fad --- .../accessibility/AccessibilityManager.java | 11 ++ .../AccessibilityManagerService.java | 96 +++++++++++++++ .../AccessibilityManagerServiceTest.java | 114 ++++++++++++++++++ 3 files changed, 221 insertions(+) diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 8e335e8837ef3..3ce419eff33fe 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -230,6 +230,17 @@ public final class AccessibilityManager { */ public static final int FLAG_CONTENT_CONTROLS = 4; + + /** + * {@link ComponentName} for the Accessibility Menu {@link AccessibilityService} as provided + * inside the system build, used for automatic migration to this version of the service. + * @hide + */ + public static final ComponentName ACCESSIBILITY_MENU_IN_SYSTEM = + new ComponentName("com.android.systemui.accessibility.accessibilitymenu", + "com.android.systemui.accessibility.accessibilitymenu" + + ".AccessibilityMenuService"); + @UnsupportedAppUsage static final Object sInstanceSync = new Object(); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index b28ab7a0081c0..bfdbb6a7994dd 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -28,6 +28,7 @@ import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAG import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; import static android.provider.Settings.Secure.CONTRAST_LEVEL; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_MENU_IN_SYSTEM; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; import static android.view.accessibility.AccessibilityManager.CONTRAST_DEFAULT_VALUE; import static android.view.accessibility.AccessibilityManager.CONTRAST_NOT_SET; @@ -170,6 +171,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -215,6 +217,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private static final String SET_PIP_ACTION_REPLACEMENT = "setPictureInPictureActionReplacingConnection"; + @VisibleForTesting + static final String MENU_SERVICE_RELATIVE_CLASS_NAME = ".AccessibilityMenuService"; + private static final char COMPONENT_NAME_SEPARATOR = ':'; private static final int OWN_PROCESS_ID = android.os.Process.myPid(); @@ -823,6 +828,95 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Context.RECEIVER_EXPORTED); } + /** + * Migrates the Accessibility Menu to the version provided by the system build, + * if necessary based on presence of the service on the device. + */ + @VisibleForTesting + void migrateAccessibilityMenuIfNecessaryLocked(AccessibilityUserState userState) { + final Set menuComponentNames = findA11yMenuComponentNamesLocked(); + final ComponentName menuOutsideSystem = getA11yMenuOutsideSystem(menuComponentNames); + final boolean shouldMigrateToMenuInSystem = menuComponentNames.size() == 2 + && menuComponentNames.contains(ACCESSIBILITY_MENU_IN_SYSTEM) + && menuOutsideSystem != null; + + if (!shouldMigrateToMenuInSystem) { + if (menuComponentNames.size() == 1) { + // If only one Menu package exists then reset its component to the default state. + mPackageManager.setComponentEnabledSetting( + menuComponentNames.stream().findFirst().get(), + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP); + } + return; + } + + // Hide Menu-outside-system so that it does not appear in Settings. + mPackageManager.setComponentEnabledSetting( + menuOutsideSystem, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + // Migrate the accessibility shortcuts. + migrateA11yMenuInSettingLocked(userState, + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, menuOutsideSystem); + migrateA11yMenuInSettingLocked(userState, + Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, menuOutsideSystem); + migrateA11yMenuInSettingLocked(userState, + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, menuOutsideSystem); + // If Menu-outside-system is currently enabled by the user then automatically + // disable it and enable Menu-in-system. + migrateA11yMenuInSettingLocked(userState, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, menuOutsideSystem); + } + + /** + * Returns all {@link ComponentName}s whose class name ends in {@link + * #MENU_SERVICE_RELATIVE_CLASS_NAME}. + **/ + private Set findA11yMenuComponentNamesLocked() { + Set result = new ArraySet<>(); + final var flags = + PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); + for (ResolveInfo resolveInfo : mPackageManager.queryIntentServicesAsUser( + new Intent(AccessibilityService.SERVICE_INTERFACE), flags, mCurrentUserId)) { + final ComponentName componentName = resolveInfo.serviceInfo.getComponentName(); + if (componentName.getClassName().endsWith(MENU_SERVICE_RELATIVE_CLASS_NAME)) { + result.add(componentName); + } + } + return result; + } + + /** + * Returns the first {@link ComponentName} in the provided set that is not equal to {@link + * AccessibilityManager#ACCESSIBILITY_MENU_IN_SYSTEM}. + */ + private static ComponentName getA11yMenuOutsideSystem(Set menuComponentNames) { + Optional menuOutsideSystem = menuComponentNames.stream().filter( + name -> !name.equals(ACCESSIBILITY_MENU_IN_SYSTEM)).findFirst(); + if (menuOutsideSystem.isEmpty()) { + return null; + } + return menuOutsideSystem.get(); + } + + /** + * Replaces toRemove with {@link AccessibilityManager#ACCESSIBILITY_MENU_IN_SYSTEM} + * in the requested setting, if present already. + */ + private void migrateA11yMenuInSettingLocked(AccessibilityUserState userState, String setting, + ComponentName toRemove) { + mTempComponentNameSet.clear(); + readComponentNamesFromSettingLocked(setting, userState.mUserId, mTempComponentNameSet); + if (mTempComponentNameSet.contains(toRemove)) { + mTempComponentNameSet.remove(toRemove); + mTempComponentNameSet.add(ACCESSIBILITY_MENU_IN_SYSTEM); + persistComponentNamesToSettingLocked(setting, mTempComponentNameSet, userState.mUserId); + } + } + // Called only during settings restore; currently supports only the owner user // TODO: b/22388012 private void restoreLegacyDisplayMagnificationNavBarIfNeededLocked(String newSetting, @@ -1592,6 +1686,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mCurrentUserId = userId; AccessibilityUserState userState = getCurrentUserStateLocked(); + migrateAccessibilityMenuIfNecessaryLocked(userState); + readConfigurationForUserStateLocked(userState); mSecurityPolicy.onSwitchUserLocked(mCurrentUserId, userState.mEnabledServices); // Even if reading did not yield change, we have to update diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 448ffe538d008..6831902e10b17 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -16,13 +16,19 @@ package com.android.server.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; +import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; +import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_MENU_IN_SYSTEM; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; +import static com.android.server.accessibility.AccessibilityManagerService.MENU_SERVICE_RELATIVE_CLASS_NAME; import static com.google.common.truth.Truth.assertThat; @@ -93,6 +99,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.List; /** * APCT tests for {@link AccessibilityManagerService}. @@ -514,6 +521,113 @@ public class AccessibilityManagerServiceTest { ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()); } + @Test + public void testMigrateA11yMenu_ResetSingularComponentToDefaultState() { + final ComponentName componentName = + ComponentName.createRelative("external", MENU_SERVICE_RELATIVE_CLASS_NAME); + when(mMockPackageManager.queryIntentServicesAsUser(any(), any(), + eq(mA11yms.getCurrentUserIdLocked()))).thenReturn( + List.of(createResolveInfo(componentName))); + + mA11yms.migrateAccessibilityMenuIfNecessaryLocked(mA11yms.getCurrentUserState()); + + verify(mMockPackageManager).setComponentEnabledSetting(componentName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP); + } + + @Test + public void testMigrateA11yMenu_DoNothing_WhenNoMenuComponents() { + when(mMockPackageManager.queryIntentServicesAsUser(any(), any(), + eq(mA11yms.getCurrentUserIdLocked()))).thenReturn(List.of()); + + mA11yms.migrateAccessibilityMenuIfNecessaryLocked(mA11yms.getCurrentUserState()); + + verify(mMockPackageManager, never()).setComponentEnabledSetting(any(), + anyInt(), anyInt()); + } + + @Test + public void testMigrateA11yMenu_DoNothing_WhenTooManyMenuComponents() { + when(mMockPackageManager.queryIntentServicesAsUser(any(), any(), + eq(mA11yms.getCurrentUserIdLocked()))).thenReturn(List.of( + createResolveInfo(ComponentName.createRelative("external1", + MENU_SERVICE_RELATIVE_CLASS_NAME)), + createResolveInfo(ComponentName.createRelative("external2", + MENU_SERVICE_RELATIVE_CLASS_NAME)), + createResolveInfo(ComponentName.createRelative("external3", + MENU_SERVICE_RELATIVE_CLASS_NAME)))); + + mA11yms.migrateAccessibilityMenuIfNecessaryLocked(mA11yms.getCurrentUserState()); + + verify(mMockPackageManager, never()).setComponentEnabledSetting(any(), + anyInt(), anyInt()); + } + + @Test + public void testMigrateA11yMenu_DoNothing_WhenNoMenuInSystem() { + when(mMockPackageManager.queryIntentServicesAsUser(any(), any(), + eq(mA11yms.getCurrentUserIdLocked()))).thenReturn(List.of( + createResolveInfo(ComponentName.createRelative("external1", + MENU_SERVICE_RELATIVE_CLASS_NAME)), + createResolveInfo(ComponentName.createRelative("external2", + MENU_SERVICE_RELATIVE_CLASS_NAME)))); + + mA11yms.migrateAccessibilityMenuIfNecessaryLocked(mA11yms.getCurrentUserState()); + + verify(mMockPackageManager, never()).setComponentEnabledSetting(any(), + anyInt(), anyInt()); + } + + @Test + public void testMigrateA11yMenu_PerformsMigration() { + final ComponentName menuOutsideSystem = + ComponentName.createRelative("external", MENU_SERVICE_RELATIVE_CLASS_NAME); + final String[] migratedSettings = { + ACCESSIBILITY_BUTTON_TARGETS, + ACCESSIBILITY_BUTTON_TARGET_COMPONENT, + ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, + ENABLED_ACCESSIBILITY_SERVICES + }; + // Start the user with Menu-outside-system enabled, + for (String setting : migratedSettings) { + Settings.Secure.putStringForUser( + mTestableContext.getContentResolver(), + setting, + menuOutsideSystem.flattenToShortString(), + mA11yms.getCurrentUserIdLocked()); + } + // and both Menu versions present. + when(mMockPackageManager.queryIntentServicesAsUser(any(), any(), + eq(mA11yms.getCurrentUserIdLocked()))).thenReturn(List.of( + createResolveInfo(menuOutsideSystem), + createResolveInfo(ACCESSIBILITY_MENU_IN_SYSTEM))); + + mA11yms.migrateAccessibilityMenuIfNecessaryLocked(mA11yms.getCurrentUserState()); + + // Menu-outside-system should be disabled, + verify(mMockPackageManager).setComponentEnabledSetting(menuOutsideSystem, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + // and all settings should migrated to Menu-in-system. + for (String setting : migratedSettings) { + ComponentName componentName = ComponentName.unflattenFromString( + Settings.Secure.getStringForUser( + mTestableContext.getContentResolver(), + setting, + mA11yms.getCurrentUserIdLocked())); + assertThat(componentName).isEqualTo(ACCESSIBILITY_MENU_IN_SYSTEM); + } + } + + private static ResolveInfo createResolveInfo(ComponentName componentName) { + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = new ServiceInfo(); + resolveInfo.serviceInfo.packageName = componentName.getPackageName(); + resolveInfo.serviceInfo.name = componentName.getClassName(); + return resolveInfo; + } + private void mockManageAccessibilityGranted(TestableContext context) { context.getTestablePermissions().setPermission(Manifest.permission.MANAGE_ACCESSIBILITY, PackageManager.PERMISSION_GRANTED);