1 /* 2 * Copyright 2020 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.car.rotary; 18 19 import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE; 20 import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE; 21 22 import android.view.accessibility.AccessibilityNodeInfo; 23 import android.view.accessibility.AccessibilityWindowInfo; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 28 import com.android.car.ui.FocusArea; 29 import com.android.car.ui.FocusParkingView; 30 31 import java.util.List; 32 33 /** 34 * Utility methods for {@link AccessibilityNodeInfo} and {@link AccessibilityWindowInfo}. 35 * <p> 36 * Because {@link AccessibilityNodeInfo}s must be recycled, it's important to be consistent about 37 * who is responsible for recycling them. For simplicity, it's best to avoid having multiple objects 38 * refer to the same instance of {@link AccessibilityNodeInfo}. Instead, each object should keep its 39 * own copy which it's responsible for. Methods that return an {@link AccessibilityNodeInfo} 40 * generally pass ownership to the caller. Such methods should never return a reference to one of 41 * their parameters or the caller will recycle it twice. 42 */ 43 class Utils { 44 45 private static final String FOCUS_AREA_CLASS_NAME = FocusArea.class.getName(); 46 private static final String FOCUS_PARKING_VIEW_CLASS_NAME = FocusParkingView.class.getName(); 47 Utils()48 private Utils() { 49 } 50 51 /** Recycles a node. */ recycleNode(@ullable AccessibilityNodeInfo node)52 static void recycleNode(@Nullable AccessibilityNodeInfo node) { 53 if (node != null) { 54 node.recycle(); 55 } 56 } 57 58 /** Recycles a list of nodes. */ recycleNodes(@ullable List<AccessibilityNodeInfo> nodes)59 static void recycleNodes(@Nullable List<AccessibilityNodeInfo> nodes) { 60 if (nodes != null) { 61 for (AccessibilityNodeInfo node : nodes) { 62 recycleNode(node); 63 } 64 } 65 } 66 67 /** 68 * Updates the given {@code node} in case the view represented by it is no longer in the view 69 * tree. If it's still in the view tree, returns the {@code node}. Otherwise recycles the 70 * {@code node} and returns null. 71 */ refreshNode(@ullable AccessibilityNodeInfo node)72 static AccessibilityNodeInfo refreshNode(@Nullable AccessibilityNodeInfo node) { 73 if (node == null) { 74 return null; 75 } 76 boolean succeeded = node.refresh(); 77 if (succeeded) { 78 return node; 79 } 80 L.w("This node is no longer in the view tree: " + node); 81 node.recycle(); 82 return null; 83 } 84 85 /** Returns whether the given {@code node} can be focused by a rotary controller. */ canTakeFocus(@onNull AccessibilityNodeInfo node)86 static boolean canTakeFocus(@NonNull AccessibilityNodeInfo node) { 87 return node.isVisibleToUser() && node.isFocusable() && node.isEnabled() 88 && !isFocusParkingView(node); 89 } 90 91 /** Returns whether the given {@code node} or its descendants can take focus. */ canHaveFocus(@onNull AccessibilityNodeInfo node)92 static boolean canHaveFocus(@NonNull AccessibilityNodeInfo node) { 93 if (canTakeFocus(node)) { 94 return true; 95 } 96 for (int i = 0; i < node.getChildCount(); i++) { 97 AccessibilityNodeInfo childNode = node.getChild(i); 98 if (childNode != null) { 99 boolean result = canHaveFocus(childNode); 100 childNode.recycle(); 101 if (result) { 102 return true; 103 } 104 } 105 } 106 return false; 107 } 108 109 /** 110 * Returns whether the given {@code node} has focus (i.e. the node or one of its descendants is 111 * focused). 112 */ hasFocus(@onNull AccessibilityNodeInfo node)113 static boolean hasFocus(@NonNull AccessibilityNodeInfo node) { 114 if (node.isFocused()) { 115 return true; 116 } 117 for (int i = 0; i < node.getChildCount(); i++) { 118 AccessibilityNodeInfo childNode = node.getChild(i); 119 if (childNode != null) { 120 boolean result = hasFocus(childNode); 121 childNode.recycle(); 122 if (result) { 123 return true; 124 } 125 } 126 } 127 return false; 128 } 129 130 /** Returns whether the given {@code node} represents a {@link FocusParkingView}. */ isFocusParkingView(@onNull AccessibilityNodeInfo node)131 static boolean isFocusParkingView(@NonNull AccessibilityNodeInfo node) { 132 CharSequence className = node.getClassName(); 133 return className != null && FOCUS_PARKING_VIEW_CLASS_NAME.contentEquals(className); 134 } 135 136 /** Returns whether the given {@code node} represents a {@link FocusArea}. */ isFocusArea(@onNull AccessibilityNodeInfo node)137 static boolean isFocusArea(@NonNull AccessibilityNodeInfo node) { 138 CharSequence className = node.getClassName(); 139 return className != null && FOCUS_AREA_CLASS_NAME.contentEquals(className); 140 } 141 142 /** 143 * Returns whether the given node represents a view which can be scrolled using the rotary 144 * controller, as indicated by its content description. 145 */ isScrollableContainer(@onNull AccessibilityNodeInfo node)146 static boolean isScrollableContainer(@NonNull AccessibilityNodeInfo node) { 147 CharSequence contentDescription = node.getContentDescription(); 148 return contentDescription != null 149 && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription) 150 || ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription)); 151 } 152 153 /** 154 * Returns whether the given node represents a view which can be scrolled horizontally using the 155 * rotary controller, as indicated by its content description. 156 */ isHorizontallyScrollableContainer(@onNull AccessibilityNodeInfo node)157 static boolean isHorizontallyScrollableContainer(@NonNull AccessibilityNodeInfo node) { 158 CharSequence contentDescription = node.getContentDescription(); 159 return contentDescription != null 160 && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)); 161 } 162 163 /** Returns whether {@code descendant} is a descendant of {@code ancestor}. */ isDescendant(@onNull AccessibilityNodeInfo ancestor, @NonNull AccessibilityNodeInfo descendant)164 static boolean isDescendant(@NonNull AccessibilityNodeInfo ancestor, 165 @NonNull AccessibilityNodeInfo descendant) { 166 AccessibilityNodeInfo parent = descendant.getParent(); 167 if (parent == null) { 168 return false; 169 } 170 boolean result = parent.equals(ancestor) || isDescendant(ancestor, parent); 171 recycleNode(parent); 172 return result; 173 } 174 175 /** Recycles a window. */ recycleWindow(@ullable AccessibilityWindowInfo window)176 static void recycleWindow(@Nullable AccessibilityWindowInfo window) { 177 if (window != null) { 178 window.recycle(); 179 } 180 } 181 182 /** Recycles a list of windows. */ recycleWindows(@ullable List<AccessibilityWindowInfo> windows)183 static void recycleWindows(@Nullable List<AccessibilityWindowInfo> windows) { 184 if (windows != null) { 185 for (AccessibilityWindowInfo window : windows) { 186 recycleWindow(window); 187 } 188 } 189 } 190 } 191