blob: 4736bc435d59c1293a2e4d81f2b029b916cf355b [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.rotary;
import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
import static com.android.car.rotary.Utils.FOCUS_AREA_CLASS_NAME;
import static com.android.car.rotary.Utils.FOCUS_PARKING_VIEW_CLASS_NAME;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A builder which builds a mock {@link AccessibilityNodeInfo}. Unlike real nodes, mock nodes don't
* need to be recycled.
*/
class NodeBuilder {
private static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 100, 100);
/**
* A list of mock nodes created via NodeBuilder. This list is used for searching for a
* node's child nodes.
*/
@NonNull
private final List<AccessibilityNodeInfo> mNodeList;
/** The window to which this node belongs. */
@Nullable
private AccessibilityWindowInfo mWindow;
/** The window ID to which this node belongs. */
private int mWindowId = UNDEFINED_WINDOW_ID;
/** The parent of this node. */
@Nullable
private AccessibilityNodeInfo mParent;
/** The class this node comes from. */
@Nullable
private String mClassName;
/** The node bounds in parent coordinates. */
@NonNull
private Rect mBoundsInParent = new Rect(DEFAULT_BOUNDS);
/** The node bounds in screen coordinates. */
@NonNull
private Rect mBoundsInScreen = new Rect(DEFAULT_BOUNDS);
/** Whether this node is focusable. */
private boolean mFocusable = true;
/** Whether this node is visible to the user. */
private boolean mVisibleToUser = true;
/** Whether this node is enabled. */
private boolean mEnabled = true;
/** Whether the view represented by this node is still in the view tree. */
private boolean mInViewTree = true;
/** The content description for this node. */
@Nullable
private String mContentDescription;
/** The action list for this node. */
@NonNull
private List<AccessibilityNodeInfo.AccessibilityAction> mActionList = new ArrayList<>();
/** The extras of this node. */
@NonNull
private Bundle mExtras = new Bundle();
NodeBuilder(@NonNull List<AccessibilityNodeInfo> nodeList) {
mNodeList = nodeList;
}
AccessibilityNodeInfo build() {
// Make a copy of the current NodeBuilder.
NodeBuilder builder = cut();
AccessibilityNodeInfo node = mock(AccessibilityNodeInfo.class);
when(node.getWindow()).thenReturn(builder.mWindow);
when(node.getWindowId()).thenReturn(builder.mWindowId);
when(node.getParent()).thenReturn(builder.mParent);
if (builder.mParent != null) {
// Mock AccessibilityNodeInfo#getChildCount().
doAnswer(invocation -> {
int childCount = 0;
for (AccessibilityNodeInfo candidate : builder.mNodeList) {
if (builder.mParent.equals(candidate.getParent())) {
childCount++;
}
}
return childCount;
}).when(builder.mParent).getChildCount();
// Mock AccessibilityNodeInfo#getChild(int).
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
int index = (int) args[0];
for (AccessibilityNodeInfo candidate : builder.mNodeList) {
if (builder.mParent.equals(candidate.getParent())) {
if (index == 0) {
return candidate;
} else {
index--;
}
}
}
return null;
}).when(builder.mParent).getChild(any(Integer.class));
}
when(node.getClassName()).thenReturn(builder.mClassName);
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
((Rect) args[0]).set(builder.mBoundsInParent);
return null;
}).when(node).getBoundsInParent(any(Rect.class));
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
((Rect) args[0]).set(builder.mBoundsInScreen);
return null;
}).when(node).getBoundsInScreen(any(Rect.class));
when(node.isFocusable()).thenReturn(builder.mFocusable);
when(node.isVisibleToUser()).thenReturn(builder.mVisibleToUser);
when(node.isEnabled()).thenReturn(builder.mEnabled);
when(node.refresh()).thenReturn(builder.mInViewTree);
when(node.getContentDescription()).thenReturn(builder.mContentDescription);
when(node.getActionList()).thenReturn(builder.mActionList);
when(node.getExtras()).thenReturn(builder.mExtras);
builder.mNodeList.add(node);
return node;
}
NodeBuilder setWindow(@Nullable AccessibilityWindowInfo window) {
mWindow = window;
return this;
}
NodeBuilder setWindowId(int windowId) {
mWindowId = windowId;
return this;
}
NodeBuilder setParent(@Nullable AccessibilityNodeInfo parent) {
mParent = parent;
return this;
}
NodeBuilder setClassName(@Nullable String className) {
mClassName = className;
return this;
}
NodeBuilder setBoundsInParent(@NonNull Rect boundsInParent) {
mBoundsInParent = boundsInParent;
return this;
}
NodeBuilder setBoundsInScreen(@NonNull Rect boundsInScreen) {
mBoundsInScreen = boundsInScreen;
return this;
}
NodeBuilder setFocusable(boolean focusable) {
mFocusable = focusable;
return this;
}
NodeBuilder setVisibleToUser(boolean visibleToUser) {
mVisibleToUser = visibleToUser;
return this;
}
NodeBuilder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
NodeBuilder setInViewTree(boolean inViewTree) {
mInViewTree = inViewTree;
return this;
}
NodeBuilder setContentDescription(@Nullable String contentDescription) {
mContentDescription = contentDescription;
return this;
}
NodeBuilder setActions(AccessibilityNodeInfo.AccessibilityAction... actions) {
mActionList = Arrays.asList(actions);
return this;
}
NodeBuilder setFocusArea() {
return setClassName(FOCUS_AREA_CLASS_NAME).setFocusable(false);
}
NodeBuilder setFocusAreaBoundsOffset(int left, int top, int right, int bottom) {
mExtras.putInt(FOCUS_AREA_LEFT_BOUND_OFFSET, left);
mExtras.putInt(FOCUS_AREA_TOP_BOUND_OFFSET, top);
mExtras.putInt(FOCUS_AREA_RIGHT_BOUND_OFFSET, right);
mExtras.putInt(FOCUS_AREA_BOTTOM_BOUND_OFFSET, bottom);
return this;
}
NodeBuilder setFpv() {
return setClassName(FOCUS_PARKING_VIEW_CLASS_NAME);
}
NodeBuilder setScrollableContainer() {
return setContentDescription(ROTARY_VERTICALLY_SCROLLABLE);
}
/**
* Creates a copy of the current NodeBuilder, and clears the states of the current NodeBuilder
* except for {@link #mNodeList}.
*/
private NodeBuilder cut() {
// Create a copy.
NodeBuilder copy = new NodeBuilder(this.mNodeList);
copy.mWindow = mWindow;
copy.mWindowId = mWindowId;
copy.mParent = mParent;
copy.mClassName = mClassName;
copy.mBoundsInParent = mBoundsInParent;
copy.mBoundsInScreen = mBoundsInScreen;
copy.mFocusable = mFocusable;
copy.mVisibleToUser = mVisibleToUser;
copy.mEnabled = mEnabled;
copy.mInViewTree = mInViewTree;
copy.mContentDescription = mContentDescription;
copy.mActionList = mActionList;
copy.mExtras = mExtras;
// Clear the states so that it doesn't infect the next NodeBuilder we create.
mWindow = null;
mWindowId = UNDEFINED_WINDOW_ID;
mParent = null;
mClassName = null;
mBoundsInParent = new Rect(DEFAULT_BOUNDS);
mBoundsInScreen = new Rect(DEFAULT_BOUNDS);
mFocusable = true;
mVisibleToUser = true;
mEnabled = true;
mInViewTree = true;
mContentDescription = null;
mActionList = new ArrayList<>();
mExtras = new Bundle();
return copy;
}
}