Java tutorial
/* * Copyright (C) 2012 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.googlecode.eyesfree.brailleback; import static com.googlecode.eyesfree.brailleback.BrailleBackService.DOT1; import static com.googlecode.eyesfree.brailleback.BrailleBackService.DOT2; import static com.googlecode.eyesfree.brailleback.BrailleBackService.DOT3; import static com.googlecode.eyesfree.brailleback.BrailleBackService.DOT4; import static com.googlecode.eyesfree.brailleback.BrailleBackService.DOT5; import com.googlecode.eyesfree.braille.display.BrailleInputEvent; import com.googlecode.eyesfree.utils.LogUtils; import android.graphics.Rect; 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 java.util.HashSet; /** * A debugging navigation mode that allows navigating through the * currently active window's node tree. * */ public class TreeDebugNavigationMode implements NavigationMode { private final DisplayManager mDisplayManager; private final FeedbackManager mFeedbackManager; private final BrailleBackService mService; // TODO: Could keep current nodes in inactive Windows in an LRU // cache to give a better user experience when switching // between windows. /** * The node that is currently shown on the display and which * navigation commands start at. */ private AccessibilityNodeInfo mCurrentNode; /** * The node of the accessibility event that was last observed. * This may become the current node under certain circumstances. */ private AccessibilityNodeInfo mPendingNode; public TreeDebugNavigationMode(DisplayManager displayManager, FeedbackManager feedbackManager, BrailleBackService service) { mDisplayManager = displayManager; mFeedbackManager = feedbackManager; mService = service; } @Override public boolean onPanLeftOverflow(DisplayManager.Content content) { return movePrevious(); } @Override public boolean onPanRightOverflow(DisplayManager.Content content) { return moveNext(); } @Override public boolean onMappedInputEvent(BrailleInputEvent event, DisplayManager.Content content) { switch (event.getCommand()) { case BrailleInputEvent.CMD_NAV_LINE_PREVIOUS: return movePreviousSibling(); case BrailleInputEvent.CMD_NAV_LINE_NEXT: return moveNextSibling(); case BrailleInputEvent.CMD_NAV_ITEM_PREVIOUS: return moveParent(); case BrailleInputEvent.CMD_NAV_ITEM_NEXT: return moveFirstChild(); case BrailleInputEvent.CMD_ROUTE: return activateCurrent(); case BrailleInputEvent.CMD_BRAILLE_KEY: if (event.getArgument() == (DOT1 | DOT2)) { // letter b showRect(); return true; } if (event.getArgument() == (DOT1 | DOT4)) { // letter c // Undocumented way of clearing the node // cache. mService.setServiceInfo(mService.getServiceInfo()); return true; } if (event.getArgument() == (DOT1 | DOT2 | DOT3 | DOT5)) { // letter r setPendingNode(mService.getRootInActiveWindow()); if (mPendingNode == null) { return false; } makePendingNodeCurrent(); displayCurrentNode(); return true; } if (event.getArgument() == (DOT1 | DOT2 | DOT3 | DOT4)) { // letter p printNodes(); return true; } break; } return false; } @Override public void onActivate() { if (mPendingNode != null) { makePendingNodeCurrent(); } displayCurrentNode(); } @Override public void onDeactivate() { setCurrentNode(null); } @Override public void onObserveAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo source = event.getSource(); if (source == null) { return; } boolean isNewWindow = false; if (mCurrentNode == null || mCurrentNode.getWindowId() != source.getWindowId()) { isNewWindow = true; } int t = event.getEventType(); boolean isInterestingEventType = false; if (t == AccessibilityEvent.TYPE_VIEW_SELECTED || t == AccessibilityEvent.TYPE_VIEW_FOCUSED || t == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED || t != AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) { isInterestingEventType = true; } if (isNewWindow || isInterestingEventType) { setPendingNode(source); } } @Override public boolean onAccessibilityEvent(AccessibilityEvent event) { if (mPendingNode == null) { return false; } if (mCurrentNode == null || mCurrentNode.getWindowId() != mPendingNode.getWindowId()) { makePendingNodeCurrent(); displayCurrentNode(); return true; } return false; } @Override public void onInvalidateAccessibilityNode(AccessibilityNodeInfoCompat node) { // Nothing to do. } private void setPendingNode(AccessibilityNodeInfo newNode) { if (mPendingNode != null && mPendingNode != newNode) { mPendingNode.recycle(); } mPendingNode = newNode; } private void setCurrentNode(AccessibilityNodeInfo newNode) { if (mCurrentNode != null && mCurrentNode != newNode) { mCurrentNode.recycle(); } mCurrentNode = newNode; } private void makePendingNodeCurrent() { setCurrentNode(mPendingNode); mPendingNode = null; } private void displayCurrentNode() { if (mCurrentNode == null) { mDisplayManager.setContent(new DisplayManager.Content("No Node")); } else { mDisplayManager.setContent(new DisplayManager.Content(formatNode(mCurrentNode))); } } private CharSequence formatNode(AccessibilityNodeInfo node) { StringBuilder sb = new StringBuilder(); sb.append(node.getWindowId()); if (node.getClassName() != null) { appendSimpleName(sb, node.getClassName()); } else { sb.append("??"); } if (!node.isVisibleToUser()) { sb.append(":invisible"); } if (node.getText() != null) { sb.append(":"); sb.append(node.getText()); } if (node.getContentDescription() != null) { sb.append(":"); sb.append(node.getContentDescription()); } int actions = node.getActions(); if (actions != 0) { sb.append(":"); if ((actions & AccessibilityNodeInfo.ACTION_FOCUS) != 0) { sb.append("F"); } if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { sb.append("A"); } if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { sb.append("a"); } if ((actions & AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) != 0) { sb.append("-"); } if ((actions & AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) != 0) { sb.append("+"); } } if (node.isCheckable()) { sb.append(":"); if (node.isChecked()) { sb.append("(X)"); } else { sb.append("( )"); } } if (node.isFocusable()) { sb.append(":focusable"); } if (node.isFocused()) { sb.append(":focused"); } if (node.isSelected()) { sb.append(":selected"); } if (node.isClickable()) { sb.append(":clickable"); } if (node.isLongClickable()) { sb.append(":longClickable"); } if (node.isAccessibilityFocused()) { sb.append(":accessibilityFocused"); } if (!node.isEnabled()) { sb.append(":disabled"); } return sb.toString(); } private void appendSimpleName(StringBuilder sb, CharSequence fullName) { int dotIndex = TextUtils.lastIndexOf(fullName, '.'); if (dotIndex < 0) { dotIndex = 0; } sb.append(fullName, dotIndex, fullName.length()); } private boolean movePreviousSibling() { if (mCurrentNode == null) { return false; } return moveTo(getPreviousSibling(mCurrentNode)); } private AccessibilityNodeInfo getPreviousSibling(AccessibilityNodeInfo from) { AccessibilityNodeInfo ret = null; AccessibilityNodeInfo parent = from.getParent(); if (parent == null) { return null; } AccessibilityNodeInfo prev = null; AccessibilityNodeInfo cur = null; try { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; ++i) { cur = parent.getChild(i); if (cur == null) { return null; } if (cur.equals(from)) { ret = prev; prev = null; return ret; } if (prev != null) { prev.recycle(); } prev = cur; cur = null; } } finally { parent.recycle(); if (prev != null) { prev.recycle(); } if (cur != null) { cur.recycle(); } } return ret; } private boolean moveNextSibling() { if (mCurrentNode == null) { return false; } return moveTo(getNextSibling(mCurrentNode)); } private AccessibilityNodeInfo getNextSibling(AccessibilityNodeInfo from) { AccessibilityNodeInfo parent = from.getParent(); if (parent == null) { return null; } AccessibilityNodeInfo cur = null; try { int childCount = parent.getChildCount(); for (int i = 0; i < childCount - 1; ++i) { cur = parent.getChild(i); if (cur == null) { return null; } if (cur.equals(from)) { return parent.getChild(i + 1); } if (cur != null) { cur.recycle(); cur = null; } } } finally { parent.recycle(); if (cur != null) { cur.recycle(); } } return null; } private boolean moveParent() { if (mCurrentNode == null) { return false; } AccessibilityNodeInfo parent = mCurrentNode.getParent(); return moveTo(parent, FeedbackManager.TYPE_NAVIGATE_OUT_OF_HIERARCHY); } private boolean moveFirstChild() { if (mCurrentNode == null) { return false; } return moveTo(getFirstChild(mCurrentNode), FeedbackManager.TYPE_NAVIGATE_OUT_OF_HIERARCHY); } private AccessibilityNodeInfo getFirstChild(AccessibilityNodeInfo from) { if (from.getChildCount() < 1) { return null; } return from.getChild(0); } private AccessibilityNodeInfo getLastChild(AccessibilityNodeInfo from) { if (from.getChildCount() < 1) { return null; } return from.getChild(from.getChildCount() - 1); } private boolean movePrevious() { if (mCurrentNode == null) { return false; } AccessibilityNodeInfo target = null; int feedbackType = FeedbackManager.TYPE_NONE; AccessibilityNodeInfo prevSibling = getPreviousSibling(mCurrentNode); if (prevSibling != null) { target = getLastDescendantDfs(prevSibling); if (target != null) { feedbackType = FeedbackManager.TYPE_NAVIGATE_INTO_HIERARCHY; prevSibling.recycle(); prevSibling = null; } else { target = prevSibling; } } if (target == null) { target = mCurrentNode.getParent(); if (target != null) { feedbackType = FeedbackManager.TYPE_NAVIGATE_OUT_OF_HIERARCHY; } } return moveTo(target, feedbackType); } private AccessibilityNodeInfo getLastDescendantDfs(AccessibilityNodeInfo from) { AccessibilityNodeInfo lastChild = getLastChild(from); if (lastChild == null) { return null; } while (true) { AccessibilityNodeInfo lastGrandChild = getLastChild(lastChild); if (lastGrandChild != null) { lastChild.recycle(); lastChild = lastGrandChild; } else { break; } } return lastChild; } private boolean moveNext() { if (mCurrentNode == null) { return false; } int feedbackType = FeedbackManager.TYPE_NONE; AccessibilityNodeInfo target = getFirstChild(mCurrentNode); if (target != null) { feedbackType = FeedbackManager.TYPE_NAVIGATE_INTO_HIERARCHY; } else { target = getNextSibling(mCurrentNode); } if (target == null) { AccessibilityNodeInfo ancestor = mCurrentNode.getParent(); int cnt = 1; while (target == null && ancestor != null) { target = getNextSibling(ancestor); if (target == null) { AccessibilityNodeInfo temp = ancestor.getParent(); ancestor.recycle(); ancestor = temp; cnt += 1; } else { ancestor.recycle(); ancestor = null; } } if (target != null) { feedbackType = FeedbackManager.TYPE_NAVIGATE_OUT_OF_HIERARCHY; } } return moveTo(target, feedbackType); } private boolean moveTo(AccessibilityNodeInfo node, int feedbackType) { if (moveTo(node)) { mFeedbackManager.emitFeedback(feedbackType); return true; } return false; } private boolean moveTo(AccessibilityNodeInfo node) { if (node == null) { return false; } setCurrentNode(node); displayCurrentNode(); return true; } private boolean activateCurrent() { if (mCurrentNode == null) { return false; } boolean ret = mCurrentNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS); return ret; } private void showRect() { if (mCurrentNode == null) { return; } Rect rect = new Rect(); mCurrentNode.getBoundsInScreen(rect); mDisplayManager.setContent(new DisplayManager.Content("b: " + rect)); } /** * Outputs the node tree from the current node using dfs preorder * traversal. */ private void printNodes() { HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); if (mCurrentNode == null) { LogUtils.log(this, Log.VERBOSE, "No current node"); return; } LogUtils.log(this, Log.VERBOSE, "Printing nodes"); printNodeTree(AccessibilityNodeInfo.obtain(mCurrentNode), "", seen); for (AccessibilityNodeInfo node : seen) { node.recycle(); } } private void printNodeTree(AccessibilityNodeInfo node, String indent, HashSet<AccessibilityNodeInfo> seen) { if (node == null) { return; } if (!seen.add(node)) { LogUtils.log(this, Log.VERBOSE, "Cycle: %d", node.hashCode()); return; } // Include the hash code as a "poor man's" id, knowing that it // might not always be unique. LogUtils.log(this, Log.VERBOSE, "%s(%d)%s", indent, node.hashCode(), formatNode(node)); int childCount = node.getChildCount(); String childIndent = indent + " "; for (int i = 0; i < childCount; ++i) { AccessibilityNodeInfo child = node.getChild(i); if (child == null) { LogUtils.log(this, Log.VERBOSE, "%sCouldn't get child %d", indent, i); continue; } printNodeTree(child, childIndent, seen); } } }