Loading services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerConstants.java 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import java.time.Duration; /** * Constants used by the accessibility checker. * * @hide */ final class AccessibilityCheckerConstants { // The min required duration between two consecutive runs of the a11y checker. static final Duration MIN_DURATION_BETWEEN_CHECKS = Duration.ofMinutes(1); // The max number of cached results at a time. static final int MAX_CACHE_CAPACITY = 10000; } services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java 0 → 100644 +169 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MAX_CACHE_CAPACITY; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MIN_DURATION_BETWEEN_CHECKS; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.server.accessibility.Flags; import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The class responsible for running AccessibilityChecks on cached nodes and caching the results for * logging. Results are cached and capped to limit the logging frequency and size. * * @hide */ public final class AccessibilityCheckerManager { private static final String LOG_TAG = "AccessibilityCheckerManager"; private final PackageManager mPackageManager; private final Set<AccessibilityHierarchyCheck> mHierarchyChecks; private final ATFHierarchyBuilder mATFHierarchyBuilder; private final Set<AccessibilityCheckResultReported> mCachedResults = new HashSet<>(); @VisibleForTesting final A11yCheckerTimer mTimer = new A11yCheckerTimer(); public AccessibilityCheckerManager(Context context) { this(AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( AccessibilityCheckPreset.LATEST), (nodeInfo) -> AccessibilityHierarchyAndroid.newBuilder(nodeInfo, context).build(), context.getPackageManager()); } @VisibleForTesting AccessibilityCheckerManager( Set<AccessibilityHierarchyCheck> hierarchyChecks, ATFHierarchyBuilder atfHierarchyBuilder, PackageManager packageManager) { this.mHierarchyChecks = hierarchyChecks; this.mATFHierarchyBuilder = atfHierarchyBuilder; this.mPackageManager = packageManager; } /** * If eligible, runs AccessibilityChecks on the given nodes and caches the results for later * logging. Returns the check results for the given nodes. */ @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public Set<AccessibilityCheckResultReported> maybeRunA11yChecker( List<AccessibilityNodeInfo> nodes, @Nullable AccessibilityEvent accessibilityEvent, ComponentName sourceComponentName, @UserIdInt int userId) { if (!shouldRunA11yChecker()) { return Set.of(); } Set<AccessibilityCheckResultReported> allResults = new HashSet<>(); String defaultBrowserName = mPackageManager.getDefaultBrowserPackageNameAsUser(userId); try { for (AccessibilityNodeInfo nodeInfo : nodes) { // Skip browser results because they are mostly related to web content and not the // browser app itself. if (nodeInfo.getPackageName() == null || nodeInfo.getPackageName().toString().equals(defaultBrowserName)) { continue; } List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode(nodeInfo); Set<AccessibilityCheckResultReported> filteredResults = AccessibilityCheckerUtils.processResults(nodeInfo, checkResults, accessibilityEvent, mPackageManager, sourceComponentName); allResults.addAll(filteredResults); } mCachedResults.addAll(allResults); } catch (RuntimeException e) { Slog.e(LOG_TAG, "An unknown error occurred while running a11y checker.", e); } return allResults; } private List<AccessibilityHierarchyCheckResult> runChecksOnNode( AccessibilityNodeInfo nodeInfo) { AccessibilityHierarchy checkableHierarchy = mATFHierarchyBuilder.getATFCheckableHierarchy( nodeInfo); List<AccessibilityHierarchyCheckResult> checkResults = new ArrayList<>(); for (AccessibilityHierarchyCheck check : mHierarchyChecks) { checkResults.addAll(check.runCheckOnHierarchy(checkableHierarchy)); } return checkResults; } public Set<AccessibilityCheckResultReported> getCachedResults() { return Collections.unmodifiableSet(mCachedResults); } @VisibleForTesting boolean shouldRunA11yChecker() { if (!Flags.enableA11yCheckerLogging() || mCachedResults.size() == MAX_CACHE_CAPACITY) { return false; } if (mTimer.getLastCheckTime() == null || mTimer.getLastCheckTime().plus( MIN_DURATION_BETWEEN_CHECKS).isBefore(Instant.now())) { mTimer.setLastCheckTime(Instant.now()); return true; } return false; } /** Timer class to facilitate testing with fake times. */ @VisibleForTesting static class A11yCheckerTimer { private Instant mLastCheckTime = null; Instant getLastCheckTime() { return mLastCheckTime; } void setLastCheckTime(Instant newTime) { mLastCheckTime = newTime; } } /** AccessibilityHierarchy wrapper to facilitate testing with fake hierarchies. */ @VisibleForTesting interface ATFHierarchyBuilder { AccessibilityHierarchy getATFCheckableHierarchy(AccessibilityNodeInfo nodeInfo); } } services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java +6 −13 Original line number Diff line number Diff line Loading @@ -19,7 +19,6 @@ package com.android.server.accessibility.a11ychecker; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Slog; Loading Loading @@ -62,6 +61,7 @@ import java.util.stream.Collectors; public class AccessibilityCheckerUtils { private static final String LOG_TAG = "AccessibilityCheckerUtils"; @VisibleForTesting // LINT.IfChange static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass> Loading Loading @@ -93,17 +93,6 @@ public class AccessibilityCheckerUtils { AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK)); // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto) static Set<AccessibilityCheckResultReported> processResults( Context context, AccessibilityNodeInfo nodeInfo, List<AccessibilityHierarchyCheckResult> checkResults, @Nullable AccessibilityEvent accessibilityEvent, ComponentName a11yServiceComponentName) { return processResults(nodeInfo, checkResults, accessibilityEvent, context.getPackageManager(), a11yServiceComponentName); } @VisibleForTesting static Set<AccessibilityCheckResultReported> processResults( AccessibilityNodeInfo nodeInfo, List<AccessibilityHierarchyCheckResult> checkResults, Loading @@ -111,12 +100,16 @@ public class AccessibilityCheckerUtils { PackageManager packageManager, ComponentName a11yServiceComponentName) { String appPackageName = nodeInfo.getPackageName().toString(); String nodePath = AccessibilityNodePathBuilder.createNodePath(nodeInfo); if (nodePath == null) { return Set.of(); } AccessibilityCheckResultReported.Builder builder; try { builder = AccessibilityCheckResultReported.newBuilder() .setPackageName(appPackageName) .setAppVersionCode(getAppVersionCode(packageManager, appPackageName)) .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo)) .setUiElementPath(nodePath) .setActivityName(getActivityName(packageManager, accessibilityEvent)) .setWindowTitle(getWindowTitle(nodeInfo)) .setSourceComponentName(a11yServiceComponentName.flattenToString()) Loading services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java +18 −12 Original line number Diff line number Diff line Loading @@ -47,12 +47,15 @@ public final class AccessibilityNodePathBuilder { * * <p>This format is consistent with elements paths in Pre-Launch Reports and the Accessibility * Scanner, starting from the window's root node instead of the first resource name. * TODO (b/344607035): link to ClusteringUtils when AATF is merged in main. * See {@link com.google.android.apps.common.testing.accessibility.framework.ClusteringUtils}. */ public static @Nullable String createNodePath(@NonNull AccessibilityNodeInfo nodeInfo) { String packageName = nodeInfo.getPackageName().toString(); if (packageName == null) { return null; } StringBuilder resourceIdBuilder = getNodePathBuilder(nodeInfo); return resourceIdBuilder == null ? null : String.valueOf(nodeInfo.getPackageName()) + ':' + resourceIdBuilder; return resourceIdBuilder == null ? null : packageName + ':' + resourceIdBuilder; } private static @Nullable StringBuilder getNodePathBuilder(AccessibilityNodeInfo nodeInfo) { Loading Loading @@ -84,20 +87,23 @@ public final class AccessibilityNodePathBuilder { //Returns the part of the element's View ID resource name after the qualifier // "package_name:id/" or the last '/', when available. Otherwise, returns the element's // simple class name. private static CharSequence getShortUiElementName(AccessibilityNodeInfo nodeInfo) { private static @Nullable CharSequence getShortUiElementName(AccessibilityNodeInfo nodeInfo) { String viewIdResourceName = nodeInfo.getViewIdResourceName(); if (viewIdResourceName != null) { if (viewIdResourceName == null) { return getSimpleClassName(nodeInfo); } String idQualifier = ":id/"; int idQualifierStartIndex = viewIdResourceName.indexOf(idQualifier); int unqualifiedNameStartIndex = idQualifierStartIndex == -1 ? 0 : (idQualifierStartIndex + idQualifier.length()); int unqualifiedNameStartIndex = idQualifierStartIndex == -1 ? 0 : (idQualifierStartIndex + idQualifier.length()); return viewIdResourceName.substring(unqualifiedNameStartIndex); } return getSimpleClassName(nodeInfo); } private static CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) { private static @Nullable CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) { CharSequence name = nodeInfo.getClassName(); if (name == null) { return null; } for (int i = name.length() - 1; i > 0; i--) { char ithChar = name.charAt(i); if (ithChar == '.' || ithChar == '$') { Loading services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import static com.android.server.accessibility.Flags.FLAG_ENABLE_A11Y_CHECKER_LOGGING; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MIN_DURATION_BETWEEN_CHECKS; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_CLASS_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_DEFAULT_BROWSER; import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom; import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps; import static com.android.server.accessibility.a11ychecker.TestUtils.getTestAccessibilityEvent; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.pm.PackageManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.view.accessibility.AccessibilityNodeInfo; import androidx.test.runner.AndroidJUnit4; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Set; @RunWith(AndroidJUnit4.class) public class AccessibilityCheckerManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private AccessibilityCheckerManager mAccessibilityCheckerManager; @Before public void setup() throws PackageManager.NameNotFoundException { PackageManager mockPackageManager = getMockPackageManagerWithInstalledApps(); mAccessibilityCheckerManager = new AccessibilityCheckerManager(setupMockChecks(), nodeInfo -> mock(AccessibilityHierarchy.class), mockPackageManager); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_firstUpdate() { assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isTrue(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_minDurationPassed() { mAccessibilityCheckerManager.mTimer.setLastCheckTime( Instant.now().minus(MIN_DURATION_BETWEEN_CHECKS.plus(Duration.ofSeconds(2)))); assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isTrue(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_tooEarly() { mAccessibilityCheckerManager.mTimer.setLastCheckTime( Instant.now().minus(MIN_DURATION_BETWEEN_CHECKS.minus(Duration.ofSeconds(2)))); assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isFalse(); } @Test @DisableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_featureDisabled() { assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isFalse(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_happyPath() { AccessibilityNodeInfo mockNodeInfo1 = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); AccessibilityNodeInfo mockNodeInfo2 = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node2") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo1, mockNodeInfo2), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).containsExactly( createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2), createAtom(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2) ); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_skipsNodesFromDefaultBrowser() { AccessibilityNodeInfo mockNodeInfo = new MockAccessibilityNodeInfoBuilder() .setPackageName(TEST_DEFAULT_BROWSER) .setViewIdResourceName("node1") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).isEmpty(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_doesNotStoreDuplicates() { AccessibilityNodeInfo mockNodeInfo = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); AccessibilityNodeInfo mockNodeInfoDuplicate = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo, mockNodeInfoDuplicate), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).containsExactly( createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2) ); } private Set<AccessibilityHierarchyCheck> setupMockChecks() { AccessibilityHierarchyCheck mockCheck1 = mock(AccessibilityHierarchyCheck.class); AccessibilityHierarchyCheckResult infoTypeResult = new AccessibilityHierarchyCheckResult( TouchTargetSizeCheck.class, AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 1, null); when(mockCheck1.runCheckOnHierarchy(any())).thenReturn(List.of(infoTypeResult)); AccessibilityHierarchyCheck mockCheck2 = mock(AccessibilityHierarchyCheck.class); AccessibilityHierarchyCheckResult errorTypeResult = new AccessibilityHierarchyCheckResult( TouchTargetSizeCheck.class, AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null); when(mockCheck2.runCheckOnHierarchy(any())).thenReturn(List.of(errorTypeResult)); return Set.of(mockCheck1, mockCheck2); } } Loading
services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerConstants.java 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import java.time.Duration; /** * Constants used by the accessibility checker. * * @hide */ final class AccessibilityCheckerConstants { // The min required duration between two consecutive runs of the a11y checker. static final Duration MIN_DURATION_BETWEEN_CHECKS = Duration.ofMinutes(1); // The max number of cached results at a time. static final int MAX_CACHE_CAPACITY = 10000; }
services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java 0 → 100644 +169 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MAX_CACHE_CAPACITY; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MIN_DURATION_BETWEEN_CHECKS; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.server.accessibility.Flags; import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The class responsible for running AccessibilityChecks on cached nodes and caching the results for * logging. Results are cached and capped to limit the logging frequency and size. * * @hide */ public final class AccessibilityCheckerManager { private static final String LOG_TAG = "AccessibilityCheckerManager"; private final PackageManager mPackageManager; private final Set<AccessibilityHierarchyCheck> mHierarchyChecks; private final ATFHierarchyBuilder mATFHierarchyBuilder; private final Set<AccessibilityCheckResultReported> mCachedResults = new HashSet<>(); @VisibleForTesting final A11yCheckerTimer mTimer = new A11yCheckerTimer(); public AccessibilityCheckerManager(Context context) { this(AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( AccessibilityCheckPreset.LATEST), (nodeInfo) -> AccessibilityHierarchyAndroid.newBuilder(nodeInfo, context).build(), context.getPackageManager()); } @VisibleForTesting AccessibilityCheckerManager( Set<AccessibilityHierarchyCheck> hierarchyChecks, ATFHierarchyBuilder atfHierarchyBuilder, PackageManager packageManager) { this.mHierarchyChecks = hierarchyChecks; this.mATFHierarchyBuilder = atfHierarchyBuilder; this.mPackageManager = packageManager; } /** * If eligible, runs AccessibilityChecks on the given nodes and caches the results for later * logging. Returns the check results for the given nodes. */ @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public Set<AccessibilityCheckResultReported> maybeRunA11yChecker( List<AccessibilityNodeInfo> nodes, @Nullable AccessibilityEvent accessibilityEvent, ComponentName sourceComponentName, @UserIdInt int userId) { if (!shouldRunA11yChecker()) { return Set.of(); } Set<AccessibilityCheckResultReported> allResults = new HashSet<>(); String defaultBrowserName = mPackageManager.getDefaultBrowserPackageNameAsUser(userId); try { for (AccessibilityNodeInfo nodeInfo : nodes) { // Skip browser results because they are mostly related to web content and not the // browser app itself. if (nodeInfo.getPackageName() == null || nodeInfo.getPackageName().toString().equals(defaultBrowserName)) { continue; } List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode(nodeInfo); Set<AccessibilityCheckResultReported> filteredResults = AccessibilityCheckerUtils.processResults(nodeInfo, checkResults, accessibilityEvent, mPackageManager, sourceComponentName); allResults.addAll(filteredResults); } mCachedResults.addAll(allResults); } catch (RuntimeException e) { Slog.e(LOG_TAG, "An unknown error occurred while running a11y checker.", e); } return allResults; } private List<AccessibilityHierarchyCheckResult> runChecksOnNode( AccessibilityNodeInfo nodeInfo) { AccessibilityHierarchy checkableHierarchy = mATFHierarchyBuilder.getATFCheckableHierarchy( nodeInfo); List<AccessibilityHierarchyCheckResult> checkResults = new ArrayList<>(); for (AccessibilityHierarchyCheck check : mHierarchyChecks) { checkResults.addAll(check.runCheckOnHierarchy(checkableHierarchy)); } return checkResults; } public Set<AccessibilityCheckResultReported> getCachedResults() { return Collections.unmodifiableSet(mCachedResults); } @VisibleForTesting boolean shouldRunA11yChecker() { if (!Flags.enableA11yCheckerLogging() || mCachedResults.size() == MAX_CACHE_CAPACITY) { return false; } if (mTimer.getLastCheckTime() == null || mTimer.getLastCheckTime().plus( MIN_DURATION_BETWEEN_CHECKS).isBefore(Instant.now())) { mTimer.setLastCheckTime(Instant.now()); return true; } return false; } /** Timer class to facilitate testing with fake times. */ @VisibleForTesting static class A11yCheckerTimer { private Instant mLastCheckTime = null; Instant getLastCheckTime() { return mLastCheckTime; } void setLastCheckTime(Instant newTime) { mLastCheckTime = newTime; } } /** AccessibilityHierarchy wrapper to facilitate testing with fake hierarchies. */ @VisibleForTesting interface ATFHierarchyBuilder { AccessibilityHierarchy getATFCheckableHierarchy(AccessibilityNodeInfo nodeInfo); } }
services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java +6 −13 Original line number Diff line number Diff line Loading @@ -19,7 +19,6 @@ package com.android.server.accessibility.a11ychecker; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Slog; Loading Loading @@ -62,6 +61,7 @@ import java.util.stream.Collectors; public class AccessibilityCheckerUtils { private static final String LOG_TAG = "AccessibilityCheckerUtils"; @VisibleForTesting // LINT.IfChange static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass> Loading Loading @@ -93,17 +93,6 @@ public class AccessibilityCheckerUtils { AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK)); // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto) static Set<AccessibilityCheckResultReported> processResults( Context context, AccessibilityNodeInfo nodeInfo, List<AccessibilityHierarchyCheckResult> checkResults, @Nullable AccessibilityEvent accessibilityEvent, ComponentName a11yServiceComponentName) { return processResults(nodeInfo, checkResults, accessibilityEvent, context.getPackageManager(), a11yServiceComponentName); } @VisibleForTesting static Set<AccessibilityCheckResultReported> processResults( AccessibilityNodeInfo nodeInfo, List<AccessibilityHierarchyCheckResult> checkResults, Loading @@ -111,12 +100,16 @@ public class AccessibilityCheckerUtils { PackageManager packageManager, ComponentName a11yServiceComponentName) { String appPackageName = nodeInfo.getPackageName().toString(); String nodePath = AccessibilityNodePathBuilder.createNodePath(nodeInfo); if (nodePath == null) { return Set.of(); } AccessibilityCheckResultReported.Builder builder; try { builder = AccessibilityCheckResultReported.newBuilder() .setPackageName(appPackageName) .setAppVersionCode(getAppVersionCode(packageManager, appPackageName)) .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo)) .setUiElementPath(nodePath) .setActivityName(getActivityName(packageManager, accessibilityEvent)) .setWindowTitle(getWindowTitle(nodeInfo)) .setSourceComponentName(a11yServiceComponentName.flattenToString()) Loading
services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java +18 −12 Original line number Diff line number Diff line Loading @@ -47,12 +47,15 @@ public final class AccessibilityNodePathBuilder { * * <p>This format is consistent with elements paths in Pre-Launch Reports and the Accessibility * Scanner, starting from the window's root node instead of the first resource name. * TODO (b/344607035): link to ClusteringUtils when AATF is merged in main. * See {@link com.google.android.apps.common.testing.accessibility.framework.ClusteringUtils}. */ public static @Nullable String createNodePath(@NonNull AccessibilityNodeInfo nodeInfo) { String packageName = nodeInfo.getPackageName().toString(); if (packageName == null) { return null; } StringBuilder resourceIdBuilder = getNodePathBuilder(nodeInfo); return resourceIdBuilder == null ? null : String.valueOf(nodeInfo.getPackageName()) + ':' + resourceIdBuilder; return resourceIdBuilder == null ? null : packageName + ':' + resourceIdBuilder; } private static @Nullable StringBuilder getNodePathBuilder(AccessibilityNodeInfo nodeInfo) { Loading Loading @@ -84,20 +87,23 @@ public final class AccessibilityNodePathBuilder { //Returns the part of the element's View ID resource name after the qualifier // "package_name:id/" or the last '/', when available. Otherwise, returns the element's // simple class name. private static CharSequence getShortUiElementName(AccessibilityNodeInfo nodeInfo) { private static @Nullable CharSequence getShortUiElementName(AccessibilityNodeInfo nodeInfo) { String viewIdResourceName = nodeInfo.getViewIdResourceName(); if (viewIdResourceName != null) { if (viewIdResourceName == null) { return getSimpleClassName(nodeInfo); } String idQualifier = ":id/"; int idQualifierStartIndex = viewIdResourceName.indexOf(idQualifier); int unqualifiedNameStartIndex = idQualifierStartIndex == -1 ? 0 : (idQualifierStartIndex + idQualifier.length()); int unqualifiedNameStartIndex = idQualifierStartIndex == -1 ? 0 : (idQualifierStartIndex + idQualifier.length()); return viewIdResourceName.substring(unqualifiedNameStartIndex); } return getSimpleClassName(nodeInfo); } private static CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) { private static @Nullable CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) { CharSequence name = nodeInfo.getClassName(); if (name == null) { return null; } for (int i = name.length() - 1; i > 0; i--) { char ithChar = name.charAt(i); if (ithChar == '.' || ithChar == '$') { Loading
services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java 0 → 100644 +192 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.accessibility.a11ychecker; import static com.android.server.accessibility.Flags.FLAG_ENABLE_A11Y_CHECKER_LOGGING; import static com.android.server.accessibility.a11ychecker.AccessibilityCheckerConstants.MIN_DURATION_BETWEEN_CHECKS; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_CLASS_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME; import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_DEFAULT_BROWSER; import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom; import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps; import static com.android.server.accessibility.a11ychecker.TestUtils.getTestAccessibilityEvent; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.pm.PackageManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.view.accessibility.AccessibilityNodeInfo; import androidx.test.runner.AndroidJUnit4; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Set; @RunWith(AndroidJUnit4.class) public class AccessibilityCheckerManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private AccessibilityCheckerManager mAccessibilityCheckerManager; @Before public void setup() throws PackageManager.NameNotFoundException { PackageManager mockPackageManager = getMockPackageManagerWithInstalledApps(); mAccessibilityCheckerManager = new AccessibilityCheckerManager(setupMockChecks(), nodeInfo -> mock(AccessibilityHierarchy.class), mockPackageManager); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_firstUpdate() { assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isTrue(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_minDurationPassed() { mAccessibilityCheckerManager.mTimer.setLastCheckTime( Instant.now().minus(MIN_DURATION_BETWEEN_CHECKS.plus(Duration.ofSeconds(2)))); assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isTrue(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_tooEarly() { mAccessibilityCheckerManager.mTimer.setLastCheckTime( Instant.now().minus(MIN_DURATION_BETWEEN_CHECKS.minus(Duration.ofSeconds(2)))); assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isFalse(); } @Test @DisableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void shouldRunA11yChecker_featureDisabled() { assertThat(mAccessibilityCheckerManager.shouldRunA11yChecker()).isFalse(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_happyPath() { AccessibilityNodeInfo mockNodeInfo1 = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); AccessibilityNodeInfo mockNodeInfo2 = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node2") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo1, mockNodeInfo2), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).containsExactly( createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2), createAtom(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2) ); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_skipsNodesFromDefaultBrowser() { AccessibilityNodeInfo mockNodeInfo = new MockAccessibilityNodeInfoBuilder() .setPackageName(TEST_DEFAULT_BROWSER) .setViewIdResourceName("node1") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).isEmpty(); } @Test @EnableFlags(FLAG_ENABLE_A11Y_CHECKER_LOGGING) public void maybeRunA11yChecker_doesNotStoreDuplicates() { AccessibilityNodeInfo mockNodeInfo = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); AccessibilityNodeInfo mockNodeInfoDuplicate = new MockAccessibilityNodeInfoBuilder() .setViewIdResourceName("node1") .build(); Set<A11yCheckerProto.AccessibilityCheckResultReported> results = mAccessibilityCheckerManager.maybeRunA11yChecker( List.of(mockNodeInfo, mockNodeInfoDuplicate), getTestAccessibilityEvent(), new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0); assertThat(results).containsExactly( createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME, A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK, A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2) ); } private Set<AccessibilityHierarchyCheck> setupMockChecks() { AccessibilityHierarchyCheck mockCheck1 = mock(AccessibilityHierarchyCheck.class); AccessibilityHierarchyCheckResult infoTypeResult = new AccessibilityHierarchyCheckResult( TouchTargetSizeCheck.class, AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 1, null); when(mockCheck1.runCheckOnHierarchy(any())).thenReturn(List.of(infoTypeResult)); AccessibilityHierarchyCheck mockCheck2 = mock(AccessibilityHierarchyCheck.class); AccessibilityHierarchyCheckResult errorTypeResult = new AccessibilityHierarchyCheckResult( TouchTargetSizeCheck.class, AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null); when(mockCheck2.runCheckOnHierarchy(any())).thenReturn(List.of(errorTypeResult)); return Set.of(mockCheck1, mockCheck2); } }