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