package com.mixpanel.android.viewcrawler;
import android.view.View;
import android.view.ViewGroup;
import com.mixpanel.android.util.MPLog;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
/**
* Paths in the view hierarchy, and the machinery for finding views using them.
*
* An individual pathfinder is NOT THREAD SAFE, and should only be used by one thread at a time.
*/
/* package */ class Pathfinder {
/**
* a path element E matches a view V if each non "prefix" or "index"
* attribute of E is equal to (or characteristic of) V.
*
* So
*
* E.viewClassName == 'com.mixpanel.Awesome' => V instanceof com.mixpanelAwesome
* E.id == 123 => V.getId() == 123
*
* The index attribute, counting from root to leaf, and first child to last child, selects a particular
* matching view amongst all possible matches. Indexing starts at zero, like an array
* index. So E.index == 2 means "Select the third possible match for this element"
*
* The prefix attribute refers to the position of the matched views in the hierarchy,
* relative to the current position of the path being searched. The "current position" of
* a path element is determined by the path that preceeded that element:
*
* - The current position of the empty path is the root view
*
* - The current position of a non-empty path is the children of any element that matched the last
* element of that path.
*
* Prefix values can be:
*
* ZERO_LENGTH_PREFIX- the next match must occur at the current position (so at the root
* view if this is the first element of a path, or at the matching children of the views
* already matched by the preceeding portion of the path.) If a path element with ZERO_LENGTH_PREFIX
* has no index, then *all* matching elements of the path will be matched, otherwise indeces
* will count from first child to last child.
*
* SHORTEST_PREFIX- the next match must occur at some descendant of the current position.
* SHORTEST_PREFIX elements are indexed depth-first, first child to last child. For performance
* reasons, at most one element will ever be matched to a SHORTEST_PREFIX element, so
* elements with no index will be treated as having index == 0
*/
public static class PathElement {
public PathElement(int usePrefix, String vClass, int ix, int vId, String cDesc, String vTag) {
prefix = usePrefix;
viewClassName = vClass;
index = ix;
viewId = vId;
contentDescription = cDesc;
tag = vTag;
}
@Override
public String toString() {
try {
final JSONObject ret = new JSONObject();
if (prefix == SHORTEST_PREFIX) {
ret.put("prefix", "shortest");
}
if (null != viewClassName) {
ret.put("view_class", viewClassName);
}
if (index > -1) {
ret.put("index", index);
}
if (viewId > -1) {
ret.put("id", viewId);
}
if (null != contentDescription) {
ret.put("contentDescription", contentDescription);
}
if (null != tag) {
ret.put("tag", tag);
}
return ret.toString();
} catch (final JSONException e) {
throw new RuntimeException("Can't serialize PathElement to String", e);
}
}
public final int prefix;
public final String viewClassName;
public final int index;
public final int viewId;
public final String contentDescription;
public final String tag;
public static final int ZERO_LENGTH_PREFIX = 0;
public static final int SHORTEST_PREFIX = 1;
}
public interface Accumulator {
public void accumulate(View v);
}
public Pathfinder() {
mIndexStack = new IntStack();
}
public void findTargetsInRoot(View givenRootView, List<PathElement> path, Accumulator accumulator) {
if (path.isEmpty()) {
return;
}
if (mIndexStack.full()) {
MPLog.w(LOGTAG, "There appears to be a concurrency issue in the pathfinding code. Path will not be matched.");
return; // No memory to perform the find.
}
final PathElement rootPathElement = path.get(0);
final List<PathElement> childPath = path.subList(1, path.size());
final int indexKey = mIndexStack.alloc();
final View rootView = findPrefixedMatch(rootPathElement, givenRootView, indexKey);
mIndexStack.free();
if (null != rootView) {
findTargetsInMatchedView(rootView, childPath, accumulator);
}
}
private void findTargetsInMatchedView(View alreadyMatched, List<PathElement> remainingPath, Accumulator accumulator) {
// When this is run, alreadyMatched has already been matched to a path prefix.
// path is a possibly empty "remaining path" suffix left over after the match
if (remainingPath.isEmpty()) {
// Nothing left to match- we're found!
accumulator.accumulate(alreadyMatched);
return;
}
if (!(alreadyMatched instanceof ViewGroup)) {
// Matching a non-empty path suffix is impossible, because we have no children
return;
}
if (mIndexStack.full()) {
MPLog.v(LOGTAG, "Path is too deep, will not match");
// Can't match anyhow, stack is too deep
return;
}
final ViewGroup parent = (ViewGroup) alreadyMatched;
final PathElement matchElement = remainingPath.get(0);
final List<PathElement> nextPath = remainingPath.subList(1, remainingPath.size());
final int childCount = parent.getChildCount();
final int indexKey = mIndexStack.alloc();
for (int i = 0; i < childCount; i++) {
final View givenChild = parent.getChildAt(i);
final View child = findPrefixedMatch(matchElement, givenChild, indexKey);
if (null != child) {
findTargetsInMatchedView(child, nextPath, accumulator);
}
if (matchElement.index >= 0 && mIndexStack.read(indexKey) > matchElement.index) {
break;
}
}
mIndexStack.free();
}
// Finds the first matching view of the path element in the given subject's view hierarchy.
// If the path is indexed, it needs a start index, and will consume some indexes
private View findPrefixedMatch(PathElement findElement, View subject, int indexKey) {
final int currentIndex = mIndexStack.read(indexKey);
if (matches(findElement, subject)) {
mIndexStack.increment(indexKey);
if (findElement.index == -1 || findElement.index == currentIndex) {
return subject;
}
}
if (findElement.prefix == PathElement.SHORTEST_PREFIX && subject instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) subject;
final int childCount = group.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = group.getChildAt(i);
final View result = findPrefixedMatch(findElement, child, indexKey);
if (null != result) {
return result;
}
}
}
return null;
}
private boolean matches(PathElement matchElement, View subject) {
if (null != matchElement.viewClassName &&
!hasClassName(subject, matchElement.viewClassName)) {
return false;
}
if (-1 != matchElement.viewId && subject.getId() != matchElement.viewId) {
return false;
}
if (null != matchElement.contentDescription &&
!matchElement.contentDescription.equals(subject.getContentDescription())) {
return false;
}
final String matchTag = matchElement.tag;
if (null != matchElement.tag) {
final Object subjectTag = subject.getTag();
if (null == subjectTag || !matchTag.equals(subject.getTag().toString())) {
return false;
}
}
return true;
}
private static boolean hasClassName(Object o, String className) {
Class<?> klass = o.getClass();
while (true) {
if (klass.getCanonicalName().equals(className)) {
return true;
}
if (klass == Object.class) {
return false;
}
klass = klass.getSuperclass();
}
}
/**
* Bargain-bin pool of integers, for use in avoiding allocations during path crawl
*/
private static class IntStack {
public IntStack() {
mStack = new int[MAX_INDEX_STACK_SIZE];
mStackSize = 0;
}
public boolean full() {
return mStack.length == mStackSize;
}
/**
* Pushes a new value, and returns the index you can use to increment and read that value later.
*/
public int alloc() {
final int index = mStackSize;
mStackSize++;
mStack[index] = 0;
return index;
}
/**
* Gets the value associated with index. index should be the result of a previous call to alloc()
*/
public int read(int index) {
return mStack[index];
}
public void increment(int index) {
mStack[index]++;
}
/**
* Should be matched to each call to alloc. Once free has been called, the key associated with the
* matching alloc should be considered invalid.
*/
public void free() {
mStackSize--;
if (mStackSize < 0) {
throw new ArrayIndexOutOfBoundsException(mStackSize);
}
}
private final int[] mStack;
private int mStackSize;
private static final int MAX_INDEX_STACK_SIZE = 256;
}
private final IntStack mIndexStack;
@SuppressWarnings("unused")
private static final String LOGTAG = "MixpanelAPI.PathFinder";
}