com.android.talkback.controller.CursorControllerAppTest.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.controller.CursorControllerAppTest.java

Source

/*
 * 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.talkback.controller;

import static org.junit.Assert.assertNotEquals;

import com.google.android.marvin.talkback.TalkBackService;

import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.webkit.WebView;
import android.widget.HorizontalScrollView;
import android.widget.ListView;
import android.widget.TextView;

import com.android.talkback.CursorGranularity;
import com.android.talkback.InputModeManager;
import com.android.talkback.R;
import com.android.talkback.Utterance;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.traversal.TraversalStrategy;
import com.googlecode.eyesfree.testing.CharSequenceFilter;
import com.googlecode.eyesfree.testing.TalkBackInstrumentationTestCase;
import com.googlecode.eyesfree.testing.UtteranceFilter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class CursorControllerAppTest extends TalkBackInstrumentationTestCase {

    private TalkBackService mTalkBack;
    private CursorController mCursorController;
    private List<AccessibilityNodeInfoCompat> mObtainedNodes;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        mTalkBack = getService();
        mCursorController = mTalkBack.getCursorController();
        mObtainedNodes = new ArrayList<>();

        // We don't want to include the action bar in our traversals.
        final Activity activity = getActivity();
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                activity.getActionBar().hide();
            }
        });
        getInstrumentation().waitForIdleSync();
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        AccessibilityNodeInfoUtils.recycleNodes(mObtainedNodes);
    }

    @MediumTest
    public void testGetSetCursor() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square);
        mCursorController.setCursor(squareButton);
        waitForAccessibilityIdleSync();
        assertEquals(squareButton, mCursorController.getCursor());
    }

    @MediumTest
    public void testGetCursorOrInputCursor() {
        setContentView(R.layout.text_activity);

        AccessibilityNodeInfoCompat usernameEditText = getNodeForId(R.id.username);
        mCursorController.setCursor(usernameEditText);
        waitForAccessibilityIdleSync();

        // Click to open the keyboard.
        mCursorController.clickCurrent();
        waitForAccessibilityIdleSync();

        // Remove accessibility focus from the text field. Input focus should stay, however.
        mCursorController.clearCursor();
        waitForAccessibilityIdleSync();

        assertEquals(usernameEditText, mCursorController.getCursorOrInputCursor());
    }

    @MediumTest
    public void testClearCursor() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square);
        mCursorController.setCursor(squareButton);
        waitForAccessibilityIdleSync();

        mCursorController.clearCursor();
        waitForAccessibilityIdleSync();
        assertFalse(squareButton.equals(mCursorController.getCursor()));
    }

    @MediumTest
    public void testRefocus() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square);
        mCursorController.setCursor(squareButton);
        waitForAccessibilityIdleSync();

        mCursorController.refocus();
        waitForAccessibilityIdleSync();
        assertEquals(squareButton, mCursorController.getCursor());
    }

    @MediumTest
    public void testClickCurrent() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        assertFalse(checkBox.isChecked());

        mCursorController.setCursor(checkBox);
        waitForAccessibilityIdleSync();

        mCursorController.clickCurrent();
        waitForAccessibilityIdleSync();

        checkBox.refresh();
        assertTrue(checkBox.isChecked());
    }

    @MediumTest
    public void testMore() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);

        AccessibilityNodeInfoCompat oldTeam = teamsList.getChild(0);
        CollectionItemInfoCompat oldItemInfo = oldTeam.getCollectionItemInfo();
        assertEquals(0, oldItemInfo.getRowIndex());

        mCursorController.setCursor(oldTeam); // Put cursor in the list view so that it will scroll.
        waitForAccessibilityIdleSync();

        mCursorController.more();
        waitForAccessibilityIdleSync();

        // The first item in the list should have scrolled out of view. The first visible item in
        // the list should now be something different.
        AccessibilityNodeInfoCompat newTeam = teamsList.getChild(0);
        CollectionItemInfoCompat newItemInfo = newTeam.getCollectionItemInfo();
        assertNotEquals(0, newItemInfo.getRowIndex());
    }

    @MediumTest
    public void testLess() {
        setContentView(R.layout.cursor_test);

        final ListView teamsListView = (ListView) getViewForId(R.id.teams_list);
        final int lastTeamIndex = teamsListView.getCount() - 1;

        // Scroll the list view down to display the last item before beginning the test.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                teamsListView.setSelection(lastTeamIndex);
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);

        AccessibilityNodeInfoCompat oldTeam = teamsList.getChild(teamsList.getChildCount() - 1);
        CollectionItemInfoCompat oldItemInfo = oldTeam.getCollectionItemInfo();
        assertEquals(lastTeamIndex, oldItemInfo.getRowIndex());

        mCursorController.setCursor(oldTeam); // Put cursor in the list view so that it will scroll.
        waitForAccessibilityIdleSync();

        mCursorController.less();
        waitForAccessibilityIdleSync();

        // The last item in the list should have scrolled out of view. The last visible item
        // should be something different.
        teamsList.refresh(); // We need to update the list view's child count.
        AccessibilityNodeInfoCompat newTeam = teamsList.getChild(teamsList.getChildCount() - 1);
        CollectionItemInfoCompat newItemInfo = newTeam.getCollectionItemInfo();
        assertNotEquals(lastTeamIndex, newItemInfo.getRowIndex());
    }

    @MediumTest
    public void testJumpToTop() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);

        mCursorController.jumpToTop(InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat cursor = mCursorController.getCursor();
        assertEquals(firstLabel, cursor);
    }

    @MediumTest
    public void testJumpToBottom() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        mCursorController.jumpToBottom(InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat cursor = mCursorController.getCursor();
        assertEquals(lastLabel, cursor);
    }

    @MediumTest
    public void testNext_noWrap_noScroll_noUseInputFocus() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square);
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        mCursorController.setCursor(firstLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_first_item
        // 1. button_square
        // 2. check_me
        // 3-N. teams_list - visible items only
        // N. text_last_item
        final int teamsVisibleCount = teamsList.getChildCount();
        final int traversals = 3 + teamsVisibleCount; // 3 non-list items + N visible list items.
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals,
                false, false, false);

        assertEquals(squareButton, nodes.get(0));
        assertEquals(checkBox, nodes.get(1));
        assertHasParent(teamsList, nodes.subList(2, traversals - 1));
        assertEquals(lastLabel, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testNext_noWrap_doScroll_noUseInputFocus() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        mCursorController.setCursor(checkBox);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) check_me
        // 1-9. teams_list - all items
        // 10. text_last_item
        final int traversals = 10;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals,
                false, true, false);

        assertHasParent(teamsList, nodes.subList(0, traversals - 1));
        assertEquals(lastLabel, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testNext_doWrap_noScroll_noUseInputFocus() {
        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);
        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);

        mCursorController.setCursor(lastLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_last_item
        // 1. text_last_item - first next() pauses on same node
        // 2. text_first_item - second next() should wrap around
        final int traversals = 2;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, true,
                false, false);

        assertEquals(lastLabel, nodes.get(0));
        assertEquals(firstLabel, nodes.get(1));
    }

    @MediumTest
    public void testNavigateWithGranularity() {
        setContentView(R.layout.cursor_test);

        // Focus to the beginning of page.
        mCursorController.setCursor(getNodeForId(R.id.text_first_item));
        waitForAccessibilityIdleSync();

        // Navigate forward by word.
        startRecordingUtterances();
        assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */,
                true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("Beginning");

        startRecordingUtterances();
        assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */,
                true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("of");

        startRecordingUtterances();
        assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */,
                true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("page");

        // Returns false if user tries to go out from the current focused item.
        assertFalse(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.WORD, false /* shouldWrap */,
                true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));

        // Navigate backward by word.
        startRecordingUtterances();
        assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.WORD,
                false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("page");

        startRecordingUtterances();
        assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.WORD,
                false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("of");

        // Navigate forward and backward by character.
        startRecordingUtterances();
        assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.CHARACTER,
                false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("o");

        startRecordingUtterances();
        assertTrue(mCursorController.nextWithSpecifiedGranularity(CursorGranularity.CHARACTER,
                false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("f");

        startRecordingUtterances();
        assertTrue(mCursorController.previousWithSpecifiedGranularity(CursorGranularity.CHARACTER,
                false /* shouldWrap */, true /* shouldScroll */, true /* useInputFocusAsPivotIfEmpty */,
                InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertUtterance("f");

        // Try to go to next item and confirm that above operation doesn't affect to this.
        assertTrue(mCursorController.next(true /* shouldWrap */, true /* shouldScroll */,
                true /* useInputFocusAsPivotIfEmpty */, InputModeManager.INPUT_MODE_TOUCH));
        waitForAccessibilityIdleSync();
        assertEquals(getNodeForId(R.id.button_square), mCursorController.getCursor());
    }

    @MediumTest
    public void testNext_noWrap_noScroll_doUseInputFocus() {
        setContentView(R.layout.cursor_test);

        // Assign input focus to the button, but don't touch accessibility focus.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                View squareButton = getViewForId(R.id.button_square);
                squareButton.requestFocus();
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) button_square - has input focus
        // 1. check_me
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        mCursorController.next(false, false, true, InputModeManager.INPUT_MODE_TOUCH);
        assertEquals(checkBox, mCursorController.getCursor());
    }

    @MediumTest
    public void testNext_horizontal() {
        setContentView(R.layout.cursor_horizontal_test);

        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);
        AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller);

        mCursorController.setCursor(firstLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_first_item
        // 1-7. horiz_scroller - all items
        // 8. text_last_item
        final int traversals = 8;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_FORWARD, traversals, true,
                true, true);

        assertHasParent(horizScroller, nodes.subList(0, traversals - 1));
        assertEquals(lastLabel, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testPrevious_doWrap_doScroll_doUseInputFocus() {
        setContentView(R.layout.cursor_test);

        // Scroll teams list to the last item.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                ListView teamsListView = (ListView) getViewForId(R.id.teams_list);
                teamsListView.setSelection(teamsListView.getCount() - 1);
            }
        });
        getInstrumentation().waitForIdleSync();

        // Put input focus on button AFTER the teams list is done updating.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                View squareButton = getViewForId(R.id.button_square);
                squareButton.requestFocus();
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        // This is the expected traversal:
        // (Initial focus) button_square - has input focus
        // 1. text_first_item
        // 2. text_first_item - first previous() pauses on node
        // 3. text_last_item - second previous() wraps around
        // 4-12: teams_list items
        // 13. check_me
        final int traversals = 13;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_BACKWARD, traversals,
                true, true, true);

        assertEquals(firstLabel, nodes.get(0));
        assertEquals(firstLabel, nodes.get(1));
        assertEquals(lastLabel, nodes.get(2));
        assertHasParent(teamsList, nodes.subList(3, traversals - 1));
        assertEquals(checkBox, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testPrevious_horizontal() {
        setContentView(R.layout.cursor_horizontal_test);

        // Scroll to the very right to reach the last items before beginning test.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                HorizontalScrollView scroller = (HorizontalScrollView) getViewForId(R.id.horiz_scroller);
                scroller.fullScroll(View.FOCUS_RIGHT);
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);
        AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller);

        mCursorController.setCursor(lastLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_last_item
        // 1-7. horiz_scroller - all items
        // 8. text_first_item
        final int traversals = 8;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_BACKWARD, traversals,
                true, true, true);

        assertHasParent(horizScroller, nodes.subList(0, traversals - 1));
        assertEquals(firstLabel, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testDown_noWrap_doScroll_noUseInputFocus() {
        if (!checkApiLevelSupportsDirectional()) {
            return;
        }

        setContentView(R.layout.cursor_test);

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat squareButton = getNodeForId(R.id.button_square);
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        mCursorController.setCursor(firstLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_first_item
        // 1. button_square
        // 2. check_me
        // 3-N. teams_list - visible items only
        // N. text_last_item
        final int teamsVisibleCount = teamsList.getChildCount();
        final int traversals = 3 + teamsVisibleCount; // 3 non-list items + N visible list items.
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_DOWN, traversals, false,
                false, false);

        assertEquals(squareButton, nodes.get(0));
        assertEquals(checkBox, nodes.get(1));
        assertHasParent(teamsList, nodes.subList(2, traversals - 1));
        assertEquals(lastLabel, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testDown_horizontal() {
        if (!checkApiLevelSupportsDirectional()) {
            return;
        }

        setContentView(R.layout.cursor_horizontal_test);

        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);
        AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller);

        mCursorController.setCursor(firstLabel);
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) text_first_item
        // 1. horiz_scroller - a single item (doesn't really matter which specifically)
        // 2. text_last_item
        // 3. text_last_item - first down() should pause on node
        // 4. text_first_item - second down() should wrap around
        final int traversals = 4;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_DOWN, traversals, true,
                true, true);

        assertHasParent(horizScroller, nodes.subList(0, 1));
        assertEquals(lastLabel, nodes.get(1));
        assertEquals(lastLabel, nodes.get(2));
        assertEquals(firstLabel, nodes.get(3));
    }

    @MediumTest
    public void testUp_doWrap_doScroll_doUseInputFocus() {
        if (!checkApiLevelSupportsDirectional()) {
            return;
        }

        setContentView(R.layout.cursor_test);

        // Scroll teams list to the last item.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                ListView teamsListView = (ListView) getViewForId(R.id.teams_list);
                teamsListView.setSelection(teamsListView.getCount() - 1);
            }
        });
        getInstrumentation().waitForIdleSync();

        // Put input focus on button AFTER the teams list is done updating.
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                View squareButton = getViewForId(R.id.button_square);
                squareButton.requestFocus();
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat checkBox = getNodeForId(R.id.check_me);
        AccessibilityNodeInfoCompat firstLabel = getNodeForId(R.id.text_first_item);
        AccessibilityNodeInfoCompat lastLabel = getNodeForId(R.id.text_last_item);

        // This is the expected traversal:
        // (Initial focus) button_square - has input focus
        // 1. text_first_item
        // 2. text_first_item - first previous() pauses on node
        // 3. text_last_item - second previous() wraps around
        // 4-12: teams_list items
        // 13. check_me
        final int traversals = 13;
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_UP, traversals, true,
                true, true);

        assertEquals(firstLabel, nodes.get(0));
        assertEquals(firstLabel, nodes.get(1));
        assertEquals(lastLabel, nodes.get(2));
        assertHasParent(teamsList, nodes.subList(3, traversals - 1));
        assertEquals(checkBox, nodes.get(traversals - 1));
    }

    @MediumTest
    public void testRight_horizontal() {
        if (!checkApiLevelSupportsDirectional()) {
            return;
        }

        setContentView(R.layout.cursor_horizontal_test);

        AccessibilityNodeInfoCompat horizScroller = getNodeForId(R.id.horiz_scroller);
        mCursorController.setCursor(horizScroller.getChild(0));
        waitForAccessibilityIdleSync();

        // This is the expected traversal:
        // (Initial focus) horiz_scroller - first item.
        // 1-20. horiz_scroller - we should wrap around twice but keep hitting horiz_scroller
        final int traversals = 20; // Arbitrary big number. Enough to wrap around a few times.
        List<AccessibilityNodeInfoCompat> nodes = navigate(TraversalStrategy.SEARCH_FOCUS_RIGHT, traversals, true,
                true, true);
        assertHasParent(horizScroller, nodes);
        assertNotEquals(nodes.get(0), nodes.get(1)); // We should get different nodes!
    }

    @MediumTest
    public void testSetGranularity() {
        setContentView(R.layout.text_activity);

        AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label);
        mCursorController.setCursor(usernameLabel);
        mCursorController.setGranularity(CursorGranularity.CHARACTER, false);
        waitForAccessibilityIdleSync();

        startRecordingUtterances();

        // It should read [username] as "u", "s", "e", "r", etc.
        // Read up until the seventh letter ("m").
        for (int i = 0; i < 7; ++i) {
            mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH);
            waitForAccessibilityIdleSync();
        }

        // Need to match "m" exactly (we want "m" by itself and not "username" which contains "m").
        final CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern("m", 0);
        UtteranceFilter utteranceFilter = new UtteranceFilter().addTextFilter(textFilter);
        final Utterance utterance = stopRecordingUtterancesAfterMatch(utteranceFilter);
        assertNotNull("Saw matching utterance", utterance);
    }

    @MediumTest
    public void testSetGranularity_thenNavigateAway() {
        setContentView(R.layout.text_activity);

        final TextView usernameView = (TextView) getViewForId(R.id.username);

        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                usernameView.setText("potatoes");
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label);
        mCursorController.setCursor(usernameLabel);
        mCursorController.setGranularity(CursorGranularity.CHARACTER, false);
        waitForAccessibilityIdleSync();

        startRecordingUtterances();

        // First read [username] at character granularity then begin reading [potatoes].
        // It should read [username] as "u", "s", "e", "r", etc.
        // 1-8: "u", "s", "e", "r"...
        // 9. (indication that we've reached the end)
        // 10. "p"
        for (int i = 0; i < 10; ++i) {
            mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH);
            waitForAccessibilityIdleSync();
        }

        // We need to match an utterance with exactly the string "p".
        final CharSequenceFilter textFilter = new CharSequenceFilter().addMatchesPattern("p", 0);
        UtteranceFilter utteranceFilter = new UtteranceFilter().addTextFilter(textFilter);
        final Utterance utterance = stopRecordingUtterancesAfterMatch(utteranceFilter);
        assertNotNull("Saw matching utterance", utterance);
    }

    @MediumTest
    public void testSetSelectionModeActive() {
        setContentView(R.layout.text_activity);

        final TextView usernameView = (TextView) getViewForId(R.id.username);

        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                usernameView.setText("abcdefghijklmnop");
            }
        });
        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat username = getNodeForId(R.id.username);
        mCursorController.setCursor(username);
        mCursorController.setGranularity(CursorGranularity.CHARACTER, false);
        waitForAccessibilityIdleSync();

        // Move cursor between "c" and "d".
        for (int i = 0; i < 3; ++i) {
            mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH);
            waitForAccessibilityIdleSync();
        }

        mCursorController.setSelectionModeActive(username, true);
        waitForAccessibilityIdleSync();

        // Select five characters "defgh".
        for (int i = 0; i < 5; ++i) {
            mCursorController.next(false, false, false, InputModeManager.INPUT_MODE_TOUCH);
            waitForAccessibilityIdleSync();
        }

        assertEquals(3, usernameView.getSelectionStart());
        assertEquals(8, usernameView.getSelectionEnd()); // 8-3 = 5 char selection.
    }

    @MediumTest
    public void testAddGranularityListener() {
        setContentView(R.layout.text_activity);

        GranularityChangeListener listener = new GranularityChangeListener();
        mCursorController.addGranularityListener(listener);

        AccessibilityNodeInfoCompat usernameLabel = getNodeForId(R.id.username_label);
        mCursorController.setCursor(usernameLabel);

        mCursorController.setGranularity(CursorGranularity.CHARACTER, false);
        waitForAccessibilityIdleSync();
        assertEquals(1, listener.count);

        mCursorController.setGranularity(CursorGranularity.WORD, false);
        waitForAccessibilityIdleSync();
        assertEquals(2, listener.count);
    }

    @MediumTest
    public void testAddScrollListener() {
        setContentView(R.layout.cursor_test);

        ScrollListener listener = new ScrollListener();
        mCursorController.addScrollListener(listener);

        AccessibilityNodeInfoCompat teamsList = getNodeForId(R.id.teams_list);
        AccessibilityNodeInfoCompat teamItem = teamsList.getChild(0);

        mCursorController.setCursor(teamItem);
        waitForAccessibilityIdleSync();

        mCursorController.more(); // Causes a downwards scroll.
        waitForAccessibilityIdleSync();
        assertEquals(1, listener.count);
    }

    @MediumTest
    public void testNextPrevious_web() {
        setContentView(R.layout.cursor_web_test);

        WebAccessibilityDelegate delegate1 = new WebAccessibilityDelegate(true /* next */, false /* previous */);
        View webElement1 = getViewForId(R.id.web_element_1);
        webElement1.setAccessibilityDelegate(delegate1);
        AccessibilityNodeInfoCompat webNode1 = getNodeForView(webElement1);

        WebAccessibilityDelegate delegate2 = new WebAccessibilityDelegate(false /* next */, true /* previous */);
        View webElement2 = getViewForId(R.id.web_element_2);
        webElement2.setAccessibilityDelegate(delegate2);
        AccessibilityNodeInfoCompat webNode2 = getNodeForView(webElement2);

        AccessibilityNodeInfoCompat nativeNode = getNodeForId(R.id.native_element);

        // Start at web element 1.
        mCursorController.setCursor(webNode1);
        waitForAccessibilityIdleSync();

        // Navigate to next web element. We verify that CursorController doesn't move cursor.
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        assertTrue(delegate1.didPerformNextHtmlAction());
        assertEquals(webNode1, mCursorController.getCursor());

        // Set a11y focus to web element 2 manually using the CursorController.
        mCursorController.setCursor(webNode2);
        waitForAccessibilityIdleSync();

        // Navigate to previous web element. We verify that CursorController doesn't move cursor.
        mCursorController.previous(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        assertTrue(delegate2.didPerformPreviousHtmlAction());
        assertEquals(webNode2, mCursorController.getCursor());

        // We're still at web element 2. Try to move to the native element.
        // We verify that the CursorController should move the cursor in this case.
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        assertEquals(nativeNode, mCursorController.getCursor());
    }

    @MediumTest
    public void testHitEdgeAnnouncement_web() {
        setContentView(R.layout.hit_edge_web_test);

        final WebView webView = (WebView) getViewForId(R.id.web_view);

        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                loadWebViewFromResource(webView, R.raw.simple_page_webview);
            }
        });

        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat wholeWeb = getNodeForId(R.id.web_view);
        mCursorController.setCursor(wholeWeb);
        waitForAccessibilityIdleSync();

        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        mCursorController.setGranularity(CursorGranularity.WEB_SECTION, true);
        waitForAccessibilityIdleSync();

        //Move cursor to Heading 1
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        //Try to move to the next element.
        //It should notify the user no next heading or landmark.
        startRecordingRawSpeech();
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();
        stopRecordingAndAssertRawSpeech("No next heading or landmark");
    }

    @MediumTest
    public void testNotPastLastHeading_web() {
        setContentView(R.layout.hit_edge_web_test);

        final WebView webView = (WebView) getViewForId(R.id.web_view);

        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                loadWebViewFromResource(webView, R.raw.simple_page_webview);
            }
        });

        getInstrumentation().waitForIdleSync();
        waitForAccessibilityIdleSync();

        AccessibilityNodeInfoCompat wholeWeb = getNodeForId(R.id.web_view);
        mCursorController.setCursor(wholeWeb);
        waitForAccessibilityIdleSync();

        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();

        mCursorController.setGranularity(CursorGranularity.WEB_SECTION, true);
        waitForAccessibilityIdleSync();

        //Move cursor to Heading 1
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();
        AccessibilityNodeInfoCompat heading = mCursorController.getCursor();

        //Try to move to the next element.
        //It should stay on the last Heading.
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();
        assertEquals(heading, mCursorController.getCursor());

        //Try again to move to the next element.
        //It should stay on the last Heading.
        mCursorController.next(false /* wrap */, false /* scroll */, false /* useInput */,
                InputModeManager.INPUT_MODE_TOUCH);
        waitForAccessibilityIdleSync();
        assertEquals(heading, mCursorController.getCursor());
    }

    private void loadWebViewFromResource(WebView webView, int resourceId) {
        InputStream inputStream = getActivity().getResources().openRawResource(resourceId);
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));

        String webContent = "";
        String tmp = "";

        try {
            while (tmp != null) {
                webContent += tmp;
                tmp = br.readLine();
            }
        } catch (IOException e) {
            Log.e(CursorControllerAppTest.this.getName(), "Cannot read from file.");
        }

        webView.loadData(webContent, "text/html", null);
    }

    private List<AccessibilityNodeInfoCompat> navigate(@TraversalStrategy.SearchDirection int direction,
            int numberTraversals, boolean wrap, boolean scroll, boolean useInputFocus) {
        List<AccessibilityNodeInfoCompat> nodesTraversed = new ArrayList<>();
        for (int i = 0; i < numberTraversals; ++i) {
            switch (direction) {
            case TraversalStrategy.SEARCH_FOCUS_FORWARD:
                mCursorController.next(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            case TraversalStrategy.SEARCH_FOCUS_BACKWARD:
                mCursorController.previous(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            case TraversalStrategy.SEARCH_FOCUS_LEFT:
                mCursorController.left(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            case TraversalStrategy.SEARCH_FOCUS_RIGHT:
                mCursorController.right(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            case TraversalStrategy.SEARCH_FOCUS_UP:
                mCursorController.up(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            case TraversalStrategy.SEARCH_FOCUS_DOWN:
                mCursorController.down(wrap, scroll, useInputFocus, InputModeManager.INPUT_MODE_TOUCH);
                break;
            default:
                throw new IllegalArgumentException("direction must be a SearchDirection");
            }

            waitForAccessibilityIdleSync();

            AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(mCursorController.getCursor());
            nodesTraversed.add(node);
            mObtainedNodes.add(node);
        }
        return nodesTraversed;
    }

    private void assertHasParent(AccessibilityNodeInfoCompat parent, List<AccessibilityNodeInfoCompat> items) {
        for (AccessibilityNodeInfoCompat node : items) {
            assertEquals(parent, node.getParent());
        }
    }

    private boolean checkApiLevelSupportsDirectional() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
    }

    private class GranularityChangeListener implements CursorController.GranularityChangeListener {
        public int count = 0;

        @Override
        public void onGranularityChanged(CursorGranularity granularity) {
            count++;
        }
    }

    private class ScrollListener implements CursorController.ScrollListener {
        public int count = 0;

        @Override
        public void onScroll(AccessibilityNodeInfoCompat scrolledNode, int action, boolean auto) {
            count++;
        }
    }

    private class WebAccessibilityDelegate extends View.AccessibilityDelegate {
        private final boolean mHasNext;
        private final boolean mHasPrevious;
        private int mActionsPerformed = 0;

        public WebAccessibilityDelegate(boolean hasNext, boolean hasPrevious) {
            mHasNext = hasNext;
            mHasPrevious = hasPrevious;
        }

        @Override
        public boolean performAccessibilityAction(View host, int action, Bundle args) {
            mActionsPerformed = mActionsPerformed | action;
            if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && mHasNext) {
                return true;
            }
            if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && mHasPrevious) {
                return true;
            }
            return super.performAccessibilityAction(host, action, args);
        }

        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            if (mHasNext) {
                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
            }
            if (mHasPrevious) {
                info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
            }
        }

        public boolean didPerformNextHtmlAction() {
            return (mActionsPerformed & AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT) != 0;
        }

        public boolean didPerformPreviousHtmlAction() {
            return (mActionsPerformed & AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT) != 0;
        }
    }

}