/*
* 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.treebuilding;
import android.content.Context;
import android.content.SharedPreferences;
import com.android.switchaccess.AccessibilityNodeActionNode;
import com.android.switchaccess.ClearFocusNode;
import com.android.switchaccess.ContextMenuItem;
import com.android.switchaccess.ContextMenuNode;
import com.android.switchaccess.KeyComboPreference;
import com.android.switchaccess.OptionScanNode;
import com.android.switchaccess.OptionScanSelectionNode;
import com.android.switchaccess.SwitchAccessNodeCompat;
import com.android.switchaccess.SwitchAccessWindowInfo;
import com.android.talkback.R;
import com.android.utils.SharedPreferencesUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Build an n-ary tree of OptionScanNodes which enables the user to traverse the options using
* n switches. The views in a window are grouped into n-clusters, with each cluster being
* associated with a particular switch */
public class TalkBackOrderNDegreeTreeBuilder extends TreeBuilder {
/**
* These are the IDs, in order, of the key assignment preferences for option scanning
* TODO When we add more option scanning builders, this list needs a proper home
*/
static public final int OPTION_SCAN_SWITCH_CONFIG_IDS[] = {
R.string.pref_key_mapped_to_click_key, R.string.pref_key_mapped_to_next_key,
R.string.pref_key_mapped_to_switch_3_key, R.string.pref_key_mapped_to_switch_4_key,
R.string.pref_key_mapped_to_switch_5_key};
static private final int CONTEXT_MENU_NODE = 0;
static private final int OPTION_SCAN_SELECTION_NODE = 1;
private int mDegree;
public TalkBackOrderNDegreeTreeBuilder(Context context) {
super(context);
updatePrefs(SharedPreferencesUtils.getSharedPreferences(mContext));
}
@Override
public OptionScanNode addViewHierarchyToTree(SwitchAccessNodeCompat node,
OptionScanNode treeToBuildOn) {
/* Not currently used */
return null;
}
@Override
public OptionScanNode addWindowListToTree(List<SwitchAccessWindowInfo> windowList,
OptionScanNode treeToBuildOn) {
treeToBuildOn = (treeToBuildOn == null) ? new ClearFocusNode(): treeToBuildOn;
if (windowList == null || windowList.size() == 0) {
return treeToBuildOn;
}
List<OptionScanNode> treeNodes = new ArrayList<>();
for (SwitchAccessWindowInfo window : windowList) {
SwitchAccessNodeCompat windowRoot = window.getRoot();
if (windowRoot != null) {
treeNodes.addAll(getNodeListFromNodeTree(windowRoot));
windowRoot.recycle();
}
}
return buildTreeFromNodeList(treeNodes, OPTION_SCAN_SELECTION_NODE, treeToBuildOn);
}
@Override
public OptionScanNode buildContextMenu(List<? extends ContextMenuItem> actionList) {
if (actionList.size() == 0) {
return new ClearFocusNode();
}
return buildTreeFromNodeList(new ArrayList<OptionScanNode>(actionList), CONTEXT_MENU_NODE,
new ClearFocusNode());
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
super.onSharedPreferenceChanged(prefs, key);
updatePrefs(prefs);
}
private void updatePrefs(SharedPreferences prefs) {
/*
* Update degree to match the number of option scan switches configured. They must be
* configured consecutively to count.
*/
int numSwitchesConfigured = 0;
while ((OPTION_SCAN_SWITCH_CONFIG_IDS.length > numSwitchesConfigured)
&& KeyComboPreference.getKeyCodesForPreference(
prefs,
mContext.getString(OPTION_SCAN_SWITCH_CONFIG_IDS[numSwitchesConfigured]))
.size() > 0) {
numSwitchesConfigured++;
}
/* We can't build a tree with degree less than 2. */
mDegree = Math.max(numSwitchesConfigured, 2);
}
/**
* Given the root of the tree of SwitchAccessNodeCompat, constructs a list of actions
* associated with each compat node in this tree. If a particular node has more than one
* action, a n-ary tree representing a context menu with all the available actions is added to
* the list instead.
*
* @param root The root of the tree of SwitchAccessNodeCompat
* @return A list of OptionScanNodes, which represent either AccessibilityNodeActionNodes or
* ContextMenuNodes (for nodes with multiple actions).
*/
private List<OptionScanNode> getNodeListFromNodeTree(SwitchAccessNodeCompat root) {
List<OptionScanNode> treeNodes = new ArrayList<>();
List<SwitchAccessNodeCompat> talkBackOrderList = getNodesInTalkBackOrder(root);
for (SwitchAccessNodeCompat node : talkBackOrderList) {
List<AccessibilityNodeActionNode> actionNodes = getCompatActionNodes(node);
node.recycle();
if (actionNodes.size() == 1) {
treeNodes.add(actionNodes.get(0));
} else if (actionNodes.size() > 1) {
treeNodes.add(buildContextMenu(actionNodes));
}
}
return treeNodes;
}
/**
* Builds an n-ary tree from a list of nodes to be included in the tree.
*
* @param nodeList The list of nodes to be included in the tree
* @param treeNodeType The type of nodes in the constructed tree.
* @param lastScanNode The node that can be presented as the last option regardless of the path
* followed in the constructed tree. For example when a context menu tree is constructed,
* the ClearFocusNode should be a leaf node at the end of any possible path followed in
* the constructed tree.
* @return An n-ary tree containing all the nodes included in the list.
*/
private OptionScanNode buildTreeFromNodeList(List<OptionScanNode> nodeList, int treeNodeType,
OptionScanNode lastScanNode) {
if (nodeList.size() == mDegree) {
/* the {@code nodeList} contains degree action nodes, however the {@code lastScanNode}
* would increase the degree of the node to degree + 1. Hence the first degree-1
* options are kept as children of the parent node while a new node is created to hold
* the last option and the {@code lastScanNode}. This node is then kept as the last
* child of the parent node returned */
nodeList.add(lastScanNode);
List<OptionScanNode> children = nodeList.subList(0, nodeList.size() - 2);
OptionScanNode lastChild = createTree(nodeList.subList(nodeList.size() - 2,
nodeList.size()), treeNodeType);
children.add(lastChild);
return createTree(children, treeNodeType);
} else if (nodeList.size() < mDegree) {
/* regardless of the path the user chooses, the last option presented to the user
* will be the contextMenu. The last scan node of a context menu itself is a
* ClearFocusNode */
nodeList.add(lastScanNode);
return createTree(nodeList, treeNodeType);
} else {
List<OptionScanNode> subtrees = new ArrayList<>();
/* The number of elements that each subtree will contain */
int elemNum = nodeList.size() / mDegree;
/* If the number of elements was not divisible by the degree specified, the remaining
* elements, which will be less than the number of subtrees, will be distributed among
* the first k-subtrees. Hence some subtrees will have at most one more element than
* other subtrees. */
int elemRemainder = nodeList.size() % mDegree;
int startIndex = 0, endIndex = 0;
List<OptionScanNode> subtreeNodes;
while (startIndex < nodeList.size()) {
endIndex = (elemRemainder > 0) ? endIndex + elemNum + 1 : endIndex + elemNum;
elemRemainder--;
subtreeNodes = new ArrayList<>(nodeList.subList(startIndex, endIndex));
if ((subtreeNodes.size() == 1) && (endIndex < nodeList.size())) {
// Selecting a single option that isn't last in the list is unambiguous
subtrees.add(subtreeNodes.get(0));
} else {
subtrees.add(buildTreeFromNodeList(subtreeNodes, treeNodeType, lastScanNode));
}
startIndex = endIndex;
}
/* make the subtrees children of the same parent node */
return createTree(subtrees, treeNodeType);
}
}
/**
* Given a list of nodes, unites the nodes under a common parent, by making them children of a
* common OptionScanNode
*
* @param treeNodes The list of nodes to be included as children of a common parent
* @param nodeType The type of the parent node to be created.
* @return A parent node whose children are all the nodes in the {@code treeNodes} list.
*/
private OptionScanNode createTree(List<OptionScanNode> treeNodes, int nodeType) {
if (treeNodes == null || treeNodes.isEmpty()) {
return null;
} else if (treeNodes.size() == 1) {
return treeNodes.get(0);
} else {
List<OptionScanNode> otherChildren = treeNodes.subList(2, treeNodes.size());
if (nodeType == CONTEXT_MENU_NODE) {
return new ContextMenuNode((ContextMenuItem) treeNodes.get(0),
(ContextMenuItem) treeNodes.get(1), otherChildren.toArray(
new ContextMenuItem[otherChildren.size()]));
} else if (nodeType == OPTION_SCAN_SELECTION_NODE) {
return new OptionScanSelectionNode(treeNodes.get(0), treeNodes.get(1),
otherChildren.toArray(new OptionScanNode[otherChildren.size()]));
}
return null;
}
}
}