Add utils to process a11y check results

Also moved a11ychecker from core/... to services/accessibility

NO_IFTTT=New IFTTT

Bug: 350530488
Bug: 341926585
Test: unit tests
Flag: com.android.server.accessibility.enable_a11y_checker_logging
Change-Id: Ibc0cb4eb2d207032ef21a46196d4b2cc04cf7e95
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 7a99b60..311addb 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -29,10 +29,12 @@
         "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
     ],
     libs: [
+        "aatf",
         "services.core",
         "androidx.annotation_annotation",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
 
@@ -68,3 +70,14 @@
     name: "com_android_server_accessibility_flags_lib",
     aconfig_declarations: "com_android_server_accessibility_flags",
 }
+
+java_library_static {
+    name: "a11ychecker-protos-java-proto-lite",
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    srcs: [
+        "java/**/a11ychecker/proto/*.proto",
+    ],
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
new file mode 100644
index 0000000..55af9a0
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
@@ -0,0 +1,218 @@
+/*
+ * 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 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;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;
+
+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.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Util class to process a11y checker results for logging.
+ *
+ * @hide
+ */
+public class AccessibilityCheckerUtils {
+
+    private static final String LOG_TAG = "AccessibilityCheckerUtils";
+    @VisibleForTesting
+    // LINT.IfChange
+    static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass>
+            CHECK_CLASS_TO_ENUM_MAP =
+            Map.ofEntries(
+                    classMapEntry(ClassNameCheck.class, AccessibilityCheckClass.CLASS_NAME_CHECK),
+                    classMapEntry(ClickableSpanCheck.class,
+                            AccessibilityCheckClass.CLICKABLE_SPAN_CHECK),
+                    classMapEntry(DuplicateClickableBoundsCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_CLICKABLE_BOUNDS_CHECK),
+                    classMapEntry(DuplicateSpeakableTextCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_SPEAKABLE_TEXT_CHECK),
+                    classMapEntry(EditableContentDescCheck.class,
+                            AccessibilityCheckClass.EDITABLE_CONTENT_DESC_CHECK),
+                    classMapEntry(ImageContrastCheck.class,
+                            AccessibilityCheckClass.IMAGE_CONTRAST_CHECK),
+                    classMapEntry(LinkPurposeUnclearCheck.class,
+                            AccessibilityCheckClass.LINK_PURPOSE_UNCLEAR_CHECK),
+                    classMapEntry(RedundantDescriptionCheck.class,
+                            AccessibilityCheckClass.REDUNDANT_DESCRIPTION_CHECK),
+                    classMapEntry(SpeakableTextPresentCheck.class,
+                            AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK),
+                    classMapEntry(TextContrastCheck.class,
+                            AccessibilityCheckClass.TEXT_CONTRAST_CHECK),
+                    classMapEntry(TextSizeCheck.class, AccessibilityCheckClass.TEXT_SIZE_CHECK),
+                    classMapEntry(TouchTargetSizeCheck.class,
+                            AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK),
+                    classMapEntry(TraversalOrderCheck.class,
+                            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,
+            @Nullable AccessibilityEvent accessibilityEvent,
+            PackageManager packageManager,
+            ComponentName a11yServiceComponentName) {
+        String appPackageName = nodeInfo.getPackageName().toString();
+        AccessibilityCheckResultReported.Builder builder;
+        try {
+            builder = AccessibilityCheckResultReported.newBuilder()
+                    .setPackageName(appPackageName)
+                    .setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
+                    .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo))
+                    .setActivityName(getActivityName(packageManager, accessibilityEvent))
+                    .setWindowTitle(getWindowTitle(nodeInfo))
+                    .setSourceComponentName(a11yServiceComponentName.flattenToString())
+                    .setSourceVersionCode(
+                            getAppVersionCode(packageManager,
+                                    a11yServiceComponentName.getPackageName()));
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(LOG_TAG, "Unknown package name", e);
+            return Set.of();
+        }
+
+        return checkResults.stream()
+                .filter(checkResult -> checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
+                        || checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
+                .map(checkResult -> builder.setResultCheckClass(
+                        getCheckClass(checkResult)).setResultType(
+                        getCheckResultType(checkResult)).setResultId(
+                        checkResult.getResultId()).build())
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+    private static long getAppVersionCode(PackageManager packageManager, String packageName) throws
+            PackageManager.NameNotFoundException {
+        PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
+        return packageInfo.getLongVersionCode();
+    }
+
+    /**
+     * Returns the simple class name of the Activity providing the cache update, if available,
+     * or an empty String if not.
+     */
+    @VisibleForTesting
+    static String getActivityName(
+            PackageManager packageManager, @Nullable AccessibilityEvent accessibilityEvent) {
+        if (accessibilityEvent == null) {
+            return "";
+        }
+        CharSequence activityName = accessibilityEvent.getClassName();
+        if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+                && accessibilityEvent.getPackageName() != null
+                && activityName != null) {
+            try {
+                // Check class is for a valid Activity.
+                packageManager
+                        .getActivityInfo(
+                                new ComponentName(accessibilityEvent.getPackageName().toString(),
+                                        activityName.toString()), 0);
+                int qualifierEnd = activityName.toString().lastIndexOf('.');
+                return activityName.toString().substring(qualifierEnd + 1);
+            } catch (PackageManager.NameNotFoundException e) {
+                // No need to spam the logs. This is very frequent when the class doesn't match
+                // an activity.
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Returns the title of the window containing the a11y node.
+     */
+    private static String getWindowTitle(AccessibilityNodeInfo nodeInfo) {
+        if (nodeInfo.getWindow() == null) {
+            return "";
+        }
+        CharSequence windowTitle = nodeInfo.getWindow().getTitle();
+        return windowTitle == null ? "" : windowTitle.toString();
+    }
+
+    /**
+     * Maps the {@link AccessibilityHierarchyCheck} class that produced the given result, with the
+     * corresponding {@link AccessibilityCheckClass} enum. This enumeration is to avoid relying on
+     * String class names in the logging, which can be proguarded. It also reduces the logging size.
+     */
+    private static AccessibilityCheckClass getCheckClass(
+            AccessibilityHierarchyCheckResult checkResult) {
+        if (CHECK_CLASS_TO_ENUM_MAP.containsKey(checkResult.getSourceCheckClass())) {
+            return CHECK_CLASS_TO_ENUM_MAP.get(checkResult.getSourceCheckClass());
+        }
+        return AccessibilityCheckClass.UNKNOWN_CHECK;
+    }
+
+    private static AccessibilityCheckResultType getCheckResultType(
+            AccessibilityHierarchyCheckResult checkResult) {
+        return switch (checkResult.getType()) {
+            case ERROR -> AccessibilityCheckResultType.ERROR;
+            case WARNING -> AccessibilityCheckResultType.WARNING;
+            default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
+        };
+    }
+
+    private static Map.Entry<Class<? extends AccessibilityHierarchyCheck>,
+            AccessibilityCheckClass> classMapEntry(
+            Class<? extends AccessibilityHierarchyCheck> checkClass,
+            AccessibilityCheckClass checkClassEnum) {
+        return new AbstractMap.SimpleImmutableEntry<>(checkClass, checkClassEnum);
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
new file mode 100644
index 0000000..bbfb217
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
@@ -0,0 +1,112 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Utility class to create developer-friendly {@link AccessibilityNodeInfo} path Strings for use
+ * in reporting AccessibilityCheck results.
+ *
+ * @hide
+ */
+public final class AccessibilityNodePathBuilder {
+
+    /**
+     * Returns the path of the node within its accessibility hierarchy starting from the root node
+     * down to the given node itself, and prefixed by the package name. This path is not guaranteed
+     * to be unique. This can return null in case the node's hierarchy changes while scanning.
+     *
+     * <p>Each element in the path is represented by its View ID resource name, when available, or
+     * the
+     * simple class name if not. The path also includes the index of each child node relative to
+     * its
+     * parent. See {@link AccessibilityNodeInfo#getViewIdResourceName()}.
+     *
+     * <p>For example,
+     * "com.example.app:RootElementClassName/parent_resource_name[1]/TargetElementClassName[3]"
+     * indicates the element has type {@code TargetElementClassName}, and is the third child of an
+     * element with the resource name {@code parent_resource_name}, which is the first child of an
+     * element of type {@code RootElementClassName}.
+     *
+     * <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.
+     */
+    public static @Nullable String createNodePath(@NonNull AccessibilityNodeInfo nodeInfo) {
+        StringBuilder resourceIdBuilder = getNodePathBuilder(nodeInfo);
+        return resourceIdBuilder == null ? null : String.valueOf(nodeInfo.getPackageName()) + ':'
+                + resourceIdBuilder;
+    }
+
+    private static @Nullable StringBuilder getNodePathBuilder(AccessibilityNodeInfo nodeInfo) {
+        AccessibilityNodeInfo parent = nodeInfo.getParent();
+        if (parent == null) {
+            return new StringBuilder(getShortUiElementName(nodeInfo));
+        }
+        StringBuilder parentNodePath = getNodePathBuilder(parent);
+        if (parentNodePath == null) {
+            return null;
+        }
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            if (!nodeInfo.equals(parent.getChild(i))) {
+                continue;
+            }
+            CharSequence uiElementName = getShortUiElementName(nodeInfo);
+            if (uiElementName != null) {
+                parentNodePath.append('/').append(uiElementName).append('[').append(i + 1).append(
+                        ']');
+            } else {
+                parentNodePath.append(":nth-child(").append(i + 1).append(')');
+            }
+            return parentNodePath;
+        }
+        return null;
+    }
+
+    //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) {
+        String viewIdResourceName = nodeInfo.getViewIdResourceName();
+        if (viewIdResourceName != null) {
+            String idQualifier = ":id/";
+            int idQualifierStartIndex = viewIdResourceName.indexOf(idQualifier);
+            int unqualifiedNameStartIndex = idQualifierStartIndex == -1 ? 0
+                    : (idQualifierStartIndex + idQualifier.length());
+            return viewIdResourceName.substring(unqualifiedNameStartIndex);
+        }
+        return getSimpleClassName(nodeInfo);
+    }
+
+    private static CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) {
+        CharSequence name = nodeInfo.getClassName();
+        for (int i = name.length() - 1; i > 0; i--) {
+            char ithChar = name.charAt(i);
+            if (ithChar == '.' || ithChar == '$') {
+                return name.subSequence(i + 1, name.length());
+            }
+        }
+        return name;
+    }
+
+    private AccessibilityNodePathBuilder() {
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
new file mode 100644
index 0000000..d1e7986
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
@@ -0,0 +1,4 @@
+# Android Accessibility Framework owners
+include /services/accessibility/OWNERS
+
+yaraabdullatif@google.com
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
new file mode 100644
index 0000000..8beed4a
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 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.
+ */
+syntax = "proto2";
+package android.accessibility;
+
+option java_package = "com.android.server.accessibility.a11ychecker";
+option java_outer_classname = "A11yCheckerProto";
+
+// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted.
+/** Logs the result of an AccessibilityCheck. */
+message AccessibilityCheckResultReported {
+  // Package name of the app containing the checked View.
+  optional string package_name = 1;
+  // Version code of the app containing the checked View.
+  optional int64 app_version_code = 2;
+  // The path of the View starting from the root element in the window. Each element is
+  // represented by the View's resource id, when available, or the View's class name.
+  optional string ui_element_path = 3;
+  // Class name of the activity containing the checked View.
+  optional string activity_name = 4;
+  // Title of the window containing the checked View.
+  optional string window_title = 5;
+  // The flattened component name of the app running the AccessibilityService which provided the a11y node.
+  optional string source_component_name = 6;
+  // Version code of the app running the AccessibilityService that provided the a11y node.
+  optional int64 source_version_code = 7;
+  // Class Name of the AccessibilityCheck that produced the result.
+  optional AccessibilityCheckClass result_check_class = 8;
+  // Result type of the AccessibilityCheckResult.
+  optional AccessibilityCheckResultType result_type = 9;
+  // Result ID of the AccessibilityCheckResult.
+  optional int32 result_id = 10;
+}
+
+/** The AccessibilityCheck class. */
+// LINT.IfChange
+enum AccessibilityCheckClass {
+  UNKNOWN_CHECK = 0;
+  CLASS_NAME_CHECK = 1;
+  CLICKABLE_SPAN_CHECK = 2;
+  DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3;
+  DUPLICATE_SPEAKABLE_TEXT_CHECK = 4;
+  EDITABLE_CONTENT_DESC_CHECK = 5;
+  IMAGE_CONTRAST_CHECK = 6;
+  LINK_PURPOSE_UNCLEAR_CHECK = 7;
+  REDUNDANT_DESCRIPTION_CHECK = 8;
+  SPEAKABLE_TEXT_PRESENT_CHECK = 9;
+  TEXT_CONTRAST_CHECK = 10;
+  TEXT_SIZE_CHECK = 11;
+  TOUCH_TARGET_SIZE_CHECK = 12;
+  TRAVERSAL_ORDER_CHECK = 13;
+}
+// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java)
+
+/** The type of AccessibilityCheckResult */
+enum AccessibilityCheckResultType {
+  UNKNOWN_RESULT_TYPE = 0;
+  ERROR = 1;
+  WARNING = 2;
+}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 753db12..b9e99dd 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,6 +36,8 @@
         "-Werror",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
+        "aatf",
         "cts-input-lib",
         "frameworks-base-testutils",
         "services.accessibility",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
new file mode 100644
index 0000000..90d4275
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.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_A11Y_SERVICE_SOURCE_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
+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.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityCheckerUtilsTest {
+
+    PackageManager mMockPackageManager;
+
+    @Before
+    public void setUp() throws PackageManager.NameNotFoundException {
+        mMockPackageManager = getMockPackageManagerWithInstalledApps();
+    }
+
+    @Test
+    public void processResults_happyPath_setsAllFields() {
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        SpeakableTextPresentCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+        AccessibilityHierarchyCheckResult result3 =
+                new AccessibilityHierarchyCheckResult(
+                        ClassNameCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 5, null);
+        AccessibilityHierarchyCheckResult result4 =
+                new AccessibilityHierarchyCheckResult(
+                        ClickableSpanCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5,
+                        null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2, result3, result4),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).containsExactly(
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1),
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2)
+        );
+    }
+
+    @Test
+    public void processResults_packageNameNotFound_returnsEmptySet()
+            throws PackageManager.NameNotFoundException {
+        when(mMockPackageManager.getPackageInfo("com.uninstalled.app", 0))
+                .thenThrow(PackageManager.NameNotFoundException.class);
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setPackageName("com.uninstalled.app")
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).isEmpty();
+    }
+
+    @Test
+    public void getActivityName_hasWindowStateChangedEvent_returnsActivityName() {
+        AccessibilityEvent accessibilityEvent =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        accessibilityEvent.setPackageName(TEST_APP_PACKAGE_NAME);
+        accessibilityEvent.setClassName(TEST_ACTIVITY_NAME);
+
+        assertThat(AccessibilityCheckerUtils.getActivityName(mMockPackageManager,
+                accessibilityEvent)).isEqualTo("MainActivity");
+    }
+
+    // Makes sure the AccessibilityHierarchyCheck class to enum mapping is up to date with the
+    // latest prod preset.
+    @Test
+    public void checkClassToEnumMap_hasAllLatestPreset() {
+        ImmutableSet<AccessibilityHierarchyCheck> checkPreset =
+                AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(
+                        AccessibilityCheckPreset.LATEST);
+        Set<Class<? extends AccessibilityHierarchyCheck>> latestCheckClasses =
+                checkPreset.stream().map(AccessibilityHierarchyCheck::getClass).collect(
+                        Collectors.toUnmodifiableSet());
+
+        assertThat(AccessibilityCheckerUtils.CHECK_CLASS_TO_ENUM_MAP.keySet())
+                .containsExactlyElementsIn(latestCheckClasses);
+    }
+
+
+    private static A11yCheckerProto.AccessibilityCheckResultReported createAtom(
+            A11yCheckerProto.AccessibilityCheckClass checkClass,
+            A11yCheckerProto.AccessibilityCheckResultType resultType,
+            int resultId) {
+        return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder()
+                .setPackageName(TEST_APP_PACKAGE_NAME)
+                .setAppVersionCode(TEST_APP_VERSION_CODE)
+                .setUiElementPath(TEST_APP_PACKAGE_NAME + ":TargetNode")
+                .setWindowTitle(TEST_WINDOW_TITLE)
+                .setActivityName("")
+                .setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_CLASS_NAME).flattenToString())
+                .setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE)
+                .setResultCheckClass(checkClass)
+                .setResultType(resultType)
+                .setResultId(resultId)
+                .build();
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
new file mode 100644
index 0000000..a53f42e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.TestUtils.TEST_APP_PACKAGE_NAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.widget.RecyclerView;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityNodePathBuilderTest {
+
+    public static final String RESOURCE_ID_PREFIX = TEST_APP_PACKAGE_NAME + ":id/";
+
+    @Test
+    public void createNodePath_pathWithResourceNames() {
+        AccessibilityNodeInfo child = new MockAccessibilityNodeInfoBuilder()
+                .setViewIdResourceName(RESOURCE_ID_PREFIX + "child_node")
+                .build();
+        AccessibilityNodeInfo parent =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "parent_node")
+                        .addChildren(ImmutableList.of(child))
+                        .build();
+        AccessibilityNodeInfo root =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "root_node")
+                        .addChildren(ImmutableList.of(parent))
+                        .build();
+
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(root))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node");
+    }
+
+    @Test
+    public void createNodePath_pathWithoutResourceNames() {
+        AccessibilityNodeInfo child =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setClassName(TextView.class.getName())
+                        .build();
+        AccessibilityNodeInfo parent =
+
+                new MockAccessibilityNodeInfoBuilder()
+                        .setClassName(RecyclerView.class.getName())
+                        .addChildren(ImmutableList.of(child))
+                        .build();
+        AccessibilityNodeInfo root =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setClassName(FrameLayout.class.getName())
+                        .addChildren(ImmutableList.of(parent))
+                        .build();
+
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(root))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
+    }
+
+    @Test
+    public void createNodePath_parentWithMultipleChildren() {
+        AccessibilityNodeInfo child1 =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "child1")
+                        .build();
+        AccessibilityNodeInfo child2 =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setClassName(TextView.class.getName())
+                        .build();
+        AccessibilityNodeInfo parent =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setClassName(FrameLayout.class.getName())
+                        .addChildren(ImmutableList.of(child1, child2))
+                        .build();
+
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/child1[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/TextView[2]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
+    }
+
+    @Test
+    public void createNodePath_handlesDifferentIdFormats() {
+        AccessibilityNodeInfo child1 =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "childId")
+                        .build();
+        AccessibilityNodeInfo child2 =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "child/Id/With/Slash")
+                        .build();
+        AccessibilityNodeInfo child3 =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName("childIdWithoutPrefix")
+                        .build();
+        AccessibilityNodeInfo parent =
+                new MockAccessibilityNodeInfoBuilder()
+                        .addChildren(ImmutableList.of(child1, child2, child3))
+                        .setViewIdResourceName(RESOURCE_ID_PREFIX + "parentId")
+                        .build();
+
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childId[1]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(child3))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
+        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId");
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
new file mode 100644
index 0000000..7cd3535
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import java.util.List;
+
+final class MockAccessibilityNodeInfoBuilder {
+    private final AccessibilityNodeInfo mMockNodeInfo = mock(AccessibilityNodeInfo.class);
+
+    MockAccessibilityNodeInfoBuilder() {
+        setPackageName(TEST_APP_PACKAGE_NAME);
+
+        AccessibilityWindowInfo windowInfo = new AccessibilityWindowInfo();
+        windowInfo.setTitle(TEST_WINDOW_TITLE);
+        when(mMockNodeInfo.getWindow()).thenReturn(windowInfo);
+    }
+
+    MockAccessibilityNodeInfoBuilder setPackageName(String packageName) {
+        when(mMockNodeInfo.getPackageName()).thenReturn(packageName);
+        return this;
+    }
+
+    MockAccessibilityNodeInfoBuilder setClassName(String className) {
+        when(mMockNodeInfo.getClassName()).thenReturn(className);
+        return this;
+    }
+
+    MockAccessibilityNodeInfoBuilder setViewIdResourceName(String
+            viewIdResourceName) {
+        when(mMockNodeInfo.getViewIdResourceName()).thenReturn(viewIdResourceName);
+        return this;
+    }
+
+    MockAccessibilityNodeInfoBuilder addChildren(List<AccessibilityNodeInfo>
+            children) {
+        when(mMockNodeInfo.getChildCount()).thenReturn(children.size());
+        for (int i = 0; i < children.size(); i++) {
+            when(mMockNodeInfo.getChild(i)).thenReturn(children.get(i));
+            when(children.get(i).getParent()).thenReturn(mMockNodeInfo);
+        }
+        return this;
+    }
+
+    AccessibilityNodeInfo build() {
+        return mMockNodeInfo;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
new file mode 100644
index 0000000..7bdc029
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
@@ -0,0 +1 @@
+include /services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
new file mode 100644
index 0000000..a04bbee
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
@@ -0,0 +1,74 @@
+/*
+ * 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 org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import org.mockito.Mockito;
+
+public class TestUtils {
+    static final String TEST_APP_PACKAGE_NAME = "com.example.app";
+    static final int TEST_APP_VERSION_CODE = 12321;
+    static final String TEST_ACTIVITY_NAME = "com.example.app.MainActivity";
+    static final String TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME = "com.assistive.app";
+    static final String TEST_A11Y_SERVICE_CLASS_NAME = "MyA11yService";
+    static final int TEST_A11Y_SERVICE_SOURCE_VERSION_CODE = 333555;
+    static final String TEST_WINDOW_TITLE = "Example window";
+
+    static PackageManager getMockPackageManagerWithInstalledApps()
+            throws PackageManager.NameNotFoundException {
+        PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+        ActivityInfo testActivityInfo = getTestActivityInfo();
+        ComponentName testActivityComponentName = new ComponentName(TEST_APP_PACKAGE_NAME,
+                TEST_ACTIVITY_NAME);
+
+        when(mockPackageManager.getActivityInfo(testActivityComponentName, 0))
+                .thenReturn(testActivityInfo);
+        when(mockPackageManager.getPackageInfo(TEST_APP_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_APP_PACKAGE_NAME, TEST_APP_VERSION_CODE,
+                        testActivityInfo));
+        when(mockPackageManager.getPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_SOURCE_VERSION_CODE, null));
+        return mockPackageManager;
+    }
+
+    static ActivityInfo getTestActivityInfo() {
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = TEST_APP_PACKAGE_NAME;
+        activityInfo.name = TEST_ACTIVITY_NAME;
+        return activityInfo;
+    }
+
+    static PackageInfo createPackageInfo(String packageName, int versionCode,
+            @Nullable ActivityInfo activityInfo) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.setLongVersionCode(versionCode);
+        if (activityInfo != null) {
+            packageInfo.activities = new ActivityInfo[]{activityInfo};
+        }
+        return packageInfo;
+
+    }
+}