/*
* 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.support.annotation.NonNull;
import com.android.switchaccess.AccessibilityNodeActionNode;
import com.android.switchaccess.ClearFocusNode;
import com.android.switchaccess.ContextMenuItem;
import com.android.switchaccess.OptionScanActionNode;
import com.android.switchaccess.OptionScanNode;
import com.android.switchaccess.OptionScanSelectionNode;
import com.android.switchaccess.ProbabilityModelReader;
import com.android.switchaccess.SwitchAccessNodeCompat;
import com.android.switchaccess.SwitchAccessWindowInfo;
import com.android.switchaccess.treebuilding.TreeBuilder;
import com.android.switchaccess.treebuilding.TreeBuilderUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
/**
* Builds a Huffman tree based on the probabilities of the views in a window.
* TODO This builder needs to be made available to users. It is currently dead code.
*/
public class HuffmanTreeBuilder extends TreeBuilder {
/* TODO This default probability value is temporary. It's currently set to be the
* lowest value. Not sure if the option that is not part of the model should always be the
* lowest likely option. I feel that it would be nice if each of the nodes knew what its
* probability was and all the OptionScanNodes had a default value. */
private Double DEFAULT_PROBABILITY = 0.0001;
ProbabilityModelReader mProbabilityModelReader;
int mDegree;
public HuffmanTreeBuilder(Context context, int degree,
ProbabilityModelReader probabilityModelReader)
throws IllegalArgumentException {
super(context);
if (degree < 2) {
throw new IllegalArgumentException("The tree degree must be greater than one");
}
mDegree = degree;
mProbabilityModelReader = probabilityModelReader;
}
/**
* Builds a Huffman tree with all the clickable nodes in the tree anchored at
* {@code windowRoot}. The context provides information about actions the user has taken so far
* and allows the probabilities for the views in a window to be adjusted based on that.
*
* @param windowRoot The root of the tree of SwitchAccessNodeCompat
* @param treeToBuildOn A tree of OptionScanNodes that should be included as part of the
* Huffman tree.
* @param context The actions the user has taken so far. In case of an IME, this would be what
* the user has typed so far.
* @return A Huffman tree of OptionScanNodes including the tree {@code treeToBuildOn} and all
* clickable nodes from the {@code windowRoot} tree. If there are no clickable nodes in
* {@code windowRoot and the treeToBuildOn is {@code null}, a {@code ClearFocusNode} is
* returned.
*/
/* TODO It will probably not be possible to capture context using a string only.
* Once we understand how to capture context better we need to change this. */
public OptionScanNode buildTreeFromNodeTree(SwitchAccessNodeCompat windowRoot,
OptionScanNode treeToBuildOn, String context) {
PriorityQueue<HuffmanNode> optionScanNodeProbabilities =
getOptionScanNodeProbabilities(context, windowRoot);
ClearFocusNode clearFocusNode = new ClearFocusNode();
if (treeToBuildOn != null) {
optionScanNodeProbabilities.add(
new HuffmanNode(treeToBuildOn, DEFAULT_PROBABILITY));
} else if (optionScanNodeProbabilities.isEmpty()) {
return clearFocusNode;
}
optionScanNodeProbabilities.add(createParentNode(optionScanNodeProbabilities,
getNodesPerParent(optionScanNodeProbabilities.size()), clearFocusNode));
while(optionScanNodeProbabilities.size() > 1) {
optionScanNodeProbabilities.add(createParentNode(optionScanNodeProbabilities, mDegree,
clearFocusNode));
}
return optionScanNodeProbabilities.peek().getOptionScanNode();
}
/**
* It adds the ClearFocusNode to the list. If the list contains mDegree children, it creates
* a new branch adding the last child and a ClearFocusNode. This way the number of children
* remains the same. If on the other hand, the list contains less than mDegree children, it
* simply adds the ClearFocusNode as another child.
*
* @param branchNodes The nodes to be included as children of a common parent node.
* @param clearFocusNode The ClearFocusNode that is included if the resulting branch would not
* contain a ClearFocusNode
*/
private void addClearFocusNodeToBranch(List<OptionScanNode> branchNodes,
ClearFocusNode clearFocusNode) {
if (branchNodes.size() < mDegree) {
branchNodes.add(clearFocusNode);
} else {
OptionScanNode nodeWithClearFocus = branchNodes.get(branchNodes.size() - 1);
branchNodes.remove(nodeWithClearFocus);
nodeWithClearFocus = new OptionScanSelectionNode(nodeWithClearFocus,
clearFocusNode);
branchNodes.add(nodeWithClearFocus);
}
}
/**
* Given a priority queue of HuffmanNodes and the number of nodes per parent, a new
* parent HuffmanNode is constructed. The probability of the parent HuffmanNode is the sum of
* the probabilities of all its children. If none of the children branches have a
* {@code ClearFocusNode}, one is included when the parent node is created.
*
* @param nodes The total nodes that will be included in the Huffman tree.
* @param nodesPerParent The number of children the parent node will have.
* @param clearFocusNode The clear focus node to be included if none of the children branches
* have a {@code ClearFocusNode} included.
* @return The parent HuffmanNode created.
*/
private HuffmanNode createParentNode(PriorityQueue<HuffmanNode> nodes, int nodesPerParent,
ClearFocusNode clearFocusNode) throws IllegalArgumentException {
if (nodesPerParent < 2 || nodes.size() < nodesPerParent) {
throw new IllegalArgumentException();
}
Double childrenProbability = 0.0;
List <OptionScanNode> children = new ArrayList<>(nodesPerParent);
Boolean clearFocusNodePresence = false;
for (int i = 0; i < nodesPerParent; i++) {
HuffmanNode huffmanNode = nodes.poll();
childrenProbability += huffmanNode.getProbability();
children.add(huffmanNode.getOptionScanNode());
if ((i == nodesPerParent - 1) && huffmanNode.hasClearFocusNode()) {
clearFocusNodePresence = true;
}
}
if (!clearFocusNodePresence) {
addClearFocusNodeToBranch(children, clearFocusNode);
}
List<OptionScanNode> otherChildren = children.subList(2, children.size());
OptionScanNode parent = new OptionScanSelectionNode(children.get(0), children.get(1),
otherChildren.toArray(new OptionScanNode[otherChildren.size()]));
HuffmanNode parentHuffmanNode = new HuffmanNode(parent, childrenProbability);
parentHuffmanNode.setClearFocusNodePresence();
return parentHuffmanNode;
}
/**
* When constructing a Huffman tree of degree greater than 2, not all sets of source nodes
* can properly form an n-ary tree. If the number of source nodes is congruent to 1 modulo
* degree-1, then the set of source nodes will form a proper Huffman tree. For example, if we
* constructed a tree of degree 4, then the set of source nodes to 1 % 3
* (i.e {1, 4, 7, 10 , ...}) would form a proper Huffman tree.
*
* However if this is not the case, to form a proper Huffman tree, the very first time a
* Huffman Node is constructed instead of picking degree nodes, we pick 2 <= degree' <= degree.
* If we let A = totalNodes mod (degree - 1), then degree' is congruent to A mod (degree -1).
* This function simply implements this logic and computes degree'.
*
* @param totalNodes The total nodes that will be included in the Huffman tree.
* @return The number of children that the Huffman node will contain.
*/
private int getNodesPerParent(int totalNodes) {
if (totalNodes <= mDegree) {
return totalNodes;
}
int nodesPerParent = totalNodes % (mDegree - 1);
while (nodesPerParent < 2) {
nodesPerParent += (mDegree - 1);
}
return nodesPerParent;
}
/**
* Creates a HuffmanNode for each of the nodes in the {@code windowRoot}. The HuffmanNode
* internally keeps track of the probability for each of these nodes. Finally, all the
* HuffmanNodes are added to a priority queue to keep them sorted on an ascending order based
* on their probabilities.
*
* @param userContext The actions the user has taken so far. In case of an IME, this would be
* what the user has typed so far.
* @param windowRoot The root of the tree of SwitchAccessNodeCompats
* @return Returns a TreeSet which contains all the HuffmanNodes in ascending order based on
* their probabilities. If the {@code windowRoot} contains no clickable nodes, an empty
* TreeSet is returned.
*/
private PriorityQueue<HuffmanNode> getOptionScanNodeProbabilities(String userContext,
SwitchAccessNodeCompat windowRoot) {
LinkedList<SwitchAccessNodeCompat> talkBackOrderList = getNodesInTalkBackOrder(windowRoot);
Set<SwitchAccessNodeCompat> talkBackOrderSet = new HashSet<>(talkBackOrderList);
Map<SwitchAccessNodeCompat, Double> probabilityDistribution =
mProbabilityModelReader.getProbabilityDistribution(userContext, talkBackOrderSet);
PriorityQueue<HuffmanNode> optionScanNodeProbabilities = new PriorityQueue<>();
for(SwitchAccessNodeCompat currentNode : talkBackOrderSet) {
Double currentNodeProbability = probabilityDistribution.get(currentNode);
List<AccessibilityNodeActionNode> currentNodeActions =
getCompatActionNodes(currentNode);
/* TODO: need to think about the correct behaviour when there are more
* than one actions associated with a node */
if (currentNodeActions.size() == 1) {
optionScanNodeProbabilities.add(
new HuffmanNode(currentNodeActions.get(0), currentNodeProbability));
}
currentNode.recycle();
}
return optionScanNodeProbabilities;
}
@Override
public OptionScanNode addViewHierarchyToTree(SwitchAccessNodeCompat node, OptionScanNode treeToBuildOn) {
return null;
}
@Override
public OptionScanNode addWindowListToTree(List<SwitchAccessWindowInfo> windowList, OptionScanNode treeToBuildOn) {
return null;
}
@Override
public OptionScanNode buildContextMenu(List<? extends ContextMenuItem> actionList) {
return null;
}
/**
* A HuffmanNode is a wrapper class that associates an OptionScanNode with a certain
* probability. It implements the Comparable interface so that the natural ordering of
* HuffmanNodes is based on their probabilities (ascending order).
*/
private class HuffmanNode implements Comparable<HuffmanNode> {
private OptionScanNode mOptionScanNode;
private Double mProbability;
private Boolean mHasClearFocusNode = false;
public HuffmanNode(OptionScanNode optionScanNode, Double probability) {
mOptionScanNode = optionScanNode;
mProbability = probability;
}
public OptionScanNode getOptionScanNode() {
return mOptionScanNode;
}
public Double getProbability() {
return mProbability;
}
public Boolean hasClearFocusNode() {
return mHasClearFocusNode;
}
public void setClearFocusNodePresence() {
mHasClearFocusNode = true;
}
@Override
public int compareTo(@NonNull HuffmanNode node) {
return this.mProbability.compareTo(node.mProbability);
}
}
}