Java tutorial
/* * 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.screenspeak.controller; import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.text.TextUtils; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.widget.DatePicker; import android.widget.NumberPicker; import com.android.screenspeak.CursorGranularity; import com.android.screenspeak.CursorGranularityManager; import com.android.screenspeak.KeyComboManager; import com.android.screenspeak.R; import com.android.screenspeak.SpeechController; import com.android.screenspeak.eventprocessor.EventState; import com.google.android.marvin.screenspeak.ScreenSpeakService; import com.android.utils.AccessibilityEventListener; import com.android.utils.AccessibilityNodeInfoUtils; import com.android.utils.LogUtils; import com.android.utils.NodeFilter; import com.android.utils.PerformActionUtils; import com.android.utils.SharedPreferencesUtils; import com.android.utils.WebInterfaceUtils; import com.android.utils.WindowManager; import com.android.utils.compat.accessibilityservice.AccessibilityServiceCompatUtils; import com.android.utils.traversal.NodeFocusFinder; import com.android.utils.traversal.OrderedTraversalStrategy; import com.android.utils.traversal.TraversalStrategy; import java.util.HashSet; import java.util.Set; /** * Handles screen reader cursor management. */ public class CursorControllerApp implements CursorController, AccessibilityEventListener, KeyComboManager.KeyComboListener { /** Represents navigation to next element. */ private static final int NAVIGATION_DIRECTION_NEXT = 1; private static final String LOGTAG = "CursorControllerApp"; /** Represents navigation to previous element. */ private static final int NAVIGATION_DIRECTION_PREVIOUS = -1; /** The host service. Used to access the root node. */ private final ScreenSpeakService mService; /** Handles traversal using granularity. */ private final CursorGranularityManager mGranularityManager; /** Whether the user hit an edge with the last swipe. */ private boolean mReachedEdge; private boolean mGranularityNavigationReachedEdge; private final Set<GranularityChangeListener> mGranularityListeners = new HashSet<>(); private final Set<ScrollListener> mScrollListeners = new HashSet<>(); private int mSwitchNodeWithGranularityDirection = 0; /** * Creates a new cursor controller using the specified input controller. * * @param service The accessibility service. Used to obtain the current root * node. */ public CursorControllerApp(ScreenSpeakService service) { mService = service; mGranularityManager = new CursorGranularityManager(service); } @Override public void addGranularityListener(GranularityChangeListener listener) { if (listener == null) { throw new IllegalArgumentException(); } mGranularityListeners.add(listener); } @Override public void removeGranularityListener(GranularityChangeListener listener) { if (listener == null) { throw new IllegalArgumentException(); } mGranularityListeners.remove(listener); } @Override public void addScrollListener(ScrollListener listener) { if (listener == null) { throw new IllegalArgumentException(); } mScrollListeners.add(listener); } @Override public void shutdown() { mGranularityManager.shutdown(); } @Override public boolean refocus() { final AccessibilityNodeInfoCompat node = getCursor(); if (node == null) { return false; } clearCursor(node); final boolean result = setCursor(node); node.recycle(); return result; } @Override public boolean next(boolean shouldWrap, boolean shouldScroll, boolean useInputFocusAsPivotIfEmpty) { return navigateWithGranularity(NAVIGATION_DIRECTION_NEXT, shouldWrap, shouldScroll, useInputFocusAsPivotIfEmpty); } @Override public boolean previous(boolean shouldWrap, boolean shouldScroll, boolean useInputFocusAsPivotIfEmpty) { return navigateWithGranularity(NAVIGATION_DIRECTION_PREVIOUS, shouldWrap, shouldScroll, useInputFocusAsPivotIfEmpty); } @Override public boolean jumpToTop() { clearCursor(); mReachedEdge = true; return next(true /*shouldWrap*/, false /*shouldScroll*/, false /*useInputFocusAsPivotIfEmpty*/); } @Override public boolean jumpToBottom() { clearCursor(); mReachedEdge = true; return previous(true /*shouldWrap*/, false /*shouldScroll*/, false /*useInputFocusAsPivotIfEmpty*/); } @Override public boolean more() { return attemptScrollToDirection(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); } @Override public boolean less() { return attemptScrollToDirection(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); } private boolean attemptScrollToDirection(int direction) { AccessibilityNodeInfoCompat cursor = null; AccessibilityNodeInfoCompat rootNode = null; AccessibilityNodeInfoCompat bfsScrollableNode = null; boolean result = false; try { cursor = getCursor(); if (cursor != null) { result = attemptScrollAction(cursor, direction); } if (!result) { rootNode = AccessibilityServiceCompatUtils.getRootInAccessibilityFocusedWindow(mService); bfsScrollableNode = AccessibilityNodeInfoUtils.searchFromBfs(rootNode, AccessibilityNodeInfoUtils.FILTER_SCROLLABLE); if (bfsScrollableNode != null && isLogicalScrollableWidget(bfsScrollableNode)) { result = attemptScrollAction(bfsScrollableNode, direction); } } } finally { AccessibilityNodeInfoUtils.recycleNodes(cursor, rootNode, bfsScrollableNode); } return result; } @Override public boolean clickCurrent() { return performAction(AccessibilityNodeInfoCompat.ACTION_CLICK); } @Override public boolean nextGranularity() { return adjustGranularity(1); } @Override public boolean previousGranularity() { return adjustGranularity(-1); } @Override public boolean setGranularity(CursorGranularity granularity, boolean fromUser) { AccessibilityNodeInfoCompat current = null; try { current = getCursor(); return setGranularity(granularity, current, fromUser); } finally { AccessibilityNodeInfoUtils.recycleNodes(current); } } @Override public boolean setGranularity(CursorGranularity granularity, AccessibilityNodeInfoCompat node, boolean fromUser) { if (node == null) { return false; } if (!mGranularityManager.setGranularityAt(node, granularity)) { return false; } granularityUpdated(granularity, fromUser); return true; } @Override public boolean setCursor(AccessibilityNodeInfoCompat node) { return PerformActionUtils.performAction(node, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); } @Override public void setSelectionModeActive(AccessibilityNodeInfoCompat node, boolean active) { if (active && !mGranularityManager.isLockedTo(node)) { setGranularity(CursorGranularity.CHARACTER, false /* fromUser */); } mGranularityManager.setSelectionModeActive(active); } @Override public boolean isSelectionModeActive() { return mGranularityManager.isSelectionModeActive(); } @Override public void clearCursor() { AccessibilityNodeInfoCompat currentNode = getCursor(); if (currentNode == null) { return; } clearCursor(currentNode); currentNode.recycle(); } @Override public void clearCursor(AccessibilityNodeInfoCompat currentNode) { if (currentNode == null) { return; } PerformActionUtils.performAction(currentNode, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } @Override public AccessibilityNodeInfoCompat getCursor() { return getAccessibilityFocusedOrRootNode(); } private AccessibilityNodeInfoCompat getAccessibilityFocusedOrRootNode() { final AccessibilityNodeInfoCompat compatRoot = AccessibilityServiceCompatUtils .getRootInAccessibilityFocusedWindow(mService); if (compatRoot == null) { return null; } AccessibilityNodeInfoCompat focusedNode = getAccessibilityFocusedNode(compatRoot); // TODO(KM): If there's no focused node, we should either mimic following // focus from new window or try to be smart for things like list views. if (focusedNode == null) { return compatRoot; } return focusedNode; } public AccessibilityNodeInfoCompat getAccessibilityFocusedOrInputFocusedEditableNode() { final AccessibilityNodeInfoCompat compatRoot = AccessibilityServiceCompatUtils .getRootInAccessibilityFocusedWindow(mService); if (compatRoot == null) { return null; } AccessibilityNodeInfoCompat focusedNode = getAccessibilityFocusedNode(compatRoot); // TODO(KM): If there's no focused node, we should either mimic following // focus from new window or try to be smart for things like list views. if (focusedNode == null) { AccessibilityNodeInfoCompat inputFocusedNode = getInputFocusedNode(); if (inputFocusedNode != null && inputFocusedNode.isEditable()) { focusedNode = inputFocusedNode; } } return focusedNode; } public AccessibilityNodeInfoCompat getAccessibilityFocusedNode(AccessibilityNodeInfoCompat compatRoot) { if (compatRoot == null) { return null; } AccessibilityNodeInfoCompat focusedNode = compatRoot .findFocus(AccessibilityNodeInfoCompat.FOCUS_ACCESSIBILITY); if (focusedNode == null) { return null; } if (!AccessibilityNodeInfoUtils.isVisible(focusedNode)) { focusedNode.recycle(); return null; } return focusedNode; } private AccessibilityNodeInfoCompat getInputFocusedNode() { AccessibilityNodeInfoCompat activeRoot = AccessibilityServiceCompatUtils.getRootInActiveWindow(mService); if (activeRoot != null) { try { return activeRoot.findFocus(AccessibilityNodeInfoCompat.FOCUS_INPUT); } finally { activeRoot.recycle(); } } return null; } public boolean isLinearNavigationLocked(AccessibilityNodeInfoCompat node) { return mGranularityManager.isLockedTo(node); } @Override public CursorGranularity getGranularityAt(AccessibilityNodeInfoCompat node) { if (mGranularityManager.isLockedTo(node)) { return mGranularityManager.getCurrentGranularity(); } return CursorGranularity.DEFAULT; } /** * Attempts to scroll using the specified action. * * @param action The scroll action to perform. * @return Whether the action was performed. */ private boolean attemptScrollAction(AccessibilityNodeInfoCompat cursor, int action) { if (cursor == null) { return false; } AccessibilityNodeInfoCompat scrollableNode = null; try { scrollableNode = getBestScrollableNode(cursor, action); if (scrollableNode == null) { return false; } final boolean performedAction = PerformActionUtils.performAction(scrollableNode, action); if (performedAction) { final Set<ScrollListener> listeners = new HashSet<>(mScrollListeners); for (ScrollListener listener : listeners) { listener.onScroll(scrollableNode, action); } } return performedAction; } finally { AccessibilityNodeInfoUtils.recycleNodes(scrollableNode); } } private AccessibilityNodeInfoCompat getBestScrollableNode(AccessibilityNodeInfoCompat cursor, final int action) { final AccessibilityNodeInfoCompat predecessor = AccessibilityNodeInfoUtils.getSelfOrMatchingAncestor(cursor, AccessibilityNodeInfoUtils.FILTER_SCROLLABLE.and(new NodeFilter() { @Override public boolean accept(AccessibilityNodeInfoCompat node) { return node != null && AccessibilityNodeInfoUtils.supportsAnyAction(node, action); } })); if (predecessor != null && isLogicalScrollableWidget(predecessor)) { return predecessor; } return null; } // TODO that is hack to temporary not to scroll DatePicker and Number picker while they are // unusable on that case private boolean isLogicalScrollableWidget(AccessibilityNodeInfoCompat node) { if (node == null) { return false; } CharSequence className = node.getClassName(); return !(TextUtils.equals(className, DatePicker.class.getName()) || TextUtils.equals(className, NumberPicker.class.getName())); } /** * Attempts to adjust granularity in the direction indicated. * * @param direction The direction to adjust granularity. One of * {@link CursorGranularityManager#CHANGE_GRANULARITY_HIGHER} or * {@link CursorGranularityManager#CHANGE_GRANULARITY_LOWER} * @return true on success, false if no nodes in the current hierarchy * support a granularity other than the default. */ private boolean adjustGranularity(int direction) { AccessibilityNodeInfoCompat currentNode = null; try { currentNode = getCursor(); if (currentNode == null) { return false; } final boolean wasAdjusted = mGranularityManager.adjustGranularityAt(currentNode, direction); if (wasAdjusted) { granularityUpdated(mGranularityManager.getCurrentGranularity(), true); } return wasAdjusted; } finally { AccessibilityNodeInfoUtils.recycleNodes(currentNode); } } /** * Attempts to move in the direction indicated. * <p> * If a navigation granularity other than DEFAULT has been applied, attempts * to move within the current object at the specified granularity. * </p> * <p> * If no granularity has been applied, or if the DEFAULT granularity has * been applied, attempts to move in the specified direction using * {@link android.view.View#focusSearch(int)}. * </p> * * @param direction The direction to move. * @param shouldWrap Whether navigating past the last item on the screen * should wrap around to the first item on the screen. * @param shouldScroll Whether navigating past the last visible item in a * scrollable container should automatically scroll to the next * visible item. * @param useInputFocusAsPivotIfEmpty Whether navigation should start from node that has input * focused editable node if there is no node with * accessibility focus * @return true on success, false on failure. */ private boolean navigateWithGranularity(int direction, boolean shouldWrap, boolean shouldScroll, boolean useInputFocusAsPivotIfEmpty) { final int navigationAction; final int scrollDirection; final int focusSearchDirection; final int edgeDirection; // Map the navigation action to various directions. if (direction == NAVIGATION_DIRECTION_NEXT) { navigationAction = AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY; scrollDirection = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD; focusSearchDirection = TraversalStrategy.SEARCH_FOCUS_FORWARD; edgeDirection = 1; } else { navigationAction = AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY; scrollDirection = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD; focusSearchDirection = TraversalStrategy.SEARCH_FOCUS_BACKWARD; edgeDirection = -1; } AccessibilityNodeInfoCompat current = null; AccessibilityNodeInfoCompat target = null; TraversalStrategy traversalStrategy = null; AccessibilityNodeInfoCompat rootNode = null; boolean processResult = false; try { current = getCurrentCursor(useInputFocusAsPivotIfEmpty); if (current == null) { processResult = false; return processResult; } // If granularity is set to anything other than default, restrict // navigation to the current node. if (mGranularityManager.isLockedTo(current)) { final int result = mGranularityManager.navigate(navigationAction); if (result == CursorGranularityManager.SUCCESS) { mGranularityNavigationReachedEdge = false; processResult = true; return processResult; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 && result == CursorGranularityManager.HIT_EDGE && !current.isEditable()) { if (!mGranularityNavigationReachedEdge) { // skip one swipe when hit edge during granularity navigation mGranularityNavigationReachedEdge = true; processResult = false; return processResult; } else { mSwitchNodeWithGranularityDirection = focusSearchDirection; EventState.getInstance() .addEvent(EventState.EVENT_SKIP_FOCUS_PROCESSING_AFTER_GRANULARITY_MOVE); } } else { processResult = false; return processResult; } } // If the current node has web content, attempt HTML navigation. if (WebInterfaceUtils.supportsWebActions(current) && attemptHtmlNavigation(current, direction)) { return true; } // If the user has disabled automatic scrolling, don't attempt to scroll. // TODO(CB): Remove once auto-scroll is settled. if (shouldScroll) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mService); shouldScroll = SharedPreferencesUtils.getBooleanPref(prefs, mService.getResources(), R.string.pref_auto_scroll_key, R.bool.pref_auto_scroll_default); } rootNode = AccessibilityNodeInfoUtils.getRoot(current); traversalStrategy = new OrderedTraversalStrategy(rootNode); // If the current item is at the edge of a scrollable view, try to // automatically scroll the view in the direction of navigation. if (shouldScroll && AccessibilityNodeInfoUtils.isAutoScrollEdgeListItem(current, edgeDirection, traversalStrategy) && attemptScrollAction(current, scrollDirection)) { processResult = true; return processResult; } // Otherwise, move focus to next or previous focusable node. target = navigateFrom(current, focusSearchDirection, traversalStrategy); if ((target != null)) { //TODO change to Build.VERSION_CODE_CONSTANT (b/23384092) if (Build.VERSION.SDK_INT >= 23 && shouldScroll && AccessibilityNodeInfoUtils .isAutoScrollEdgeListItem(target, edgeDirection, traversalStrategy)) { //comment for lint //noinspection Annotator PerformActionUtils.performAction(target, AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_ON_SCREEN.getId()); } if (setCursor(target)) { mReachedEdge = false; processResult = true; return processResult; } } // skip one swipe if in the border of window and no other application window to // move focus to if (!mReachedEdge && needPauseInTraversalAfterCurrentWindow(focusSearchDirection)) { mReachedEdge = true; processResult = false; return processResult; } // move focus from application to next application window if (navigateToNextApplicationWindow(focusSearchDirection)) { mReachedEdge = false; processResult = true; return processResult; } if (mReachedEdge && shouldWrap) { mReachedEdge = false; processResult = navigateWrapAround(rootNode, focusSearchDirection, traversalStrategy); return processResult; } processResult = false; return processResult; } finally { AccessibilityNodeInfoUtils.recycleNodes(current, target, rootNode); if (traversalStrategy != null) { traversalStrategy.recycle(); } if (!processResult) { mSwitchNodeWithGranularityDirection = 0; EventState.getInstance().clearEvent(EventState.EVENT_SKIP_FOCUS_PROCESSING_AFTER_GRANULARITY_MOVE); } } } private AccessibilityNodeInfoCompat getCurrentCursor(boolean useInputFocusAsPivotIfEmpty) { AccessibilityNodeInfoCompat cursor = null; if (useInputFocusAsPivotIfEmpty) { cursor = getAccessibilityFocusedOrInputFocusedEditableNode(); } if (cursor == null) { cursor = getAccessibilityFocusedOrRootNode(); } return cursor; } @SuppressLint("InlinedApi") private boolean needPauseInTraversalAfterCurrentWindow(int direction) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { // always pause before loop in one-window conditions return true; } WindowManager windowsManager = new WindowManager(); windowsManager.setWindows(mService.getWindows()); if (!windowsManager.isApplicationWindowFocused()) { // need pause before looping traversal in non-application window return true; } if (direction == NodeFocusFinder.SEARCH_FORWARD) { return windowsManager.isLastWindow(windowsManager.getCurrentWindow(), AccessibilityWindowInfo.TYPE_APPLICATION); } else { return windowsManager.isFirstWindow(windowsManager.getCurrentWindow(), AccessibilityWindowInfo.TYPE_APPLICATION); } } private boolean navigateToNextApplicationWindow(int direction) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { WindowManager windowsManager = new WindowManager(); windowsManager.setWindows(mService.getWindows()); if (!windowsManager.isApplicationWindowFocused()) { return false; } AccessibilityWindowInfo currentWindow = windowsManager.getCurrentWindow(); if (currentWindow == null) { return false; } AccessibilityWindowInfo targetWindow = null; AccessibilityWindowInfo pivotWindow = currentWindow; while (!currentWindow.equals(targetWindow)) { switch (direction) { case NodeFocusFinder.SEARCH_FORWARD: targetWindow = windowsManager.getNextWindow(pivotWindow); break; case NodeFocusFinder.SEARCH_BACKWARD: targetWindow = windowsManager.getPreviousWindow(pivotWindow); break; } pivotWindow = targetWindow; if (targetWindow == null) { return false; } if (targetWindow.getType() != AccessibilityWindowInfo.TYPE_APPLICATION) { continue; } AccessibilityNodeInfo windowRoot = targetWindow.getRoot(); if (windowRoot == null) { continue; } AccessibilityNodeInfoCompat compatRoot = new AccessibilityNodeInfoCompat(windowRoot); TraversalStrategy traversalStrategy = new OrderedTraversalStrategy(compatRoot); if (navigateWrapAround(compatRoot, direction, traversalStrategy)) { return true; } } } return false; } private boolean navigateWrapAround(AccessibilityNodeInfoCompat root, int direction, TraversalStrategy traversalStrategy) { if (root == null) { return false; } AccessibilityNodeInfoCompat tempNode = null; AccessibilityNodeInfoCompat wrapNode = null; try { switch (direction) { case OrderedTraversalStrategy.SEARCH_FOCUS_FORWARD: tempNode = traversalStrategy.focusFirst(root); wrapNode = navigateSelfOrFrom(tempNode, direction, traversalStrategy); break; case OrderedTraversalStrategy.SEARCH_FOCUS_BACKWARD: tempNode = traversalStrategy.focusLast(root); wrapNode = navigateSelfOrFrom(tempNode, direction, traversalStrategy); break; } if (wrapNode == null) { if (LogUtils.LOG_LEVEL <= Log.ERROR) { Log.e(LOGTAG, "Failed to wrap navigation"); } return false; } return setCursor(wrapNode); } finally { AccessibilityNodeInfoUtils.recycleNodes(tempNode, wrapNode); } } private AccessibilityNodeInfoCompat navigateSelfOrFrom(AccessibilityNodeInfoCompat node, int direction, TraversalStrategy traversalStrategy) { if (node == null) { return null; } if (AccessibilityNodeInfoUtils.shouldFocusNode(node, traversalStrategy.getSpeakingNodesCache())) { return AccessibilityNodeInfoCompat.obtain(node); } return navigateFrom(node, direction, traversalStrategy); } private AccessibilityNodeInfoCompat navigateFrom(AccessibilityNodeInfoCompat node, int direction, final TraversalStrategy traversalStrategy) { if (node == null) { return null; } NodeFilter filter = new NodeFilter() { @Override public boolean accept(AccessibilityNodeInfoCompat node) { return node != null && AccessibilityNodeInfoUtils.shouldFocusNode(node, traversalStrategy.getSpeakingNodesCache()); } }; return AccessibilityNodeInfoUtils.searchFocus(traversalStrategy, node, direction, filter); } private void granularityUpdated(CursorGranularity granularity, boolean fromUser) { final Set<GranularityChangeListener> localListeners = new HashSet<>(mGranularityListeners); for (GranularityChangeListener listener : localListeners) { listener.onGranularityChanged(granularity); } if (fromUser) { mService.getSpeechController().speak(mService.getString(granularity.resourceId), SpeechController.QUEUE_MODE_INTERRUPT, 0, null); } } /** * Performs the specified action on the current cursor. * * @param action The action to perform on the current cursor. * @return {@code true} if successful. */ private boolean performAction(int action) { AccessibilityNodeInfoCompat current = null; try { current = getCursor(); return current != null && PerformActionUtils.performAction(current, action); } finally { AccessibilityNodeInfoUtils.recycleNodes(current); } } @Override public boolean onComboPerformed(int id) { switch (id) { case KeyComboManager.ACTION_NAVIGATE_NEXT: next(true, true, true); return true; case KeyComboManager.ACTION_NAVIGATE_PREVIOUS: previous(true, true, true); return true; case KeyComboManager.ACTION_NAVIGATE_FIRST: jumpToTop(); return true; case KeyComboManager.ACTION_NAVIGATE_LAST: jumpToBottom(); return true; case KeyComboManager.ACTION_PERFORM_CLICK: clickCurrent(); return true; } return false; } @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { final AccessibilityNodeInfo node = event.getSource(); if (node == null) { if (LogUtils.LOG_LEVEL <= Log.WARN) { Log.w(LOGTAG, "TYPE_VIEW_ACCESSIBILITY_FOCUSED event without a source."); } return; } // When a new view gets focus, clear the state of the granularity // manager if this event came from a different node than the locked // node but from the same window. final AccessibilityNodeInfoCompat nodeCompat = new AccessibilityNodeInfoCompat(node); mGranularityManager.onNodeFocused(nodeCompat); if (mSwitchNodeWithGranularityDirection == TraversalStrategy.SEARCH_FOCUS_FORWARD) { mGranularityManager.navigate(AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); } else if (mSwitchNodeWithGranularityDirection == TraversalStrategy.SEARCH_FOCUS_BACKWARD) { mGranularityManager.startFromLastNode(); mGranularityManager.navigate(AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); } mSwitchNodeWithGranularityDirection = 0; nodeCompat.recycle(); mReachedEdge = false; mGranularityNavigationReachedEdge = false; } } /** * Attempts to navigate the node using HTML navigation. * * @param node to navigate on * @param direction The direction to navigate, one of: * <ul> * <li>{@link #NAVIGATION_DIRECTION_NEXT}</li> * <li>{@link #NAVIGATION_DIRECTION_PREVIOUS}</li> * </ul> * @return {@code true} if navigation succeeded. */ private static boolean attemptHtmlNavigation(AccessibilityNodeInfoCompat node, int direction) { final int action = (direction == NAVIGATION_DIRECTION_NEXT) ? AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT : AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT; return PerformActionUtils.performAction(node, action); } }