/* * 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 static org.junit.Assert.*; import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.*; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.preference.PreferenceManager; import android.view.Display; import android.view.WindowManager; 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.treebuilding.LinearScanTreeBuilder; import com.android.switchaccess.treebuilding.MainTreeBuilder; import com.android.switchaccess.treebuilding.RowColumnTreeBuilder; import com.android.switchaccess.treebuilding.TalkBackOrderNDegreeTreeBuilder; import com.android.talkback.BuildConfig; import com.android.talkback.R; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.fakes.RoboSharedPreferences; import org.robolectric.internal.ShadowExtractor; import org.robolectric.shadows.ShadowPreferenceManager; import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Robolectric tests for MainTreeBuilder */ @Config( constants = BuildConfig.class, sdk = 21, shadows = { ShadowAccessibilityNodeInfo.class, ShadowAccessibilityNodeInfo.ShadowAccessibilityAction.class, ShadowAccessibilityService.class}) @TargetApi(Build.VERSION_CODES.LOLLIPOP) @RunWith(RobolectricGradleTestRunner.class) public class MainTreeBuilderTest { private static final CharSequence WINDOW_0_ROOT_CONTENT_DESCRIPTION = "Description 0"; private static final CharSequence WINDOW_1_ROOT_CONTENT_DESCRIPTION = "Description 1"; private static final Rect NO_OVERLAP_WINDOW_0_BOUNDS = new Rect(10, 10, 90, 20); private static final Rect NO_OVERLAP_WINDOW_1_BOUNDS = new Rect(10, 30, 90, 80); private final Context mContext = RuntimeEnvironment.application.getApplicationContext(); private final LinearScanTreeBuilder mMockLinearScanTreeBuilder = mock(LinearScanTreeBuilder.class); private final RowColumnTreeBuilder mMockRowColumnTreeBuilder = mock(RowColumnTreeBuilder.class); private final TalkBackOrderNDegreeTreeBuilder mMockTalkBackOrderNDegreeTreeBuilder = mock(TalkBackOrderNDegreeTreeBuilder.class); private MainTreeBuilder mMainTreeBuilderWithRealBuilders; private MainTreeBuilder mMainTreeBuilderWithMockBuilders; private AccessibilityNodeInfo mWindow0Root, mWindow0Node; private AccessibilityNodeInfo mWindow1Root, mWindow1Node; private List<SwitchAccessWindowInfo> mWindows = new ArrayList<>(); @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 */ mWindow0Root = AccessibilityNodeInfo.obtain(); mWindow0Root.setClickable(false); mWindow0Root.setFocusable(false); mWindow0Node = AccessibilityNodeInfo.obtain(); mWindow0Node.setClickable(true); mWindow0Node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); mWindow0Node.setFocusable(true); mWindow0Node.setVisibleToUser(true); ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mWindow0Root)) .addChild(mWindow0Node); mWindow0Root.setContentDescription(WINDOW_0_ROOT_CONTENT_DESCRIPTION); mWindow0Node.setBoundsInScreen(NO_OVERLAP_WINDOW_0_BOUNDS); mWindow1Root = AccessibilityNodeInfo.obtain(); mWindow1Root.setClickable(false); mWindow1Root.setFocusable(false); mWindow1Node = AccessibilityNodeInfo.obtain(); mWindow1Node.setClickable(true); mWindow1Node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); mWindow1Node.setFocusable(true); mWindow1Node.setVisibleToUser(true); ((ShadowAccessibilityNodeInfo) ShadowExtractor.extract(mWindow1Root)) .addChild(mWindow1Node); mWindow1Root.setContentDescription(WINDOW_1_ROOT_CONTENT_DESCRIPTION); mWindow1Node.setBoundsInScreen(NO_OVERLAP_WINDOW_1_BOUNDS); mMainTreeBuilderWithRealBuilders = new MainTreeBuilder(mContext); mMainTreeBuilderWithMockBuilders = new MainTreeBuilder(mContext, mMockLinearScanTreeBuilder, mMockRowColumnTreeBuilder, mMockTalkBackOrderNDegreeTreeBuilder); mWindows.add(mock(SwitchAccessWindowInfo.class)); mWindows.add(mock(SwitchAccessWindowInfo.class)); when(mWindows.get(0).getRoot()) .thenAnswer(new Answer<SwitchAccessNodeCompat>() { @Override public SwitchAccessNodeCompat answer(InvocationOnMock invocation) { return new SwitchAccessNodeCompat(AccessibilityNodeInfo .obtain(mWindow0Root)); } }); when(mWindows.get(1).getRoot()) .thenAnswer(new Answer<SwitchAccessNodeCompat>() { @Override public SwitchAccessNodeCompat answer(InvocationOnMock invocation) { return new SwitchAccessNodeCompat(AccessibilityNodeInfo .obtain(mWindow1Root)); } }); } @After public void tearDown() { mWindow0Root.recycle(); mWindow1Root.recycle(); mWindow0Node.recycle(); mWindow1Node.recycle(); assertFalse(ShadowAccessibilityNodeInfo.areThereUnrecycledNodes(true)); } @Test public void testSystemWindowFirst_appWindowShouldGetFocusFirst() { configureWindowsWithNoOverlap(); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_SYSTEM); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); AccessibilityNodeActionNode firstNode = (AccessibilityNodeActionNode) treeRoot.getChild(0); assertTrue(firstNode.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_1_BOUNDS)); treeRoot.recycle(); } @Test public void testAppWindowFirst_appWindowShouldGetFocusFirst() { configureWindowsWithNoOverlap(); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_SYSTEM); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); AccessibilityNodeActionNode firstNode = (AccessibilityNodeActionNode) treeRoot.getChild(0); assertTrue(firstNode.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void testAppWindowFirst_systemWindowShouldGetFocusSecond() { configureWindowsWithNoOverlap(); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_SYSTEM); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); OptionScanSelectionNode secondWindowSelectionNode = (OptionScanSelectionNode) treeRoot.getChild(1); AccessibilityNodeActionNode secondNode = (AccessibilityNodeActionNode) secondWindowSelectionNode.getChild(0); assertTrue(secondNode.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_1_BOUNDS)); treeRoot.recycle(); } public void testImeWindowFirst_imeWindowShouldGetFocusFirst() { configureWindowsWithNoOverlap(); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_INPUT_METHOD); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); AccessibilityNodeActionNode firstNode = (AccessibilityNodeActionNode) treeRoot.getChild(0); assertTrue(firstNode.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_1_BOUNDS)); treeRoot.recycle(); } public void testImeWindowSecond_imeWindowShouldGetFocusFirst() { configureWindowsWithNoOverlap(); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_INPUT_METHOD); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); AccessibilityNodeActionNode firstNode = (AccessibilityNodeActionNode) treeRoot.getChild(0); assertTrue(firstNode.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_1_BOUNDS)); treeRoot.recycle(); } @Test public void removeSystemButtons_onBottom() { configureWindowsWithNoOverlap(); Point screenSize = getScreenSize(); setupOneAppAndOneSystemWindowWithBounds(new Rect(0, screenSize.y - 100, screenSize.x, 100)); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); // System window should be missing assertFalse(treeRoot.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void removeSystemButtons_onRight() { configureWindowsWithNoOverlap(); Point screenSize = getScreenSize(); setupOneAppAndOneSystemWindowWithBounds(new Rect(screenSize.x - 100, 0, 100, screenSize.y)); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); // System window should be missing assertFalse(treeRoot.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void removeSystemButtons_butKeepNotifications() { configureWindowsWithNoOverlap(); Point screenSize = getScreenSize(); setupOneAppAndOneSystemWindowWithBounds(new Rect(0, 0, screenSize.x, 100)); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); // System window should be present assertTrue(treeRoot.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void removeSystemButtons_butKeepDialogsInMiddleOfScreen() { configureWindowsWithNoOverlap(); Point screenSize = getScreenSize(); setupOneAppAndOneSystemWindowWithBounds( new Rect(10, 10, screenSize.x - 20, screenSize.y - 20)); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); // System window should be present assertTrue(treeRoot.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void removeSystemButtons_butKeepFullScreenOverlay() { configureWindowsWithNoOverlap(); Point screenSize = getScreenSize(); setupOneAppAndOneSystemWindowWithBounds(new Rect(0, 50, screenSize.x, screenSize.y)); OptionScanSelectionNode treeRoot = (OptionScanSelectionNode) mMainTreeBuilderWithRealBuilders.addWindowListToTree(mWindows, null); // System window should be present assertTrue(treeRoot.getRectsForNodeHighlight().contains(NO_OVERLAP_WINDOW_0_BOUNDS)); treeRoot.recycle(); } @Test public void ifPrefIsRowCol_viewsUseRowColumn() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.row_col_scanning_key)); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); mMainTreeBuilderWithMockBuilders .addWindowListToTree(Arrays.asList(mWindows.get(0)), null); verify(mMockRowColumnTreeBuilder, times(1)).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verify(mMockLinearScanTreeBuilder, never()).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verifyNoMoreInteractions(mMockTalkBackOrderNDegreeTreeBuilder); } @Test public void ifPrefIsRowColForImeOnly_viewsUseLinear() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.views_linear_ime_row_col_key)); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); mMainTreeBuilderWithMockBuilders .addWindowListToTree(Arrays.asList(mWindows.get(0)), null); verify(mMockLinearScanTreeBuilder, times(1)).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verify(mMockRowColumnTreeBuilder, never()).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verifyNoMoreInteractions(mMockTalkBackOrderNDegreeTreeBuilder); } @Test public void ifPrefIsRowColForImeOnly_keyboardUsesRowColumn() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.views_linear_ime_row_col_key)); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_INPUT_METHOD); mMainTreeBuilderWithMockBuilders .addWindowListToTree(Arrays.asList(mWindows.get(0)), null); verify(mMockRowColumnTreeBuilder, times(1)).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verify(mMockLinearScanTreeBuilder, never()).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verifyNoMoreInteractions(mMockTalkBackOrderNDegreeTreeBuilder); } @Test public void ifPrefIsOption_usesOptionScanning() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.option_scanning_key)); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_INPUT_METHOD); mMainTreeBuilderWithMockBuilders .addWindowListToTree(Arrays.asList(mWindows.get(0)), null); verify(mMockTalkBackOrderNDegreeTreeBuilder, times(1)).addWindowListToTree( (List<SwitchAccessWindowInfo>) anyObject(), (OptionScanNode) anyObject()); verify(mMockLinearScanTreeBuilder, never()).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); verify(mMockRowColumnTreeBuilder, never()).addViewHierarchyToTree( (SwitchAccessNodeCompat) anyObject(), (OptionScanNode) anyObject()); } @Test public void ifPrefIsRowCol_contextMenuUsesLinear() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.views_linear_ime_row_col_key)); List<ContextMenuItem> actionList = new ArrayList<>(); mMainTreeBuilderWithMockBuilders.buildContextMenu(actionList); verify(mMockLinearScanTreeBuilder, times(1)).buildContextMenu(actionList); verify(mMockTalkBackOrderNDegreeTreeBuilder, never()).buildContextMenu( (List<ContextMenuItem>) anyObject()); verify(mMockRowColumnTreeBuilder, never()).buildContextMenu( (List<ContextMenuItem>) anyObject()); } @Test public void ifPrefIsOption_contextMenuUsesOption() { setStringPreference(mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.option_scanning_key)); List<ContextMenuItem> actionList = new ArrayList<>(); mMainTreeBuilderWithMockBuilders.buildContextMenu(actionList); verify(mMockTalkBackOrderNDegreeTreeBuilder, times(1)).buildContextMenu(actionList); verify(mMockLinearScanTreeBuilder, never()).buildContextMenu( (List<ContextMenuItem>) anyObject()); verify(mMockRowColumnTreeBuilder, never()).buildContextMenu( (List<ContextMenuItem>) anyObject()); } private void setStringPreference(String preferenceKey, String value) { PreferenceManager.getDefaultSharedPreferences(mContext).edit() .putString(preferenceKey, value).commit(); } /* Set up an app window with default bounds behind a system window with specified bounds */ private void setupOneAppAndOneSystemWindowWithBounds(Rect rect) { setBoundsInScreen(mWindows.get(0), rect); when(mWindows.get(0).getType()).thenReturn(AccessibilityWindowInfo.TYPE_SYSTEM); when(mWindows.get(1).getType()).thenReturn(AccessibilityWindowInfo.TYPE_APPLICATION); } private Point getScreenSize() { final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); final Display display = wm.getDefaultDisplay(); final Point screenSize = new Point(); display.getSize(screenSize); return screenSize; } private void configureWindowsWithNoOverlap() { setBoundsInScreen(mWindows.get(0), NO_OVERLAP_WINDOW_0_BOUNDS); setBoundsInScreen(mWindows.get(1), NO_OVERLAP_WINDOW_1_BOUNDS); } private void setBoundsInScreen(SwitchAccessWindowInfo window, Rect bounds) { final Rect finalBounds = new Rect(); finalBounds.set(bounds); doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock invocationOnMock) { Rect bounds = (Rect) invocationOnMock.getArguments()[0]; bounds.set(finalBounds); return null; } }).when(window).getBoundsInScreen((Rect) anyObject()); } }