A11yManagerService turns off button targets for services whose packages have forcibly stopped.

Change involves reorganizing relevant code so that it is testable.
Exposed code is documented.

Feature flag:
-namespace: accessibility
-flag: com.android.server.accessibility.disable_continuous_shortcut_on_force_stop

Bug: 198018180
Test: atest A11yManagerServiceTest
Change-Id: Ic54591590af402115607f2d0f524276d1ec365f7
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index f09cb19..1a735f8 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -43,6 +43,13 @@
 }
 
 flag {
+    name: "disable_continuous_shortcut_on_force_stop"
+    namespace: "accessibility"
+    description: "When a package is force stopped, remove the button shortcuts of any continuously-running shortcuts."
+    bug: "198018180"
+}
+
+flag {
     name: "deprecate_package_list_observer"
     namespace: "accessibility"
     description: "Stops using the deprecated PackageListObserver."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 8c1d444..203d9a1 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT;
@@ -182,6 +183,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.function.Consumer;
@@ -650,6 +652,16 @@
         }
     }
 
+    /**
+     * Returns the lock object for any synchronized test blocks.
+     * Should not be used outside of testing.
+     * @return lock object.
+     */
+    @VisibleForTesting
+    Object getLock() {
+        return mLock;
+    }
+
     AccessibilityUserState getCurrentUserState() {
         synchronized (mLock) {
             return getCurrentUserStateLocked();
@@ -746,6 +758,62 @@
         }
     }
 
+    /**
+     * Handles a package or packages being force stopped.
+     * Will disable any relevant services,
+     * and remove any button targets of continuous services,
+     * denoted by {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON}.
+     * If the result is {@code true},
+     * then {@link AccessibilityManagerService#onUserStateChangedLocked(
+     * AccessibilityUserState, boolean)} should be called afterwards.
+     *
+     * @param packages list of packages that have stopped.
+     * @param userState user state to be read & modified.
+     * @return {@code true} if a service was enabled or a button target was removed,
+     * {@code false} otherwise.
+     */
+    @VisibleForTesting
+    boolean onPackagesForceStoppedLocked(
+            String[] packages, AccessibilityUserState userState) {
+        final List<String> continuousServicePackages =
+                userState.mInstalledServices.stream().filter(service ->
+                        (service.flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON)
+                                == FLAG_REQUEST_ACCESSIBILITY_BUTTON
+                ).map(service -> service.getComponentName().flattenToString()).toList();
+
+        boolean enabledServicesChanged = false;
+        final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+        while (it.hasNext()) {
+            final ComponentName comp = it.next();
+            final String compPkg = comp.getPackageName();
+            for (String pkg : packages) {
+                if (compPkg.equals(pkg)) {
+                    it.remove();
+                    userState.getBindingServicesLocked().remove(comp);
+                    userState.getCrashedServicesLocked().remove(comp);
+                    enabledServicesChanged = true;
+                }
+            }
+        }
+        if (enabledServicesChanged) {
+            persistComponentNamesToSettingLocked(
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                    userState.mEnabledServices, userState.mUserId);
+        }
+
+        boolean buttonTargetsChanged = userState.mAccessibilityButtonTargets.removeIf(
+                target -> continuousServicePackages.stream().anyMatch(
+                        pkg -> Objects.equals(target, pkg)));
+        if (buttonTargetsChanged) {
+            persistColonDelimitedSetToSettingLocked(
+                    Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                    userState.mUserId,
+                    userState.mAccessibilityButtonTargets, str -> str);
+        }
+
+        return enabledServicesChanged || buttonTargetsChanged;
+    }
+
     @VisibleForTesting
     PackageMonitor getPackageMonitor() {
         return mPackageMonitor;
@@ -850,6 +918,16 @@
                 }
             }
 
+            /**
+             * Handles instances in which a package or packages have forcibly stopped.
+             *
+             * @param intent intent containing package event information.
+             * @param uid linux process user id (different from Android user id).
+             * @param packages array of package names that have stopped.
+             * @param doit whether to try and handle the stop or just log the trace.
+             *
+             * @return {@code true} if package should be restarted, {@code false} otherwise.
+             */
             @Override
             public boolean onHandleForceStop(Intent intent, String[] packages,
                     int uid, boolean doit) {
@@ -867,26 +945,36 @@
                         return false;
                     }
                     final AccessibilityUserState userState = getUserStateLocked(userId);
