/*
* Copyright (C) 2015 Google Inc.
*
* 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.switchaccess.builders;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.preference.PreferenceManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import com.android.switchaccess.*;
import com.android.switchaccess.test.ShadowAccessibilityNodeInfo;
import com.android.switchaccess.test.ShadowAccessibilityService;
import com.android.switchaccess.test.ShadowAccessibilityWindowInfo;
import com.android.switchaccess.treebuilding.TalkBackOrderNDegreeTreeBuilder;
import com.android.talkback.BuildConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.internal.ShadowExtractor;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
/**
* Robolectric tests for NDegreeTreeBuilder
*/
@Config(
constants = BuildConfig.class,
sdk = 21,
shadows = {
ShadowAccessibilityNodeInfo.class,
ShadowAccessibilityNodeInfo.ShadowAccessibilityAction.class,
ShadowAccessibilityService.class,
ShadowAccessibilityWindowInfo.class})
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)
public class TalkBackOrderNDegreeTreeBuilderTest {
private final List<AccessibilityWindowInfo> mWindows = new ArrayList<>();
private final Context mContext = RuntimeEnvironment.application.getApplicationContext();
private static final Rect WINDOW_0_BOUNDS = new Rect(10, 10, 110, 110);
private static final Rect N0_BOUNDS = new Rect(10, 10, 30, 30);
private static final Rect N00_BOUNDS = new Rect(10, 40, 30, 70);
private static final Rect N01_BOUNDS = new Rect(40, 40, 70, 70);
private static final Rect N000_BOUNDS = new Rect(80, 40, 110, 70);
private static final Rect N001_BOUNDS = new Rect(40, 120, 70, 150);
private static final Rect N1_BOUNDS = new Rect(10, 80, 30, 110);
private static final Rect N10_BOUNDS = new Rect(10, 120, 30, 150);
private static final Rect N11_BOUNDS = new Rect(80, 120, 110, 150);
private AccessibilityNodeInfo mWindowRoot0, mN0, mN1, mN10, mN11, mN00, mN01, mN000, mN001;
private List<SwitchAccessWindowInfo> mExtendedWindows = new ArrayList<>();
private ShadowAccessibilityWindowInfo mShadowWindow0;
/*
* We build a tree of degree N from AccessibilityNodeInfos:
*
* root0
* / \
* n0 n1
* / \ / \
* n00 n01 n10 n11
* / \
* n000 n001
*
* all of which, other than the root, are clickable.
*/
@Before
public void setUp() {
/* For some reason this value becomes 22 when I allow the manifest to load */
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21);
ShadowAccessibilityNodeInfo.resetObtainedInstances();
/* Build accessibility node tree */
mWindows.add(AccessibilityWindowInfo.obtain());
mWindowRoot0 = AccessibilityNodeInfo.obtain();
mWindowRoot0.setClickable(false);
mWindowRoot0.setFocusable(false);
mWindowRoot0.setContentDescription("mWindowRoot0");
mN0 = AccessibilityNodeInfo.obtain();
mN0.setVisibleToUser(true);
mN0.setClickable(true);
mN0.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN0.setContentDescription("mN0");
mN0.setBoundsInScreen(N0_BOUNDS);
mN00 = AccessibilityNodeInfo.obtain();
mN00.setVisibleToUser(true);
mN00.setClickable(true);
mN00.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN00.setContentDescription("mN00");
mN00.setBoundsInScreen(N00_BOUNDS);
mN000 = AccessibilityNodeInfo.obtain();
mN000.setVisibleToUser(true);
mN000.setClickable(true);
mN000.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN000.setContentDescription("mN000");
mN000.setBoundsInScreen(N000_BOUNDS);
mN001 = AccessibilityNodeInfo.obtain();
mN001.setVisibleToUser(true);
mN001.setClickable(true);
mN001.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN001.setContentDescription("mN001");
mN001.setBoundsInScreen(N001_BOUNDS);
mN01 = AccessibilityNodeInfo.obtain();
mN01.setVisibleToUser(true);
mN01.setClickable(true);
mN01.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN01.setContentDescription("mN01");
mN01.setBoundsInScreen(N01_BOUNDS);
mN1 = AccessibilityNodeInfo.obtain();
mN1.setVisibleToUser(true);
mN1.setClickable(true);
mN1.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN1.setContentDescription("mN1");
mN1.setBoundsInScreen(N1_BOUNDS);
mN10 = AccessibilityNodeInfo.obtain();
mN10.setClickable(true);
mN10.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN10.setVisibleToUser(true);
mN10.setContentDescription("mN10");
mN10.setBoundsInScreen(N10_BOUNDS);
mN11 = AccessibilityNodeInfo.obtain();
mN11.setClickable(true);
mN11.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
mN11.setVisibleToUser(true);
mN11.setContentDescription("mN11");
mN11.setBoundsInScreen(N11_BOUNDS);
ShadowAccessibilityNodeInfo shadowRoot =
(ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mWindowRoot0);
shadowRoot.addChild(mN0);
shadowRoot.addChild(mN1);
final ShadowAccessibilityNodeInfo shadowN0 =
(ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mN0);
shadowN0.addChild(mN00);
shadowN0.addChild(mN01);
final ShadowAccessibilityNodeInfo shadowN00 =
(ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mN00);
shadowN00.addChild(mN000);
shadowN00.addChild(mN001);
final ShadowAccessibilityNodeInfo shadowN1 =
(ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mN1);
shadowN1.addChild(mN10);
shadowN1.addChild(mN11);
mShadowWindow0 = (ShadowAccessibilityWindowInfo) ShadowExtractor.extract(mWindows.get(0));
mShadowWindow0.setBoundsInScreen(WINDOW_0_BOUNDS);
mShadowWindow0.setRoot(mWindowRoot0);
mExtendedWindows = SwitchAccessWindowInfo.convertZOrderWindowList(mWindows);
}
@After
public void tearDown() {
mWindowRoot0.recycle();
mN0.recycle();
mN00.recycle();
mN000.recycle();
mN001.recycle();
mN01.recycle();
mN1.recycle();
mN10.recycle();
mN11.recycle();
assertFalse(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(true));
}
@Test
public void buildTreeWithHigherDegreeThanNodesAvailable_hasExpectedStructure() {
configureNumOptionScanningSwitches(5);
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
/* Change the root to a node that will have four clickable children */
mShadowWindow0.setRoot(mN0);
OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) treeBuilder
.addWindowListToTree(mExtendedWindows, new ClearFocusNode());
/* four clickable children + context menu node */
assertEquals(5, treeRoot.getChildCount());
treeRoot.recycle();
}
@Test
public void buildTreeWithZeroLengthWindowList_hasContextMenu() {
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
OptionScanNode treeRoot = treeBuilder.addWindowListToTree(
new ArrayList<SwitchAccessWindowInfo>(), new ClearFocusNode());
assertTrue(treeRoot instanceof ClearFocusNode);
treeRoot.recycle();
}
@Test
public void buildTreeWithNullWindowListNullContextMenu_hasOnlyClearFocusNode() {
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
OptionScanNode treeRoot = treeBuilder.addWindowListToTree(null, null);
assertTrue(treeRoot instanceof ClearFocusNode);
treeRoot.recycle();
}
@Test
public void buildDegreeThreeFullTree_hasExpectedStructure() {
/* with 8 total clickable nodes and a tree of degree 3, the nodes are not evenly
* distributed among the three branches */
configureNumOptionScanningSwitches(3);
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
CharSequence globalActionLabel0 = "global action label 0";
CharSequence globalActionLabel1 = "global action label 1";
ContextMenuItem globalNode0 = new GlobalActionNode(0, null, globalActionLabel0);
ContextMenuItem globalNode1 = new GlobalActionNode(1, null, globalActionLabel1);
OptionScanNode contextMenuTree = treeBuilder
.buildContextMenu(Arrays.asList(globalNode0, globalNode1));
OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) treeBuilder
.addWindowListToTree(mExtendedWindows, contextMenuTree);
assertEquals(3, treeRoot.getChildCount());
OptionScanSelectionNode firstLevelNode0 = (OptionScanSelectionNode) treeRoot.getChild(0);
OptionScanSelectionNode firstLevelNode1 = (OptionScanSelectionNode) treeRoot.getChild(1);
OptionScanSelectionNode firstLevelNode2 = (OptionScanSelectionNode) treeRoot.getChild(2);
/* check the structure of the tree rooted at firstLevelNode0 */
assertEquals(firstLevelNode0.getRectsForNodeHighlight().size(), 3);
assertTrue(firstLevelNode0.getRectsForNodeHighlight().contains(N0_BOUNDS));
assertTrue(firstLevelNode0.getRectsForNodeHighlight().contains(N00_BOUNDS));
assertTrue(firstLevelNode0.getRectsForNodeHighlight().contains(N000_BOUNDS));
assertTrue(firstLevelNode0.getChild(0) instanceof AccessibilityNodeActionNode);
assertTrue(firstLevelNode0.getChild(1) instanceof AccessibilityNodeActionNode);
OptionScanSelectionNode secondLevelNode0 = (OptionScanSelectionNode)firstLevelNode0
.getChild(2);
assertEquals(2, secondLevelNode0.getChildCount());
assertTrue(secondLevelNode0.getChild(1) instanceof ContextMenuNode);
/* check the structure of the tree rooted at firstLevelNode1 */
assertEquals(firstLevelNode1.getRectsForNodeHighlight().size(), 3);
assertTrue(firstLevelNode1.getRectsForNodeHighlight().contains(N001_BOUNDS));
assertTrue(firstLevelNode1.getRectsForNodeHighlight().contains(N01_BOUNDS));
assertTrue(firstLevelNode1.getRectsForNodeHighlight().contains(N1_BOUNDS));
OptionScanSelectionNode secondLevelNode1 = (OptionScanSelectionNode)firstLevelNode1
.getChild(2);
assertEquals(2, secondLevelNode1.getChildCount());
assertTrue(secondLevelNode1.getChild(1) instanceof ContextMenuNode);
/* check the structure of the tree rooted at firstLevelNode2 */
assertEquals(firstLevelNode2.getRectsForNodeHighlight().size(), 2);
assertTrue(firstLevelNode2.getRectsForNodeHighlight().contains(N10_BOUNDS));
assertTrue(firstLevelNode2.getRectsForNodeHighlight().contains(N11_BOUNDS));
assertTrue(firstLevelNode2.getChild(2) instanceof ContextMenuNode);
contextMenuTree.recycle();
treeRoot.recycle();
}
@Test
public void buildDegreeFourFullTree_hasExpectedStructure() {
/* with 8 total clickable nodes and a tree of degree 4, the nodes are evenly
* distributed among the four branches */
configureNumOptionScanningSwitches(4);
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) treeBuilder
.addWindowListToTree(mExtendedWindows, new ClearFocusNode());
assertEquals(4, treeRoot.getChildCount());
OptionScanSelectionNode firstLevelNode0 = (OptionScanSelectionNode) treeRoot.getChild(0);
OptionScanSelectionNode firstLevelNode1 = (OptionScanSelectionNode) treeRoot.getChild(1);
OptionScanSelectionNode firstLevelNode2 = (OptionScanSelectionNode) treeRoot.getChild(2);
OptionScanSelectionNode firstLevelNode3 = (OptionScanSelectionNode) treeRoot.getChild(3);
/* check the structure of the tree rooted at firstLevelNode0 */
assertEquals(firstLevelNode0.getRectsForNodeHighlight().size(), 2);
assertTrue(firstLevelNode0.getRectsForNodeHighlight().contains(N0_BOUNDS));
assertTrue(firstLevelNode0.getRectsForNodeHighlight().contains(N00_BOUNDS));
assertTrue(firstLevelNode0.getChild(2) instanceof ClearFocusNode);
/* check the structure of the tree rooted at firstLevelNode1 */
assertEquals(firstLevelNode1.getRectsForNodeHighlight().size(), 2);
assertTrue(firstLevelNode1.getRectsForNodeHighlight().contains(N000_BOUNDS));
assertTrue(firstLevelNode1.getRectsForNodeHighlight().contains(N001_BOUNDS));
assertTrue(firstLevelNode1.getChild(2) instanceof ClearFocusNode);
/* check the structure of the tree rooted at firstLevelNode2 */
assertEquals(firstLevelNode2.getRectsForNodeHighlight().size(), 2);
assertTrue(firstLevelNode2.getRectsForNodeHighlight().contains(N01_BOUNDS));
assertTrue(firstLevelNode2.getRectsForNodeHighlight().contains(N1_BOUNDS));
assertTrue(firstLevelNode2.getChild(2) instanceof ClearFocusNode);
/* check the structure of the tree rooted at firstLevelNode3 */
assertEquals(firstLevelNode3.getRectsForNodeHighlight().size(), 2);
assertTrue(firstLevelNode3.getRectsForNodeHighlight().contains(N10_BOUNDS));
assertTrue(firstLevelNode3.getRectsForNodeHighlight().contains(N11_BOUNDS));
assertTrue(firstLevelNode3.getChild(2) instanceof ClearFocusNode);
treeRoot.recycle();
}
@Test
public void buildNoActionsContextMenuTree_hasOnlyClearFocusNode() {
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
List<ContextMenuItem> actions = new ArrayList<>();
OptionScanNode contextMenuTree = treeBuilder.buildContextMenu(actions);
assertTrue(contextMenuTree instanceof ClearFocusNode);
contextMenuTree.recycle();
}
@Test
public void buildContextMenuTree_order2_hasExpectedStructure() {
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
ContextMenuItem globalNode0 = new GlobalActionNode(0, null, null);
ContextMenuItem globalNode1 = new GlobalActionNode(1, null, null);
ContextMenuItem globalNode2 = new GlobalActionNode(2, null, null);
ContextMenuNode contextMenuTree = (ContextMenuNode) treeBuilder
.buildContextMenu(Arrays.asList(globalNode0, globalNode1, globalNode2));
/*
* Expected structure:
* /-globalNode0 /-globalNode1
* /-contextMenuNode---contextMenuNode---clearFocus
* rootContextMenuNode---contextMenuNode---globalNode2
* \-clearFocus
*/
assertEquals(2, contextMenuTree.getChildCount());
ContextMenuNode secondLevelNode0 = (ContextMenuNode) contextMenuTree.getChild(0);
assertEquals(2, secondLevelNode0.getChildCount());
assertEquals(globalNode0, secondLevelNode0.getChild(0));
assertEquals(globalNode1, ((ContextMenuNode) secondLevelNode0.getChild(1)).getChild(0));
assertTrue(((ContextMenuNode) secondLevelNode0.getChild(1)).getChild(1)
instanceof ClearFocusNode);
ContextMenuNode secondLevelNode1 = (ContextMenuNode) contextMenuTree.getChild(1);
assertEquals(2, secondLevelNode1.getChildCount());
assertEquals(globalNode2, secondLevelNode1.getChild(0));
assertTrue(secondLevelNode1.getChild(1) instanceof ClearFocusNode);
contextMenuTree.recycle();
}
@Test
public void buildContextMenuTree_order3_hasExpectedStructure() {
configureNumOptionScanningSwitches(3);
TalkBackOrderNDegreeTreeBuilder treeBuilder = new TalkBackOrderNDegreeTreeBuilder(mContext);
ContextMenuItem globalNode0 = new GlobalActionNode(0, null, null);
ContextMenuItem globalNode1 = new GlobalActionNode(1, null, null);
ContextMenuItem globalNode2 = new GlobalActionNode(2, null, null);
ContextMenuItem globalNode3 = new GlobalActionNode(3, null, null);
ContextMenuNode contextMenuTree = (ContextMenuNode) treeBuilder.buildContextMenu(
Arrays.asList(globalNode0, globalNode1, globalNode2, globalNode3));
/*
* Expected structure:
* /-globalNode0
* /-contextMenuNode---globalNode1
* /-globalNode2 \-cleanFocus
* rootContextMenuNode---contextMenuNode---globalNode3
* \-clearFocus
*/
assertEquals(3, contextMenuTree.getChildCount());
ContextMenuNode secondLevelNode0 = (ContextMenuNode) contextMenuTree.getChild(0);
assertEquals(globalNode2, contextMenuTree.getChild(1));
ContextMenuNode secondLevelNode1 = (ContextMenuNode) contextMenuTree.getChild(2);
assertEquals(globalNode0, secondLevelNode0.getChild(0));
assertEquals(globalNode1, secondLevelNode0.getChild(1));
assertTrue(secondLevelNode0.getChild(2) instanceof ClearFocusNode);
assertEquals(globalNode3, secondLevelNode1.getChild(0));
assertTrue(secondLevelNode1.getChild(1) instanceof ClearFocusNode);
contextMenuTree.recycle();
}
/* Configure the specified number of option scanning preferences */
private void configureNumOptionScanningSwitches(int num) {
assertTrue(num > 0);
assertTrue(num <= TalkBackOrderNDegreeTreeBuilder.OPTION_SCAN_SWITCH_CONFIG_IDS.length);
SharedPreferences.Editor prefEditor =
PreferenceManager.getDefaultSharedPreferences(mContext).edit();
for (int i = 0; i < TalkBackOrderNDegreeTreeBuilder.OPTION_SCAN_SWITCH_CONFIG_IDS.length;
i++) {
String prefKey = mContext
.getString(TalkBackOrderNDegreeTreeBuilder.OPTION_SCAN_SWITCH_CONFIG_IDS[i]);
prefEditor.remove(prefKey);
if (i < num) {
Set<String> prefStringSet = new HashSet<>(1);
prefStringSet.add(String.format("%d", i));
prefEditor.putStringSet(prefKey, prefStringSet);
}
}
prefEditor.apply();
}
}