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

Commit 243bc2ab authored by Yara Hassan's avatar Yara Hassan
Browse files

Add AccessibilityNodePathBuilder

Creates node paths for ANIs to identify elements in a11y checker results.

Test: unit tests
Change-Id: If76a19f1bb9fe64fb471e455b3fd5016a4996685
Bug: b/341925733
Flag: com.android.server.accessibility.enable_a11y_checker_logging
parent 4257d776
Loading
Loading
Loading
Loading
+112 −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 android.view.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() {
    }
}
+7 −0
Original line number Diff line number Diff line
java_library_static {
    name: "A11yChecker",
    srcs: [
        "*.java",
    ],
    visibility: ["//visibility:public"],
}
+4 −0
Original line number Diff line number Diff line
# Android Accessibility Framework owners
include /services/accessibility/OWNERS

yaraabdullatif@google.com
+1 −0
Original line number Diff line number Diff line
@@ -63,6 +63,7 @@ android_test {
        "-c fa",
    ],
    static_libs: [
        "A11yChecker",
        "collector-device-lib-platform",
        "frameworks-base-testutils",
        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
+145 −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 android.view.accessibility.a11ychecker;

import static android.view.accessibility.a11ychecker.MockAccessibilityNodeInfoBuilder.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 = 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(PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
                .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(root))
                .isEqualTo(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(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
                .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(root))
                .isEqualTo(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(PACKAGE_NAME + ":FrameLayout/child1[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
                .isEqualTo(PACKAGE_NAME + ":FrameLayout/TextView[2]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
                .isEqualTo(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(PACKAGE_NAME + ":parentId/childId[1]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
                .isEqualTo(PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(child3))
                .isEqualTo(PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
        assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
                .isEqualTo(PACKAGE_NAME + ":parentId");
    }

}
Loading