1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.accessibility.a11ychecker;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.view.accessibility.AccessibilityNodeInfo;
22 
23 /**
24  * Utility class to create developer-friendly {@link AccessibilityNodeInfo} path Strings for use
25  * in reporting AccessibilityCheck results.
26  *
27  * @hide
28  */
29 public final class AccessibilityNodePathBuilder {
30 
31     /**
32      * Returns the path of the node within its accessibility hierarchy starting from the root node
33      * down to the given node itself, and prefixed by the package name. This path is not guaranteed
34      * to be unique. This can return null in case the node's hierarchy changes while scanning.
35      *
36      * <p>Each element in the path is represented by its View ID resource name, when available, or
37      * the
38      * simple class name if not. The path also includes the index of each child node relative to
39      * its
40      * parent. See {@link AccessibilityNodeInfo#getViewIdResourceName()}.
41      *
42      * <p>For example,
43      * "com.example.app:RootElementClassName/parent_resource_name[1]/TargetElementClassName[3]"
44      * indicates the element has type {@code TargetElementClassName}, and is the third child of an
45      * element with the resource name {@code parent_resource_name}, which is the first child of an
46      * element of type {@code RootElementClassName}.
47      *
48      * <p>This format is consistent with elements paths in Pre-Launch Reports and the Accessibility
49      * Scanner, starting from the window's root node instead of the first resource name.
50      * See {@link com.google.android.apps.common.testing.accessibility.framework.ClusteringUtils}.
51      */
createNodePath(@onNull AccessibilityNodeInfo nodeInfo)52     public static @Nullable String createNodePath(@NonNull AccessibilityNodeInfo nodeInfo) {
53         String packageName = nodeInfo.getPackageName().toString();
54         if (packageName == null) {
55             return null;
56         }
57         StringBuilder resourceIdBuilder = getNodePathBuilder(nodeInfo);
58         return resourceIdBuilder == null ? null : packageName + ':' + resourceIdBuilder;
59     }
60 
getNodePathBuilder(AccessibilityNodeInfo nodeInfo)61     private static @Nullable StringBuilder getNodePathBuilder(AccessibilityNodeInfo nodeInfo) {
62         AccessibilityNodeInfo parent = nodeInfo.getParent();
63         if (parent == null) {
64             return new StringBuilder(getShortUiElementName(nodeInfo));
65         }
66         StringBuilder parentNodePath = getNodePathBuilder(parent);
67         if (parentNodePath == null) {
68             return null;
69         }
70         int childCount = parent.getChildCount();
71         for (int i = 0; i < childCount; i++) {
72             if (!nodeInfo.equals(parent.getChild(i))) {
73                 continue;
74             }
75             CharSequence uiElementName = getShortUiElementName(nodeInfo);
76             if (uiElementName != null) {
77                 parentNodePath.append('/').append(uiElementName).append('[').append(i + 1).append(
78                         ']');
79             } else {
80                 parentNodePath.append(":nth-child(").append(i + 1).append(')');
81             }
82             return parentNodePath;
83         }
84         return null;
85     }
86 
87     //Returns the part of the element's View ID resource name after the qualifier
88     // "package_name:id/"  or the last '/', when available. Otherwise, returns the element's
89     // simple class name.
getShortUiElementName(AccessibilityNodeInfo nodeInfo)90     private static @Nullable CharSequence getShortUiElementName(AccessibilityNodeInfo nodeInfo) {
91         String viewIdResourceName = nodeInfo.getViewIdResourceName();
92         if (viewIdResourceName == null) {
93             return getSimpleClassName(nodeInfo);
94         }
95         String idQualifier = ":id/";
96         int idQualifierStartIndex = viewIdResourceName.indexOf(idQualifier);
97         int unqualifiedNameStartIndex =
98                 idQualifierStartIndex == -1 ? 0 : (idQualifierStartIndex + idQualifier.length());
99         return viewIdResourceName.substring(unqualifiedNameStartIndex);
100     }
101 
getSimpleClassName(AccessibilityNodeInfo nodeInfo)102     private static @Nullable CharSequence getSimpleClassName(AccessibilityNodeInfo nodeInfo) {
103         CharSequence name = nodeInfo.getClassName();
104         if (name == null) {
105             return null;
106         }
107         for (int i = name.length() - 1; i > 0; i--) {
108             char ithChar = name.charAt(i);
109             if (ithChar == '.' || ithChar == '$') {
110                 return name.subSequence(i + 1, name.length());
111             }
112         }
113         return name;
114     }
115 
AccessibilityNodePathBuilder()116     private AccessibilityNodePathBuilder() {
117     }
118 }
119