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