Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f59f0d28 authored by Daniel Norman's avatar Daniel Norman
Browse files

Adds a config for trusted accessibility services.

Any service which is both preinstalled and on this list does not
require a warning dialog to be shown to the user.

New feature flag:
adb shell device_config override accessibility android.view.accessibility.skip_accessibility_warning_dialog_for_trusted_services true

Depends on existing feature flag:
adb shell device_config override accessibility android.view.accessibility.cleanup_accessibility_warning_dialog true

Bug: 303511250
Test: atest AccessibilityManagerServiceTest
Test: Populate the list with component names;
      Open Settings > Accessibility;
      Enable a service on the list: no warning dialog
      Enable a service not on the list: warning dialog
Change-Id: I49e5d33dd43212d8b73d3225ad668a804554f54d
parent 9522f634
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -86,6 +86,13 @@ flag {
    bug: "277305460"
}

flag {
    name: "skip_accessibility_warning_dialog_for_trusted_services"
    namespace: "accessibility"
    description: "Skips showing the accessibility warning dialog for trusted services."
    bug: "303511250"
}

flag {
    namespace: "accessibility"
    name: "update_always_on_a11y_service"
+10 −0
Original line number Diff line number Diff line
@@ -4493,6 +4493,16 @@
    <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. -->
    <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string>

    <!-- Array of component names, each flattened to a string, for accessibility services that
         can be enabled by the user without showing a warning prompt. These services must be
         preinstalled. -->
    <string-array translatable="false" name="config_trustedAccessibilityServices">
        <!--
        <item>com.example.package.first/com.example.class.FirstService</item>
        <item>com.example.package.second/com.example.class.SecondService</item>
        -->
    </string-array>

    <!-- Warning: This API can be dangerous when not implemented properly. In particular,
         escrow token must NOT be retrievable from device storage. In other words, either
         escrow token is not stored on device or its ciphertext is stored on device while
+1 −0
Original line number Diff line number Diff line
@@ -3629,6 +3629,7 @@
  <java-symbol type="string" name="config_defaultAccessibilityService" />
  <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" />
  <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" />
  <java-symbol type="array" name="config_trustedAccessibilityServices" />

  <java-symbol type="string" name="accessibility_select_shortcut_menu_title" />
  <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" />
+27 −2
Original line number Diff line number Diff line
@@ -4413,25 +4413,50 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
    @Override
    public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) {
        mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
        final ComponentName componentName = info.getComponentName();

        // Warning is not required if the service is already enabled.
        synchronized (mLock) {
            final AccessibilityUserState userState = getCurrentUserStateLocked();
            if (userState.getEnabledServicesLocked().contains(info.getComponentName())) {
            if (userState.getEnabledServicesLocked().contains(componentName)) {
                return false;
            }
        }
        // Warning is not required if the service is already assigned to a shortcut.
        for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) {
            if (getAccessibilityShortcutTargets(shortcutType).contains(
                    info.getComponentName().flattenToString())) {
                    componentName.flattenToString())) {
                return false;
            }
        }
        // Warning is not required if the service is preinstalled and in the
        // trustedAccessibilityServices allowlist.
        if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices()
                && isAccessibilityServicePreinstalledAndTrusted(info)) {
            return false;
        }

        // Warning is required by default.
        return true;
    }

    private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) {
        final ComponentName componentName = info.getComponentName();
        final boolean isPreinstalled =
                info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp();
        if (isPreinstalled) {
            final String[] trustedAccessibilityServices =
                    mContext.getResources().getStringArray(
                            R.array.config_trustedAccessibilityServices);
            if (Arrays.stream(trustedAccessibilityServices)
                    .map(ComponentName::unflattenFromString)
                    .anyMatch(componentName::equals)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
+57 −12
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG;
import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES;

import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -82,6 +83,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;

import com.android.compatibility.common.util.TestUtils;
import com.android.internal.R;
import com.android.internal.compat.IPlatformCompat;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener;
@@ -857,8 +859,7 @@ public class AccessibilityManagerServiceTest {
    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
    public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
        mockManageAccessibilityGranted(mTestableContext);
        final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
        info.setComponentName(COMPONENT_NAME);
        final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);

        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue();
    }
@@ -867,10 +868,9 @@ public class AccessibilityManagerServiceTest {
    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
    public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
        mockManageAccessibilityGranted(mTestableContext);
        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
        info_a.setComponentName(COMPONENT_NAME);
        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
        info_b.setComponentName(new ComponentName("package_b", "class_b"));
        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
                new ComponentName("package_b", "class_b"));
        final AccessibilityUserState userState = mA11yms.getCurrentUserState();
        userState.mEnabledServices.clear();
        userState.mEnabledServices.add(info_b.getComponentName());
@@ -883,12 +883,12 @@ public class AccessibilityManagerServiceTest {
    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
    public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
        mockManageAccessibilityGranted(mTestableContext);
        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
        info_a.setComponentName(new ComponentName("package_a", "class_a"));
        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
        info_b.setComponentName(new ComponentName("package_b", "class_b"));
        final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo();
        info_c.setComponentName(new ComponentName("package_c", "class_c"));
        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
                new ComponentName("package_a", "class_a"));
        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
                new ComponentName("package_b", "class_b"));
        final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
                new ComponentName("package_c", "class_c"));
        final AccessibilityUserState userState = mA11yms.getCurrentUserState();
        userState.mAccessibilityButtonTargets.clear();
        userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString());
@@ -900,6 +900,51 @@ public class AccessibilityManagerServiceTest {
        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
    }

    @Test
    @RequiresFlagsEnabled({
            FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG,
            FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES})
    public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() {
        mockManageAccessibilityGranted(mTestableContext);
        final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
                new ComponentName("package_a", "class_a"), true);
        final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
                new ComponentName("package_b", "class_b"), false);
        final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
                new ComponentName("package_c", "class_c"), true);
        mTestableContext.getOrCreateTestableResources().addOverride(
                R.array.config_trustedAccessibilityServices,
                new String[]{
                        info_b.getComponentName().flattenToString(),
                        info_c.getComponentName().flattenToString()});

        // info_a is not in the allowlist => require the warning
        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue();
        // info_b is not preinstalled => require the warning
        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue();
        // info_c is both in the allowlist and preinstalled => do not require the warning
        assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
    }

    private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
            ComponentName componentName) {
        return mockAccessibilityServiceInfo(componentName, false);
    }

    private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
            ComponentName componentName,
            boolean isSystemApp) {
        AccessibilityServiceInfo accessibilityServiceInfo =
                Mockito.spy(new AccessibilityServiceInfo());
        accessibilityServiceInfo.setComponentName(componentName);
        ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class);
        when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo);
        mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class);
        mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
        when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp);
        return accessibilityServiceInfo;
    }

    // Single package intents can trigger multiple PackageMonitor callbacks.
    // Collect the state of the lock in a set, since tests only care if calls
    // were all locked or all unlocked.