• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 package com.android.car.rotary;
17 
18 import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_INPUT;
19 import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
20 
21 import static com.android.car.rotary.Utils.FOCUS_AREA_CLASS_NAME;
22 import static com.android.car.rotary.Utils.FOCUS_PARKING_VIEW_CLASS_NAME;
23 import static com.android.car.rotary.Utils.GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME;
24 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
25 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
26 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
27 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
28 import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
29 import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
30 
31 import static org.mockito.Mockito.any;
32 import static org.mockito.Mockito.doAnswer;
33 import static org.mockito.Mockito.mock;
34 import static org.mockito.Mockito.when;
35 import static org.mockito.internal.util.MockUtil.isMock;
36 
37 import android.graphics.Rect;
38 import android.os.Bundle;
39 import android.view.accessibility.AccessibilityNodeInfo;
40 import android.view.accessibility.AccessibilityWindowInfo;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.VisibleForTesting;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 
50 /**
51  * A builder which builds a mock {@link AccessibilityNodeInfo}. Unlike real nodes, mock nodes don't
52  * need to be recycled.
53  */
54 class NodeBuilder {
55     @VisibleForTesting
56     static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 100, 100);
57     /**
58      * A list of mock nodes created via NodeBuilder. This list is used for searching for a
59      * node's child nodes.
60      */
61     @NonNull
62     private final List<AccessibilityNodeInfo> mNodeList;
63     /** The window to which this node belongs. */
64     @Nullable
65     private AccessibilityWindowInfo mWindow;
66     /** The window ID to which this node belongs. */
67     private int mWindowId = UNDEFINED_WINDOW_ID;
68     /** The parent of this node. */
69     @Nullable
70     private AccessibilityNodeInfo mParent;
71     /** The package this node comes from. */
72     @Nullable
73     private String mPackageName;
74     /** The class this node comes from. */
75     @Nullable
76     private String mClassName;
77     /** The node bounds in parent coordinates. */
78     @NonNull
79     private Rect mBoundsInParent = new Rect(DEFAULT_BOUNDS);
80     /** The node bounds in screen coordinates. */
81     @NonNull
82     private Rect mBoundsInScreen = new Rect(DEFAULT_BOUNDS);
83     /** Whether this node is focusable. */
84     private boolean mFocusable = true;
85     /** Whether this node is focused. */
86     private boolean mFocused = false;
87     /** Whether this node is visible to the user. */
88     private boolean mVisibleToUser = true;
89     /** Whether this node is enabled. */
90     private boolean mEnabled = true;
91     /** Whether the view represented by this node is still in the view tree. */
92     private boolean mInViewTree = true;
93     /** Whether this node is scrollable. */
94     private boolean mScrollable = false;
95     /** The content description for this node. */
96     @Nullable
97     private String mContentDescription;
98     /** The state description for this node. */
99     @Nullable
100     private String mStateDescription;
101     /** The action list for this node. */
102     @NonNull
103     private List<AccessibilityNodeInfo.AccessibilityAction> mActionList = new ArrayList<>();
104     /** The extras of this node. */
105     @NonNull
106     private Bundle mExtras = new Bundle();
107     /** Whether this node is checkable. */
108     private boolean mCheckable = false;
109 
NodeBuilder(@onNull List<AccessibilityNodeInfo> nodeList)110     NodeBuilder(@NonNull List<AccessibilityNodeInfo> nodeList) {
111         mNodeList = nodeList;
112     }
113 
build()114     AccessibilityNodeInfo build() {
115         // Make a copy of the current NodeBuilder.
116         NodeBuilder builder = cut();
117         AccessibilityNodeInfo node = mock(AccessibilityNodeInfo.class);
118         when(node.getWindow()).thenReturn(builder.mWindow);
119         when(node.getWindowId()).thenReturn(builder.mWindowId);
120         if (builder.mParent == null || isMock(builder.mParent)) {
121             when(node.getParent()).thenReturn(MockNodeCopierProvider.get().copy(builder.mParent));
122         } else {
123             when(node.getParent()).thenReturn(
124                     // In Navigator, nodes will be recycled once they're no longer used. When a real
125                     // node is recycled, it can't be used again, such as performing an action,
126                     // otherwise it will cause the "Cannot perform this action on a not sealed
127                     // instance" exception. If mParent is a real node and the mock getParent()
128                     // always returns the same instance , it might cause the exception when
129                     // getParent() is called multiple on the same mock node.
130                     // To fix it, this method returns a different instance each time it's called.
131                     // Note: if this method is called too many times and triggers the "Exceeded the
132                     // maximum calls", please add more parameters.
133                     MockNodeCopierProvider.get().copy(builder.mParent),
134                     MockNodeCopierProvider.get().copy(builder.mParent),
135                     MockNodeCopierProvider.get().copy(builder.mParent),
136                     MockNodeCopierProvider.get().copy(builder.mParent),
137                     MockNodeCopierProvider.get().copy(builder.mParent),
138                     MockNodeCopierProvider.get().copy(builder.mParent))
139                     .thenThrow(new RuntimeException("Exceeded the maximum calls"));
140         }
141         // There is no need to mock getChildCount() or getChild() if mParent is null or a real node.
142         if (builder.mParent != null && isMock(builder.mParent)) {
143             // Mock AccessibilityNodeInfo#getChildCount().
144             doAnswer(invocation -> {
145                 int childCount = 0;
146                 for (AccessibilityNodeInfo candidate : builder.mNodeList) {
147                     if (builder.mParent.equals(candidate.getParent())) {
148                         childCount++;
149                     }
150                 }
151                 return childCount;
152             }).when(builder.mParent).getChildCount();
153             // Mock AccessibilityNodeInfo#getChild(int).
154             doAnswer(invocation -> {
155                 Object[] args = invocation.getArguments();
156                 int index = (int) args[0];
157                 for (AccessibilityNodeInfo candidate : builder.mNodeList) {
158                     if (builder.mParent.equals(candidate.getParent())) {
159                         if (index == 0) {
160                             return candidate;
161                         } else {
162                             index--;
163                         }
164                     }
165                 }
166                 return null;
167             }).when(builder.mParent).getChild(any(Integer.class));
168         }
169         when(node.getPackageName()).thenReturn(builder.mPackageName);
170         when(node.getClassName()).thenReturn(builder.mClassName);
171         doAnswer(invocation -> {
172             Object[] args = invocation.getArguments();
173             ((Rect) args[0]).set(builder.mBoundsInParent);
174             return null;
175         }).when(node).getBoundsInParent(any(Rect.class));
176         doAnswer(invocation -> {
177             Object[] args = invocation.getArguments();
178             ((Rect) args[0]).set(builder.mBoundsInScreen);
179             return null;
180         }).when(node).getBoundsInScreen(any(Rect.class));
181         when(node.getBoundsInScreen()).thenReturn(builder.mBoundsInScreen);
182         when(node.isFocusable()).thenReturn(builder.mFocusable);
183         when(node.isFocused()).thenReturn(builder.mFocused);
184         doAnswer(invocation -> {
185             if (node.isFocused()) {
186                 return node;
187             }
188             for (int i = 0; i < node.getChildCount(); i++) {
189                 AccessibilityNodeInfo child = node.getChild(i);
190                 AccessibilityNodeInfo inputFocus = child.findFocus(FOCUS_INPUT);
191                 if (inputFocus != null) {
192                     return inputFocus;
193                 }
194             }
195             return null;
196         }).when(node).findFocus(FOCUS_INPUT);
197         when(node.isVisibleToUser()).thenReturn(builder.mVisibleToUser);
198         when(node.isEnabled()).thenReturn(builder.mEnabled);
199         when(node.refresh()).thenReturn(builder.mInViewTree);
200         when(node.isScrollable()).thenReturn(builder.mScrollable);
201         when(node.getContentDescription()).thenReturn(builder.mContentDescription);
202         when(node.getStateDescription()).thenReturn(builder.mStateDescription);
203         when(node.getActionList()).thenReturn(builder.mActionList);
204         when(node.getExtras()).thenReturn(builder.mExtras);
205         when(node.isCheckable()).thenReturn(builder.mCheckable);
206         builder.mNodeList.add(node);
207         return node;
208     }
209 
setWindow(@ullable AccessibilityWindowInfo window)210     NodeBuilder setWindow(@Nullable AccessibilityWindowInfo window) {
211         mWindow = window;
212         return this;
213     }
214 
setWindowId(int windowId)215     NodeBuilder setWindowId(int windowId) {
216         mWindowId = windowId;
217         return this;
218     }
219 
setParent(@ullable AccessibilityNodeInfo parent)220     NodeBuilder setParent(@Nullable AccessibilityNodeInfo parent) {
221         // The parent node could be a mock node or a real node. In the latter case, a copy is
222         // saved.
223         mParent = MockNodeCopierProvider.get().copy(parent);
224         return this;
225     }
226 
setPackageName(@ullable String packageName)227     NodeBuilder setPackageName(@Nullable String packageName) {
228         mPackageName = packageName;
229         return this;
230     }
231 
setClassName(@ullable String className)232     NodeBuilder setClassName(@Nullable String className) {
233         mClassName = className;
234         return this;
235     }
236 
setBoundsInParent(@onNull Rect boundsInParent)237     NodeBuilder setBoundsInParent(@NonNull Rect boundsInParent) {
238         mBoundsInParent = boundsInParent;
239         return this;
240     }
241 
setBoundsInScreen(@onNull Rect boundsInScreen)242     NodeBuilder setBoundsInScreen(@NonNull Rect boundsInScreen) {
243         mBoundsInScreen = boundsInScreen;
244         return this;
245     }
246 
setFocusable(boolean focusable)247     NodeBuilder setFocusable(boolean focusable) {
248         mFocusable = focusable;
249         return this;
250     }
251 
setFocused(boolean focused)252     NodeBuilder setFocused(boolean focused) {
253         mFocused = focused;
254         return this;
255     }
256 
setVisibleToUser(boolean visibleToUser)257     NodeBuilder setVisibleToUser(boolean visibleToUser) {
258         mVisibleToUser = visibleToUser;
259         return this;
260     }
261 
setEnabled(boolean enabled)262     NodeBuilder setEnabled(boolean enabled) {
263         mEnabled = enabled;
264         return this;
265     }
266 
setInViewTree(boolean inViewTree)267     NodeBuilder setInViewTree(boolean inViewTree) {
268         mInViewTree = inViewTree;
269         return this;
270     }
271 
setScrollable(boolean scrollable)272     NodeBuilder setScrollable(boolean scrollable) {
273         mScrollable = scrollable;
274         return this;
275     }
276 
setContentDescription(@ullable String contentDescription)277     NodeBuilder setContentDescription(@Nullable String contentDescription) {
278         mContentDescription = contentDescription;
279         return this;
280     }
281 
setStateDescription(@ullable String stateDescription)282     NodeBuilder setStateDescription(@Nullable String stateDescription) {
283         mStateDescription = stateDescription;
284         return this;
285     }
286 
setActions(AccessibilityNodeInfo.AccessibilityAction... actions)287     NodeBuilder setActions(AccessibilityNodeInfo.AccessibilityAction... actions) {
288         mActionList = Arrays.asList(actions);
289         return this;
290     }
291 
setFocusArea()292     NodeBuilder setFocusArea() {
293         return setClassName(FOCUS_AREA_CLASS_NAME).setFocusable(false);
294     }
295 
setFocusAreaBoundsOffset(int left, int top, int right, int bottom)296     NodeBuilder setFocusAreaBoundsOffset(int left, int top, int right, int bottom) {
297         mExtras.putInt(FOCUS_AREA_LEFT_BOUND_OFFSET, left);
298         mExtras.putInt(FOCUS_AREA_TOP_BOUND_OFFSET, top);
299         mExtras.putInt(FOCUS_AREA_RIGHT_BOUND_OFFSET, right);
300         mExtras.putInt(FOCUS_AREA_BOTTOM_BOUND_OFFSET, bottom);
301         return this;
302     }
303 
setFpv()304     NodeBuilder setFpv() {
305         return setClassName(FOCUS_PARKING_VIEW_CLASS_NAME);
306     }
307 
setGenericFpv()308     NodeBuilder setGenericFpv() {
309         return setClassName(GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME);
310     }
311 
setScrollableContainer()312     NodeBuilder setScrollableContainer() {
313         return setContentDescription(ROTARY_VERTICALLY_SCROLLABLE);
314     }
315 
setRotaryContainer()316     NodeBuilder setRotaryContainer() {
317         return setContentDescription(ROTARY_CONTAINER);
318     }
319 
setCheckable(boolean checkable)320     NodeBuilder setCheckable(boolean checkable) {
321         mCheckable = checkable;
322         return this;
323     }
324 
325     /**
326      * Creates a copy of the current NodeBuilder, and clears the states of the current NodeBuilder
327      * except for {@link #mNodeList}.
328      */
cut()329     private NodeBuilder cut() {
330         // Create a copy.
331         NodeBuilder copy = new NodeBuilder(this.mNodeList);
332         copy.mWindow = mWindow;
333         copy.mWindowId = mWindowId;
334         copy.mParent = mParent;
335         copy.mPackageName = mPackageName;
336         copy.mClassName = mClassName;
337         copy.mBoundsInParent = mBoundsInParent;
338         copy.mBoundsInScreen = mBoundsInScreen;
339         copy.mFocusable = mFocusable;
340         copy.mFocused = mFocused;
341         copy.mVisibleToUser = mVisibleToUser;
342         copy.mEnabled = mEnabled;
343         copy.mInViewTree = mInViewTree;
344         copy.mScrollable = mScrollable;
345         copy.mContentDescription = mContentDescription;
346         copy.mStateDescription = mStateDescription;
347         copy.mActionList = mActionList;
348         copy.mExtras = mExtras;
349         copy.mCheckable = mCheckable;
350         // Clear the states so that it doesn't infect the next NodeBuilder we create.
351         mWindow = null;
352         mWindowId = UNDEFINED_WINDOW_ID;
353         mParent = null;
354         mPackageName = null;
355         mClassName = null;
356         mBoundsInParent = new Rect(DEFAULT_BOUNDS);
357         mBoundsInScreen = new Rect(DEFAULT_BOUNDS);
358         mFocusable = true;
359         mFocused = false;
360         mVisibleToUser = true;
361         mEnabled = true;
362         mInViewTree = true;
363         mScrollable = false;
364         mContentDescription = null;
365         mStateDescription = null;
366         mActionList = new ArrayList<>();
367         mExtras = new Bundle();
368         mCheckable = false;
369         return copy;
370     }
371 }
372