-                    final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
-                    while (it.hasNext()) {
-                        final ComponentName comp = it.next();
-                        final String compPkg = comp.getPackageName();
-                        for (String pkg : packages) {
-                            if (compPkg.equals(pkg)) {
-                                if (!doit) {
-                                    return true;
+
+                    if (Flags.disableContinuousShortcutOnForceStop()) {
+                        if (doit && onPackagesForceStoppedLocked(packages, userState)) {
+                            onUserStateChangedLocked(userState);
+                            return false;
+                        } else {
+                            return true;
+                        }
+                    } else {
+                        final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+                        while (it.hasNext()) {
+                            final ComponentName comp = it.next();
+                            final String compPkg = comp.getPackageName();
+                            for (String pkg : packages) {
+                                if (compPkg.equals(pkg)) {
+                                    if (!doit) {
+                                        return true;
+                                    }
+                                    it.remove();
+                                    userState.getBindingServicesLocked().remove(comp);
+                                    userState.getCrashedServicesLocked().remove(comp);
+                                    persistComponentNamesToSettingLocked(
+                                            Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                                            userState.mEnabledServices, userId);
+                                    onUserStateChangedLocked(userState);
                                 }
-                                it.remove();
-                                userState.getBindingServicesLocked().remove(comp);
-                                userState.getCrashedServicesLocked().remove(comp);
-                                persistComponentNamesToSettingLocked(
-                                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
-                                        userState.mEnabledServices, userId);
-                                onUserStateChangedLocked(userState);
                             }
                         }
+                        return false;
                     }
-                    return false;
                 }
             }
         };
@@ -2452,7 +2540,8 @@
      * @param userId The user id.
      * @param outComponentNames The output component names.
      */
-    private void readComponentNamesFromSettingLocked(String settingName, int userId,
+    @VisibleForTesting
+    void readComponentNamesFromSettingLocked(String settingName, int userId,
             Set<ComponentName> outComponentNames) {
         readColonDelimitedSettingToSet(settingName, userId,
                 str -> ComponentName.unflattenFromString(str), outComponentNames);
@@ -2481,7 +2570,19 @@
                 componentName -> componentName.flattenToShortString());
     }
 
-    private <T> void readColonDelimitedSettingToSet(String settingName, int userId,
+    /**
+     * Reads a colon delimited setting,
+     * passes the values through a function,
+     * then stores the values in a provided set.
+     *
+     * @param settingName Name of setting.
+     * @param userId user id corresponding to setting.
+     * @param toItem function mapping values to the output set.
+     * @param outSet output set to write to.
+     * @param <T> type of output set.
+     */
+    @VisibleForTesting
+    <T> void readColonDelimitedSettingToSet(String settingName, int userId,
             Function<String, T> toItem, Set<T> outSet) {
         final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                 settingName, userId);
@@ -3472,7 +3573,7 @@
                 return true;
             }
             final boolean requestA11yButton = (serviceInfo.flags
-                    & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
+                    & FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
             if (requestA11yButton && !userState.mEnabledServices.contains(componentName)) {
                 // An a11y service targeting sdk version > Q and request A11y button and is assigned
                 // to a11y btn should be in the enabled list.
@@ -3773,7 +3874,7 @@
             final int targetSdk = installedServiceInfo.getResolveInfo()
                     .serviceInfo.applicationInfo.targetSdkVersion;
             final boolean requestA11yButton = (installedServiceInfo.flags
-                    & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
+                    & FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
             // Turns on / off the accessibility service
             if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == ACCESSIBILITY_SHORTCUT_KEY)
                     || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
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 cf315a4..b9e45ba 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
 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;
@@ -68,6 +69,7 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.TestableContext;
+import android.util.ArraySet;
 import android.view.Display;
 import android.view.DisplayAdjustments;
 import android.view.DisplayInfo;
@@ -600,6 +602,74 @@
     }
 
     @Test
+    public void testPackagesForceStopped_disablesRelevantService() {
+        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
+        info_a.setComponentName(COMPONENT_NAME);
+        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
+        info_b.setComponentName(new ComponentName("package", "class"));
+
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.clear();
+        userState.mInstalledServices.add(info_a);
+        userState.mInstalledServices.add(info_b);
+        userState.mEnabledServices.clear();
+        userState.mEnabledServices.add(info_a.getComponentName());
+        userState.mEnabledServices.add(info_b.getComponentName());
+
+        synchronized (mA11yms.getLock()) {
+            mA11yms.onPackagesForceStoppedLocked(
+                    new String[]{info_a.getComponentName().getPackageName()}, userState);
+        }
+
+        //Assert user state change
+        userState = mA11yms.getCurrentUserState();
+        assertThat(userState.mEnabledServices).containsExactly(info_b.getComponentName());
+        //Assert setting change
+        final Set<ComponentName> componentsFromSetting = new ArraySet<>();
+        mA11yms.readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                userState.mUserId, componentsFromSetting);
+        assertThat(componentsFromSetting).containsExactly(info_b.getComponentName());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DISABLE_CONTINUOUS_SHORTCUT_ON_FORCE_STOP)
+    public void testPackagesForceStopped_fromContinuousService_removesButtonTarget() {
+        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
+        info_a.setComponentName(COMPONENT_NAME);
+        info_a.flags = FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
+        info_b.setComponentName(new ComponentName("package", "class"));
+
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.clear();
+        userState.mInstalledServices.add(info_a);
+        userState.mInstalledServices.add(info_b);
+        userState.mAccessibilityButtonTargets.clear();
+        userState.mAccessibilityButtonTargets.add(info_a.getComponentName().flattenToString());
+        userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString());
+
+        // despite force stopping both packages, only the first service has the relevant flag,
+        // so only the first should be removed.
+        synchronized (mA11yms.getLock()) {
+            mA11yms.onPackagesForceStoppedLocked(
+                    new String[]{
+                            info_a.getComponentName().getPackageName(),
+                            info_b.getComponentName().getPackageName()},
+                    userState);
+        }
+
+        //Assert user state change
+        userState = mA11yms.getCurrentUserState();
+        assertThat(userState.mAccessibilityButtonTargets).containsExactly(
+                info_b.getComponentName().flattenToString());
+        //Assert setting change
+        final Set<String> targetsFromSetting = new ArraySet<>();
+        mA11yms.readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                userState.mUserId, str -> str, targetsFromSetting);
+        assertThat(targetsFromSetting).containsExactly(info_b.getComponentName().flattenToString());
+    }
+
+    @Test
     @RequiresFlagsDisabled(Flags.FLAG_SCAN_PACKAGES_WITHOUT_LOCK)
     // Test old behavior to validate lock detection for the old (locked access) case.
     public void testPackageMonitorScanPackages_scansWhileHoldingLock() {