aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2013-10-30 13:10:10 -0700
committerKevin Jin <kjin@google.com>2013-11-04 13:47:14 -0800
commitdfc316e1bfb37148c50947c46f5aaed5cb2e708a (patch)
tree9633e30b2e6ff79fd692652b315b57452a819bc9 /src
parent45828d52e6a2d9694eb507b5cafd3b6fcae9c33c (diff)
downloaddroiddriver-dfc316e1bfb37148c50947c46f5aaed5cb2e708a.tar.gz
Take snapshot of the underlying View or AccessibilityNodeInfo tree.
This improves reliability. Add JavaDoc on order of getChildren. Change-Id: Iec4a4b693ef29eea1e067d538bab0078699e3d50
Diffstat (limited to 'src')
-rw-r--r--src/com/google/android/droiddriver/UiElement.java7
-rw-r--r--src/com/google/android/droiddriver/base/BaseUiElement.java114
-rw-r--r--src/com/google/android/droiddriver/finders/Attribute.java135
-rw-r--r--src/com/google/android/droiddriver/finders/By.java12
-rw-r--r--src/com/google/android/droiddriver/finders/ByAttribute.java2
-rw-r--r--src/com/google/android/droiddriver/finders/ByXPath.java5
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java4
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java3
-rw-r--r--src/com/google/android/droiddriver/instrumentation/RootFinder.java3
-rw-r--r--src/com/google/android/droiddriver/instrumentation/ViewElement.java319
-rw-r--r--src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java23
-rw-r--r--src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java23
-rw-r--r--src/com/google/android/droiddriver/scroll/SentinelScroller.java9
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java4
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java2
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java193
16 files changed, 466 insertions, 392 deletions
diff --git a/src/com/google/android/droiddriver/UiElement.java b/src/com/google/android/droiddriver/UiElement.java
index a0d2ae3..1ae6254 100644
--- a/src/com/google/android/droiddriver/UiElement.java
+++ b/src/com/google/android/droiddriver/UiElement.java
@@ -196,6 +196,13 @@ public interface UiElement {
* does not include off-screen children, but may include invisible on-screen
* children.</li>
* </ul>
+ * <p>
+ * Another discrepancy between {@link InstrumentationDriver}
+ * {@link UiAutomationDriver} is the order of children. The Accessibility API
+ * returns children in the order of layout (see
+ * {@link android.view.ViewGroup#addChildrenForAccessibility}, which is added
+ * in API16).
+ * </p>
*/
List<? extends UiElement> getChildren(Predicate<? super UiElement> predicate);
diff --git a/src/com/google/android/droiddriver/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java
index dce0ee5..6a2f88c 100644
--- a/src/com/google/android/droiddriver/base/BaseUiElement.java
+++ b/src/com/google/android/droiddriver/base/BaseUiElement.java
@@ -16,6 +16,8 @@
package com.google.android.droiddriver.base;
+import android.graphics.Rect;
+
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.Action;
import com.google.android.droiddriver.actions.ClickAction;
@@ -31,12 +33,14 @@ import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import org.w3c.dom.Element;
import java.lang.ref.WeakReference;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
@@ -46,9 +50,90 @@ import java.util.concurrent.FutureTask;
public abstract class BaseUiElement implements UiElement {
private WeakReference<Element> domNode;
+ @SuppressWarnings("unchecked")
@Override
public <T> T get(Attribute attribute) {
- return attribute.getValue(this);
+ return (T) getAttributes().get(attribute);
+ }
+
+ @Override
+ public String getText() {
+ return get(Attribute.TEXT);
+ }
+
+ @Override
+ public String getContentDescription() {
+ return get(Attribute.CONTENT_DESC);
+ }
+
+ @Override
+ public String getClassName() {
+ return get(Attribute.CLASS);
+ }
+
+ @Override
+ public String getResourceId() {
+ return get(Attribute.RESOURCE_ID);
+ }
+
+ @Override
+ public String getPackageName() {
+ return get(Attribute.PACKAGE);
+ }
+
+ @Override
+ public boolean isCheckable() {
+ return (Boolean) get(Attribute.CHECKABLE);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return (Boolean) get(Attribute.CHECKED);
+ }
+
+ @Override
+ public boolean isClickable() {
+ return (Boolean) get(Attribute.CLICKABLE);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return (Boolean) get(Attribute.ENABLED);
+ }
+
+ @Override
+ public boolean isFocusable() {
+ return (Boolean) get(Attribute.FOCUSABLE);
+ }
+
+ @Override
+ public boolean isFocused() {
+ return (Boolean) get(Attribute.FOCUSED);
+ }
+
+ @Override
+ public boolean isScrollable() {
+ return (Boolean) get(Attribute.SCROLLABLE);
+ }
+
+ @Override
+ public boolean isLongClickable() {
+ return (Boolean) get(Attribute.LONG_CLICKABLE);
+ }
+
+ @Override
+ public boolean isPassword() {
+ return (Boolean) get(Attribute.PASSWORD);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return (Boolean) get(Attribute.SELECTED);
+ }
+
+ @Override
+ public Rect getBounds() {
+ return get(Attribute.BOUNDS);
}
@Override
@@ -122,9 +207,9 @@ public abstract class BaseUiElement implements UiElement {
perform(SwipeAction.toScroll(direction));
}
- protected abstract int getChildCount();
+ protected abstract Map<Attribute, Object> getAttributes();
- protected abstract BaseUiElement getChild(int index);
+ protected abstract List<? extends BaseUiElement> getChildren();
protected abstract InputInjector getInjector();
@@ -135,21 +220,16 @@ public abstract class BaseUiElement implements UiElement {
}
@Override
- public List<BaseUiElement> getChildren(Predicate<? super UiElement> predicate) {
- if (predicate == null) {
- predicate = Predicates.notNull();
- } else {
- predicate = Predicates.and(Predicates.notNull(), predicate);
+ public List<? extends BaseUiElement> getChildren(Predicate<? super UiElement> predicate) {
+ List<? extends BaseUiElement> children = getChildren();
+ if (children == null) {
+ return ImmutableList.of();
}
-
- List<BaseUiElement> list = Lists.newArrayList();
- for (int i = 0; i < getChildCount(); i++) {
- BaseUiElement child = getChild(i);
- if (predicate.apply(child)) {
- list.add(child);
- }
+ if (predicate == null || predicate.equals(Predicates.alwaysTrue())) {
+ return children;
}
- return list;
+
+ return ImmutableList.copyOf(Collections2.filter(children, predicate));
}
@Override
diff --git a/src/com/google/android/droiddriver/finders/Attribute.java b/src/com/google/android/droiddriver/finders/Attribute.java
index 2f8bbf6..1aeb86c 100644
--- a/src/com/google/android/droiddriver/finders/Attribute.java
+++ b/src/com/google/android/droiddriver/finders/Attribute.java
@@ -16,124 +16,23 @@
package com.google.android.droiddriver.finders;
-import android.graphics.Rect;
-
-import com.google.android.droiddriver.UiElement;
-
public enum Attribute {
- CHECKABLE("checkable") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isCheckable();
- }
- },
- CHECKED("checked") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isChecked();
- }
- },
- CLASS("class") {
- @SuppressWarnings("unchecked")
- @Override
- public String getValue(UiElement element) {
- return element.getClassName();
- }
- },
- CLICKABLE("clickable") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isClickable();
- }
- },
- CONTENT_DESC("content-desc") {
- @SuppressWarnings("unchecked")
- @Override
- public String getValue(UiElement element) {
- return element.getContentDescription();
- }
- },
- ENABLED("enabled") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isEnabled();
- }
- },
- FOCUSABLE("focusable") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isFocusable();
- }
- },
- FOCUSED("focused") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isFocused();
- }
- },
- LONG_CLICKABLE("long-clickable") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isLongClickable();
- }
- },
- PACKAGE("package") {
- @SuppressWarnings("unchecked")
- @Override
- public String getValue(UiElement element) {
- return element.getPackageName();
- }
- },
- PASSWORD("password") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isPassword();
- }
- },
- RESOURCE_ID("resource-id") {
- @SuppressWarnings("unchecked")
- @Override
- public String getValue(UiElement element) {
- return element.getResourceId();
- }
- },
- SCROLLABLE("scrollable") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isScrollable();
- }
- },
- SELECTED("selected") {
- @SuppressWarnings("unchecked")
- @Override
- public Boolean getValue(UiElement element) {
- return element.isSelected();
- }
- },
- TEXT("text") {
- @SuppressWarnings("unchecked")
- @Override
- public String getValue(UiElement element) {
- return element.getText();
- }
- },
- BOUNDS("bounds") {
- @SuppressWarnings("unchecked")
- @Override
- public Rect getValue(UiElement element) {
- // TODO: clip by boundsInParent?
- return element.getBounds();
- }
- };
+ CHECKABLE("checkable"),
+ CHECKED("checked"),
+ CLASS("class"),
+ CLICKABLE("clickable"),
+ CONTENT_DESC("content-desc"),
+ ENABLED("enabled"),
+ FOCUSABLE("focusable"),
+ FOCUSED("focused"),
+ LONG_CLICKABLE("long-clickable"),
+ PACKAGE("package"),
+ PASSWORD("password"),
+ RESOURCE_ID("resource-id"),
+ SCROLLABLE("scrollable"),
+ SELECTED("selected"),
+ TEXT("text"),
+ BOUNDS("bounds");
private final String name;
@@ -145,8 +44,6 @@ public enum Attribute {
return name;
}
- public abstract <T> T getValue(UiElement element);
-
@Override
public String toString() {
return name;
diff --git a/src/com/google/android/droiddriver/finders/By.java b/src/com/google/android/droiddriver/finders/By.java
index b4e1a74..0f1592b 100644
--- a/src/com/google/android/droiddriver/finders/By.java
+++ b/src/com/google/android/droiddriver/finders/By.java
@@ -32,6 +32,18 @@ import java.util.List;
* Convenience methods to create commonly used finders.
*/
public class By {
+ private static final MatchFinder ANY = new MatchFinder(null) {
+ @Override
+ public String toString() {
+ return "any";
+ }
+ };
+
+ /** Matches any UiElement. */
+ public static MatchFinder any() {
+ return ANY;
+ }
+
/** Matches by {@link Object#equals}. */
public static final MatchStrategy<Object> OBJECT_EQUALS = new MatchStrategy<Object>() {
@Override
diff --git a/src/com/google/android/droiddriver/finders/ByAttribute.java b/src/com/google/android/droiddriver/finders/ByAttribute.java
index 3356533..a797a6d 100644
--- a/src/com/google/android/droiddriver/finders/ByAttribute.java
+++ b/src/com/google/android/droiddriver/finders/ByAttribute.java
@@ -33,7 +33,7 @@ public class ByAttribute<T> extends MatchFinder {
super(new Predicate<UiElement>() {
@Override
public boolean apply(UiElement element) {
- T value = attribute.getValue(element);
+ T value = element.get(attribute);
return strategy.match(expected, value);
}
});
diff --git a/src/com/google/android/droiddriver/finders/ByXPath.java b/src/com/google/android/droiddriver/finders/ByXPath.java
index 50b1d3e..dbe4363 100644
--- a/src/com/google/android/droiddriver/finders/ByXPath.java
+++ b/src/com/google/android/droiddriver/finders/ByXPath.java
@@ -136,7 +136,10 @@ public class ByXPath implements Finder {
setAttribute(element, Attribute.SELECTED, uiElement.isSelected());
element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString());
- // TODO: visitor pattern
+ // TODO: Make VISIBLE optional so that the DOM dump contains all children.
+ // This is especially useful for InstrumentationDriver which sees more than
+ // uiautomatorviewer does. For now users can temporarily change VISIBLE to
+ // null, rebuild test apk and run to get a more comprehensive dump.
for (BaseUiElement child : uiElement.getChildren(UiElement.VISIBLE)) {
element.appendChild(child.getDomNode());
}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
index 3868082..27fa975 100644
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
@@ -68,10 +68,10 @@ class InstrumentationContext implements DroidDriverContext {
return injector;
}
- public ViewElement getUiElement(View view) {
+ public ViewElement getUiElement(View view, ViewElement parent) {
ViewElement element = map.get(view);
if (element == null) {
- element = new ViewElement(this, view);
+ element = new ViewElement(this, view, parent);
map.put(view, element);
}
return element;
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
index 8686098..dbe9789 100644
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
@@ -46,7 +46,7 @@ public class InstrumentationDriver extends BaseDroidDriver {
@Override
protected ViewElement getNewRootElement() {
- return context.getUiElement(findRootView());
+ return context.getUiElement(findRootView(), null /* parent */);
}
@Override
@@ -58,6 +58,7 @@ public class InstrumentationDriver extends BaseDroidDriver {
Activity runningActivity = getRunningActivity();
View[] views = RootFinder.getRootViews();
if (views.length > 1) {
+ Logs.log(Log.VERBOSE, "views.length=" + views.length);
for (View view : views) {
if (view.hasWindowFocus()) {
return view;
diff --git a/src/com/google/android/droiddriver/instrumentation/RootFinder.java b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
index 2cdbd51..eec06c0 100644
--- a/src/com/google/android/droiddriver/instrumentation/RootFinder.java
+++ b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
@@ -17,11 +17,9 @@
package com.google.android.droiddriver.instrumentation;
import android.os.Build;
-import android.util.Log;
import android.view.View;
import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.util.Logs;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -78,7 +76,6 @@ public class RootFinder {
try {
views = (View[]) viewsField.get(windowManagerObj);
- Logs.log(Log.DEBUG, "View size:" + views.length);
return views;
} catch (RuntimeException re) {
throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
diff --git a/src/com/google/android/droiddriver/instrumentation/ViewElement.java b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
index 5106975..c4aecc1 100644
--- a/src/com/google/android/droiddriver/instrumentation/ViewElement.java
+++ b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
@@ -20,32 +20,164 @@ import static com.google.android.droiddriver.util.TextUtils.charSequenceToString
import android.content.res.Resources;
import android.graphics.Rect;
-import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewParent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.TextView;
import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.base.BaseUiElement;
-import com.google.android.droiddriver.util.Logs;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.finders.Attribute;
+import com.google.common.base.Function;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import java.util.List;
import java.util.Map;
/**
- * A UiElement that is backed by a View.
+ * A UiElement that is backed by a View. A snapshot of all attributes is taken
+ * at construction. The attributes of a {@code ViewElement} instance are
+ * immutable. If the underlying view is updated, a new {@code ViewElement}
+ * instance will be created in
+ * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
*/
-// TODO: always accessing view on the UI thread even when only get access is
-// needed -- the field may be in the middle of updating.
public class ViewElement extends BaseUiElement {
- private static final Map<String, String> CLASS_NAME_OVERRIDES = Maps.newHashMap();
+ private static class SnapshotViewAttributesRunnable implements Runnable {
+ private final View view;
+ final Map<Attribute, Object> attribs = Maps.newEnumMap(Attribute.class);
+ boolean visible;
+ Rect visibleBounds;
+ List<View> childViews;
+ Throwable exception;
- private final InstrumentationContext context;
- private final View view;
+ private SnapshotViewAttributesRunnable(View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void run() {
+ try {
+ put(Attribute.PACKAGE, view.getContext().getPackageName());
+ put(Attribute.CLASS, getClassName());
+ put(Attribute.TEXT, getText());
+ put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription()));
+ put(Attribute.RESOURCE_ID, getResourceId());
+ put(Attribute.CHECKABLE, view instanceof Checkable);
+ put(Attribute.CHECKED, isChecked());
+ put(Attribute.CLICKABLE, view.isClickable());
+ put(Attribute.ENABLED, view.isEnabled());
+ put(Attribute.FOCUSABLE, view.isFocusable());
+ put(Attribute.FOCUSED, view.isFocused());
+ put(Attribute.LONG_CLICKABLE, view.isLongClickable());
+ put(Attribute.PASSWORD, isPassword());
+ put(Attribute.SCROLLABLE, isScrollable());
+ put(Attribute.SELECTED, view.isSelected());
+ put(Attribute.BOUNDS, getBounds());
+
+ // Order matters as setVisible() depends on setVisibleBounds().
+ this.visibleBounds = getVisibleBounds();
+ // isShown() checks the visibility flag of this view and ancestors; it
+ // needs to have the VISIBLE flag as well as non-empty bounds to be
+ // visible.
+ this.visible = view.isShown() && !visibleBounds.isEmpty();
+ setChildViews();
+ } catch (Throwable e) {
+ exception = e;
+ }
+ }
+
+ private void put(Attribute key, Object value) {
+ if (value != null) {
+ attribs.put(key, value);
+ }
+ }
+
+ private String getText() {
+ if (!(view instanceof TextView)) {
+ return null;
+ }
+ return charSequenceToString(((TextView) view).getText());
+ }
+
+ private String getClassName() {
+ String className = view.getClass().getName();
+ return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className)
+ : className;
+ }
+
+ private String getResourceId() {
+ if (view.getId() != View.NO_ID && view.getResources() != null) {
+ try {
+ return charSequenceToString(view.getResources().getResourceName(view.getId()));
+ } catch (Resources.NotFoundException nfe) {
+ /* ignore */
+ }
+ }
+ return null;
+ }
+
+ private boolean isChecked() {
+ return view instanceof Checkable && ((Checkable) view).isChecked();
+ }
+
+ private boolean isScrollable() {
+ // TODO: find a meaningful implementation
+ return true;
+ }
+
+ private boolean isPassword() {
+ // TODO: find a meaningful implementation
+ return false;
+ }
+
+ private Rect getBounds() {
+ Rect rect = new Rect();
+ int[] xy = new int[2];
+ view.getLocationOnScreen(xy);
+ rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight());
+ return rect;
+ }
+
+ private Rect getVisibleBounds() {
+ Rect visibleBounds = new Rect();
+ if (!view.isShown() || !view.getGlobalVisibleRect(visibleBounds)) {
+ visibleBounds.setEmpty();
+ }
+ int[] xyScreen = new int[2];
+ view.getLocationOnScreen(xyScreen);
+ int[] xyWindow = new int[2];
+ view.getLocationInWindow(xyWindow);
+ int windowLeft = xyScreen[0] - xyWindow[0];
+ int windowTop = xyScreen[1] - xyWindow[1];
+
+ // Bounds are relative to root view; adjust to screen coordinates.
+ visibleBounds.offset(windowLeft, windowTop);
+ return visibleBounds;
+ }
+
+ private void setChildViews() {
+ if (!(view instanceof ViewGroup)) {
+ return;
+ }
+ ViewGroup group = (ViewGroup) view;
+ int childCount = group.getChildCount();
+ childViews = Lists.newArrayListWithExpectedSize(childCount);
+ for (int i = 0; i < childCount; i++) {
+ View child = group.getChildAt(i);
+ if (child != null) {
+ childViews.add(child);
+ }
+ }
+ }
+ }
+
+ private static final Map<String, String> CLASS_NAME_OVERRIDES = Maps.newHashMap();
/**
* Typically users find the class name to use in tests using SDK tool
@@ -66,161 +198,62 @@ public class ViewElement extends BaseUiElement {
CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName);
}
- public ViewElement(InstrumentationContext context, View view) {
- this.context = Preconditions.checkNotNull(context);
- this.view = Preconditions.checkNotNull(view);
- }
-
- @Override
- public String getText() {
- if (!(view instanceof TextView)) {
- return null;
- }
- return charSequenceToString(((TextView) view).getText());
- }
-
- @Override
- public String getContentDescription() {
- return charSequenceToString(view.getContentDescription());
- }
-
- @Override
- public String getClassName() {
- String className = view.getClass().getName();
- return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className)
- : className;
- }
+ private final InstrumentationContext context;
+ private final Map<Attribute, Object> attributes;
+ private final boolean visible;
+ private final Rect visibleBounds;
+ private final ViewElement parent;
+ private final List<ViewElement> children;
- @Override
- public String getResourceId() {
- if (view.getId() != View.NO_ID && view.getResources() != null) {
- try {
- return charSequenceToString(view.getResources().getResourceName(view.getId()));
- } catch (Resources.NotFoundException nfe) {
- /* ignore */
- }
+ public ViewElement(final InstrumentationContext context, View view, ViewElement parent) {
+ this.context = Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(view);
+ this.parent = parent;
+ SnapshotViewAttributesRunnable attributesSnapshot = new SnapshotViewAttributesRunnable(view);
+ context.getInstrumentation().runOnMainSync(attributesSnapshot);
+ if (attributesSnapshot.exception != null) {
+ throw new DroidDriverException(attributesSnapshot.exception);
}
- return null;
- }
- @Override
- public String getPackageName() {
- return view.getContext().getPackageName();
+ attributes = ImmutableMap.copyOf(attributesSnapshot.attribs);
+ this.visibleBounds = attributesSnapshot.visibleBounds;
+ this.visible = attributesSnapshot.visible;
+ this.children =
+ attributesSnapshot.childViews == null ? null : ImmutableList.copyOf(Lists.transform(
+ attributesSnapshot.childViews, new Function<View, ViewElement>() {
+ public ViewElement apply(View input) {
+ return context.getUiElement(input, ViewElement.this);
+ }
+ }));
}
@Override
- public InputInjector getInjector() {
- return context.getInjector();
+ public Rect getVisibleBounds() {
+ return visibleBounds;
}
@Override
public boolean isVisible() {
- // isShown() checks the visibility flag of this view and ancestors; it needs
- // to have the VISIBLE flag as well as non-empty bounds to be visible.
- return view.isShown() && !getVisibleBounds().isEmpty();
- }
-
- @Override
- public boolean isCheckable() {
- return view instanceof Checkable;
- }
-
- @Override
- public boolean isChecked() {
- if (!isCheckable()) {
- return false;
- }
- return ((Checkable) view).isChecked();
- }
-
- @Override
- public boolean isClickable() {
- return view.isClickable();
- }
-
- @Override
- public boolean isEnabled() {
- return view.isEnabled();
- }
-
- @Override
- public boolean isFocusable() {
- return view.isFocusable();
- }
-
- @Override
- public boolean isFocused() {
- return view.isFocused();
- }
-
- @Override
- public boolean isScrollable() {
- // TODO: find a meaningful implementation
- return true;
- }
-
- @Override
- public boolean isLongClickable() {
- return view.isLongClickable();
+ return visible;
}
@Override
- public boolean isPassword() {
- // TODO: find a meaningful implementation
- return false;
- }
-
- @Override
- public boolean isSelected() {
- return view.isSelected();
- }
-
- @Override
- public Rect getBounds() {
- Rect rect = new Rect();
- int[] xy = new int[2];
- view.getLocationOnScreen(xy);
- rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight());
- return rect;
- }
-
- @Override
- public Rect getVisibleBounds() {
- Rect visibleBounds = new Rect();
- if (!view.getGlobalVisibleRect(visibleBounds)) {
- Logs.log(Log.VERBOSE, "View is invisible: " + toString());
- visibleBounds.setEmpty();
- }
- int[] xy = new int[2];
- view.getLocationOnScreen(xy);
- // Bounds are relative to root view; adjust to screen coordinates.
- visibleBounds.offsetTo(xy[0], xy[1]);
- return visibleBounds;
+ public ViewElement getParent() {
+ return parent;
}
@Override
- protected int getChildCount() {
- if (!(view instanceof ViewGroup)) {
- return 0;
- }
- return ((ViewGroup) view).getChildCount();
+ protected List<ViewElement> getChildren() {
+ return children;
}
@Override
- protected ViewElement getChild(int index) {
- if (!(view instanceof ViewGroup)) {
- return null;
- }
- View child = ((ViewGroup) view).getChildAt(index);
- return child == null ? null : context.getUiElement(child);
+ protected Map<Attribute, Object> getAttributes() {
+ return attributes;
}
@Override
- public ViewElement getParent() {
- ViewParent parent = view.getParent();
- if (!(parent instanceof View)) {
- return null;
- }
- return context.getUiElement((View) parent);
+ protected InputInjector getInjector() {
+ return context.getInjector();
}
}
diff --git a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java
index 4a1c373..2c339ad 100644
--- a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java
@@ -15,6 +15,8 @@
*/
package com.google.android.droiddriver.scroll;
+import android.util.Log;
+
import com.google.android.droiddriver.DroidDriver;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.exceptions.ElementNotFoundException;
@@ -23,6 +25,7 @@ import com.google.android.droiddriver.finders.Finder;
import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
import com.google.android.droiddriver.scroll.Direction.LogicalDirection;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Logs;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
@@ -105,6 +108,24 @@ public abstract class AbstractSentinelStrategy implements SentinelStrategy {
};
/**
+ * Returns the second last child as the sentinel. Useful when the activity
+ * always shows the last child as an anchor (for example a footer).
+ * <p>
+ * Sometimes uiautomatorviewer may not show the anchor as the last child, due
+ * to the reordering by layout described in {@link UiElement#getChildren}.
+ * This is not a problem with UiAutomationDriver because it sees the same as
+ * uiautomatorviewer does, but could be a problem with InstrumentationDriver.
+ * </p>
+ */
+ public static final GetStrategy SECOND_LAST_CHILD_GETTER = new GetStrategy(
+ Predicates.alwaysTrue(), "SECOND_LAST_CHILD") {
+ @Override
+ protected UiElement getSentinel(List<? extends UiElement> children) {
+ return children.size() < 2 ? null : children.get(children.size() - 2);
+ }
+ };
+
+ /**
* Returns the second child as the sentinel. Useful when the activity shows a
* fixed first child.
*/
@@ -130,6 +151,7 @@ public abstract class AbstractSentinelStrategy implements SentinelStrategy {
if (sentinel == null) {
throw new ElementNotFoundException(this);
}
+ Logs.log(Log.INFO, "Found match: " + sentinel);
return sentinel;
}
@@ -156,6 +178,7 @@ public abstract class AbstractSentinelStrategy implements SentinelStrategy {
protected UiElement getSentinel(DroidDriver driver, Finder containerFinder,
PhysicalDirection direction) {
+ Logs.call(this, "getSentinel", driver, containerFinder, direction);
Finder sentinelFinder;
LogicalDirection logicalDirection = directionConverter.toLogicalDirection(direction);
if (logicalDirection == LogicalDirection.BACKWARD) {
diff --git a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
index 488b231..3ae0e50 100644
--- a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
@@ -20,6 +20,7 @@ import android.util.Log;
import com.google.android.droiddriver.DroidDriver;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.finders.By;
import com.google.android.droiddriver.finders.Finder;
import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
@@ -150,6 +151,28 @@ public class DynamicSentinelStrategy extends AbstractSentinelStrategy {
}
}
+ /**
+ * Determines whether the sentinel is updated by checking the resource-id of a
+ * descendant element of the sentinel (often itself). This is useful when the
+ * children of the container are heterogeneous -- they don't have a common
+ * pattern to get a unique string.
+ */
+ public static class ResourceIdUpdated extends SingleStringUpdated {
+ /**
+ * Uses the resource-id of the sentinel itself.
+ */
+ public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any());
+
+ public ResourceIdUpdated(Finder uniqueStringFinder) {
+ super(uniqueStringFinder);
+ }
+
+ @Override
+ protected String getUniqueString(UiElement uniqueStringElement) {
+ return uniqueStringElement.getResourceId();
+ }
+ }
+
private final IsUpdatedStrategy isUpdatedStrategy;
/**
diff --git a/src/com/google/android/droiddriver/scroll/SentinelScroller.java b/src/com/google/android/droiddriver/scroll/SentinelScroller.java
index dd0b17e..273d6fa 100644
--- a/src/com/google/android/droiddriver/scroll/SentinelScroller.java
+++ b/src/com/google/android/droiddriver/scroll/SentinelScroller.java
@@ -41,8 +41,13 @@ import com.google.android.droiddriver.util.Logs;
* {@link SentinelStrategy} is used to determine whether more scrolling is
* possible.
* <p>
- * This algorithm is needed unless the DroidDriver implementation supports
- * directly jumping to the item.
+ * This implementation may not work well with InstrumentationDriver if
+ * {@link UiElement#getChildren} returns children in an order different from the
+ * Accessibility API. In this case you may try
+ * {@link AbstractSentinelStrategy#SECOND_LAST_CHILD_GETTER}.
+ * </p>
+ * TODO: A {@link Scroller} that directly jumps to the item if an
+ * InstrumentationDriver is used.
*/
public class SentinelScroller implements Scroller {
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
index 6e56dfb..0b56d33 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
@@ -62,10 +62,10 @@ class UiAutomationContext implements DroidDriverContext {
return injector;
}
- public UiAutomationElement getUiElement(AccessibilityNodeInfo node) {
+ public UiAutomationElement getUiElement(AccessibilityNodeInfo node, UiAutomationElement parent) {
UiAutomationElement element = map.get(node);
if (element == null) {
- element = new UiAutomationElement(this, node);
+ element = new UiAutomationElement(this, node, parent);
map.put(node, element);
}
return element;
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
index 49b04ac..0e5ea78 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
@@ -53,7 +53,7 @@ public class UiAutomationDriver extends BaseDroidDriver {
@Override
protected UiAutomationElement getNewRootElement() {
- return context.getUiElement(getRootNode());
+ return context.getUiElement(getRootNode(), null /* parent */);
}
@Override
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
index cc3832f..7c4b5e8 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
@@ -26,14 +26,26 @@ import android.view.accessibility.AccessibilityNodeInfo;
import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.base.BaseUiElement;
+import com.google.android.droiddriver.finders.Attribute;
import com.google.android.droiddriver.util.Logs;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeoutException;
/**
- * A UiElement that is backed by the UiAutomation object.
+ * A UiElement that is backed by an {@link AccessibilityNodeInfo}. A snapshot of
+ * all attributes is taken at construction. The attributes of a
+ * {@code UiAutomationElement} instance are immutable. If the underlying
+ * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement}
+ * instance will be created in
+ * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
*/
public class UiAutomationElement extends BaseUiElement {
private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
@@ -44,108 +56,76 @@ public class UiAutomationElement extends BaseUiElement {
};
private final UiAutomationContext context;
- private final AccessibilityNodeInfo node;
-
- public UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node) {
+ private final Map<Attribute, Object> attributes;
+ private final boolean visible;
+ private final Rect visibleBounds;
+ private final UiAutomationElement parent;
+ private final List<UiAutomationElement> children;
+
+ public UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
+ UiAutomationElement parent) {
this.context = Preconditions.checkNotNull(context);
- this.node = Preconditions.checkNotNull(node);
- }
-
- @Override
- public String getText() {
- return charSequenceToString(node.getText());
- }
-
- @Override
- public String getContentDescription() {
- return charSequenceToString(node.getContentDescription());
- }
-
- @Override
- public String getClassName() {
- return charSequenceToString(node.getClassName());
- }
-
- @Override
- public String getResourceId() {
- return charSequenceToString(node.getViewIdResourceName());
- }
-
- @Override
- public String getPackageName() {
- return charSequenceToString(node.getPackageName());
- }
-
- @Override
- public InputInjector getInjector() {
- return context.getInjector();
- }
-
- @Override
- public boolean isVisible() {
- return node.isVisibleToUser();
- }
-
- @Override
- public boolean isCheckable() {
- return node.isCheckable();
- }
-
- @Override
- public boolean isChecked() {
- return node.isChecked();
- }
-
- @Override
- public boolean isClickable() {
- return node.isClickable();
- }
-
- @Override
- public boolean isEnabled() {
- return node.isEnabled();
- }
-
- @Override
- public boolean isFocusable() {
- return node.isFocusable();
- }
-
- @Override
- public boolean isFocused() {
- return node.isFocused();
- }
-
- @Override
- public boolean isScrollable() {
- return node.isScrollable();
- }
-
- @Override
- public boolean isLongClickable() {
- return node.isLongClickable();
- }
-
- @Override
- public boolean isPassword() {
- return node.isPassword();
+ Preconditions.checkNotNull(node);
+ this.parent = parent;
+
+ Map<Attribute, Object> attribs = Maps.newEnumMap(Attribute.class);
+ put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName()));
+ put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName()));
+ put(attribs, Attribute.TEXT, charSequenceToString(node.getText()));
+ put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription()));
+ put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName()));
+ put(attribs, Attribute.CHECKABLE, node.isCheckable());
+ put(attribs, Attribute.CHECKED, node.isChecked());
+ put(attribs, Attribute.CLICKABLE, node.isClickable());
+ put(attribs, Attribute.ENABLED, node.isEnabled());
+ put(attribs, Attribute.FOCUSABLE, node.isFocusable());
+ put(attribs, Attribute.FOCUSED, node.isFocused());
+ put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable());
+ put(attribs, Attribute.PASSWORD, node.isPassword());
+ put(attribs, Attribute.SCROLLABLE, node.isScrollable());
+ put(attribs, Attribute.SELECTED, node.isSelected());
+ put(attribs, Attribute.BOUNDS, getBounds(node));
+ attributes = ImmutableMap.copyOf(attribs);
+
+ // Order matters as getVisibleBounds depends on visible
+ visible = node.isVisibleToUser();
+ visibleBounds = getVisibleBounds(node);
+ List<UiAutomationElement> mutableChildren = buildChildren(context, node);
+ this.children = mutableChildren == null ? null : ImmutableList.copyOf(mutableChildren);
+ }
+
+ private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
+ if (value != null) {
+ attribs.put(key, value);
+ }
}
- @Override
- public boolean isSelected() {
- return node.isSelected();
+ private List<UiAutomationElement> buildChildren(UiAutomationContext context,
+ AccessibilityNodeInfo node) {
+ List<UiAutomationElement> children;
+ int childCount = node.getChildCount();
+ if (childCount == 0) {
+ children = null;
+ } else {
+ children = Lists.newArrayListWithExpectedSize(childCount);
+ for (int i = 0; i < childCount; i++) {
+ AccessibilityNodeInfo child = node.getChild(i);
+ if (child != null) {
+ children.add(context.getUiElement(child, this));
+ }
+ }
+ }
+ return children;
}
- @Override
- public Rect getBounds() {
+ private Rect getBounds(AccessibilityNodeInfo node) {
Rect rect = new Rect();
node.getBoundsInScreen(rect);
return rect;
}
- @Override
- public Rect getVisibleBounds() {
- if (!isVisible()) {
+ private Rect getVisibleBounds(AccessibilityNodeInfo node) {
+ if (!visible) {
Logs.log(Log.DEBUG, "Node is invisible: " + node);
return new Rect();
}
@@ -161,20 +141,33 @@ public class UiAutomationElement extends BaseUiElement {
}
@Override
- protected int getChildCount() {
- return node.getChildCount();
+ public Rect getVisibleBounds() {
+ return visibleBounds;
}
@Override
- protected UiAutomationElement getChild(int index) {
- AccessibilityNodeInfo child = node.getChild(index);
- return child == null ? null : context.getUiElement(child);
+ public boolean isVisible() {
+ return visible;
}
@Override
public UiAutomationElement getParent() {
- AccessibilityNodeInfo parent = node.getParent();
- return parent == null ? null : context.getUiElement(parent);
+ return parent;
+ }
+
+ @Override
+ protected List<UiAutomationElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ protected Map<Attribute, Object> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ protected InputInjector getInjector() {
+ return context.getInjector();
}
@Override