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() {