aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2014-06-12 14:54:41 -0700
committerKevin Jin <kjin@google.com>2014-06-12 14:54:41 -0700
commit74676fdd3c8a9e599eddd13bea56898674d9916a (patch)
treec97cbf56b25ea186d14672ae612541353015dcad /src
parenta738fe74f57f48dde2dd7a28479bab3f5441dadb (diff)
downloaddroiddriver-74676fdd3c8a9e599eddd13bea56898674d9916a.tar.gz
add Validator interface and DefaultAccessibilityValidator
refactor for cleaner implementation Change-Id: I6ba13c5a46e444806f492bc7de365405fecae0d5
Diffstat (limited to 'src')
-rw-r--r--src/com/google/android/droiddriver/UiElement.java16
-rw-r--r--src/com/google/android/droiddriver/actions/EventAction.java3
-rw-r--r--src/com/google/android/droiddriver/actions/EventUiElementActor.java1
-rw-r--r--src/com/google/android/droiddriver/actions/UiElementActor.java (renamed from src/com/google/android/droiddriver/base/UiElementActor.java)2
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java4
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java2
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java2
-rw-r--r--src/com/google/android/droiddriver/base/BaseDroidDriver.java19
-rw-r--r--src/com/google/android/droiddriver/base/BaseUiDevice.java4
-rw-r--r--src/com/google/android/droiddriver/base/BaseUiElement.java61
-rw-r--r--src/com/google/android/droiddriver/base/DroidDriverContext.java36
-rw-r--r--src/com/google/android/droiddriver/finders/ByXPath.java19
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java80
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java69
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java45
-rw-r--r--src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java54
-rw-r--r--src/com/google/android/droiddriver/instrumentation/ViewElement.java21
-rw-r--r--src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java9
-rw-r--r--src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java (renamed from src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java)20
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java33
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java112
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java193
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java (renamed from src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java)7
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java (renamed from src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java)9
-rw-r--r--src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java53
-rw-r--r--src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java51
-rw-r--r--src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationContext.java97
-rw-r--r--src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationDriver.java131
-rw-r--r--src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationElement.java220
-rw-r--r--src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java34
-rw-r--r--src/com/google/android/droiddriver/validators/Validator.java (renamed from src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java)17
-rw-r--r--src/com/google/android/droiddriver/validators/VisibilityValidator.java (renamed from src/com/google/android/droiddriver/uiautomation/base/UiAutomationCallable.java)16
32 files changed, 629 insertions, 811 deletions
diff --git a/src/com/google/android/droiddriver/UiElement.java b/src/com/google/android/droiddriver/UiElement.java
index fc75e5b..71b43ed 100644
--- a/src/com/google/android/droiddriver/UiElement.java
+++ b/src/com/google/android/droiddriver/UiElement.java
@@ -19,7 +19,7 @@ package com.google.android.droiddriver;
import android.graphics.Rect;
import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
+import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.finders.Attribute;
import com.google.android.droiddriver.finders.Predicate;
import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
@@ -140,7 +140,6 @@ public interface UiElement {
* Executes the given action.
*
* @param action The action to execute
- * @throws ElementNotVisibleException when the element is not visible
* @return true if the action is successful
*/
boolean perform(Action action);
@@ -149,31 +148,24 @@ public interface UiElement {
* Sets the text of this element.
*
* @param text The text to enter.
- * @throws ElementNotVisibleException when the element is not visible
*/
void setText(String text);
/**
* Clicks this element. The click will be at the center of the visible
* element.
- *
- * @throws ElementNotVisibleException when the element is not visible
*/
void click();
/**
* Long-clicks this element. The click will be at the center of the visible
* element.
- *
- * @throws ElementNotVisibleException when the element is not visible
*/
void longClick();
/**
* Double-clicks this element. The click will be at the center of the visible
* element.
- *
- * @throws ElementNotVisibleException when the element is not visible
*/
void doubleClick();
@@ -182,7 +174,6 @@ public interface UiElement {
*
* @param direction specifies where the view port will move, instead of the
* finger.
- * @throws ElementNotVisibleException when the element is not visible
*/
void scroll(PhysicalDirection direction);
@@ -228,4 +219,9 @@ public interface UiElement {
* Gets the parent.
*/
UiElement getParent();
+
+ /**
+ * Gets the {@link InputInjector} for injecting InputEvent.
+ */
+ InputInjector getInjector();
}
diff --git a/src/com/google/android/droiddriver/actions/EventAction.java b/src/com/google/android/droiddriver/actions/EventAction.java
index 71ceb6f..f2736e3 100644
--- a/src/com/google/android/droiddriver/actions/EventAction.java
+++ b/src/com/google/android/droiddriver/actions/EventAction.java
@@ -19,7 +19,6 @@ package com.google.android.droiddriver.actions;
import android.view.InputEvent;
import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.base.BaseUiElement;
/**
* Implements {@link Action} by injecting synthesized events.
@@ -31,7 +30,7 @@ public abstract class EventAction extends BaseAction {
@Override
public boolean perform(UiElement element) {
- return perform(((BaseUiElement) element).getInjector(), element);
+ return perform(element.getInjector(), element);
}
/**
diff --git a/src/com/google/android/droiddriver/actions/EventUiElementActor.java b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
index dbee143..1913e5d 100644
--- a/src/com/google/android/droiddriver/actions/EventUiElementActor.java
+++ b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
@@ -17,7 +17,6 @@
package com.google.android.droiddriver.actions;
import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.base.UiElementActor;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
/**
diff --git a/src/com/google/android/droiddriver/base/UiElementActor.java b/src/com/google/android/droiddriver/actions/UiElementActor.java
index b7ffe47..abca286 100644
--- a/src/com/google/android/droiddriver/base/UiElementActor.java
+++ b/src/com/google/android/droiddriver/actions/UiElementActor.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.base;
+package com.google.android.droiddriver.actions;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
index 4d47eac..5edc131 100644
--- a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
@@ -21,7 +21,7 @@ import android.view.accessibility.AccessibilityNodeInfo;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.Action;
import com.google.android.droiddriver.actions.BaseAction;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationElement;
+import com.google.android.droiddriver.uiautomation.UiAutomationElement;
/**
* Implements {@link Action} via the Accessibility API.
@@ -33,7 +33,7 @@ public abstract class AccessibilityAction extends BaseAction {
@Override
public boolean perform(UiElement element) {
- return perform(((BaseUiAutomationElement<?>) element).getNode(), element);
+ return perform(((UiAutomationElement) element).getRawElement(), element);
}
/**
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
index 4a4033c..0e7cc2b 100644
--- a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
@@ -22,7 +22,7 @@ import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.exceptions.ActionException;
/**
- * An {@link AccessibilityAction} that clicks on an UiElement.
+ * An {@link AccessibilityAction} that clicks on a UiElement.
*/
public abstract class AccessibilityClickAction extends AccessibilityAction {
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
index 491ad7a..dec5979 100644
--- a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
@@ -18,7 +18,7 @@ package com.google.android.droiddriver.actions.accessibility;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.TextAction;
-import com.google.android.droiddriver.base.UiElementActor;
+import com.google.android.droiddriver.actions.UiElementActor;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
/**
diff --git a/src/com/google/android/droiddriver/base/BaseDroidDriver.java b/src/com/google/android/droiddriver/base/BaseDroidDriver.java
index 4776a66..b881b91 100644
--- a/src/com/google/android/droiddriver/base/BaseDroidDriver.java
+++ b/src/com/google/android/droiddriver/base/BaseDroidDriver.java
@@ -19,6 +19,7 @@ package com.google.android.droiddriver.base;
import com.google.android.droiddriver.DroidDriver;
import com.google.android.droiddriver.Poller;
import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.exceptions.ElementNotFoundException;
import com.google.android.droiddriver.exceptions.TimeoutException;
import com.google.android.droiddriver.finders.ByXPath;
@@ -28,10 +29,10 @@ import com.google.android.droiddriver.util.Logs;
/**
* Base DroidDriver that implements the common operations.
*/
-public abstract class BaseDroidDriver implements DroidDriver {
+public abstract class BaseDroidDriver<R, E extends BaseUiElement<R, E>> implements DroidDriver {
private Poller poller = new DefaultPoller();
- private BaseUiElement rootElement;
+ private E rootElement;
@Override
public UiElement find(Finder finder) {
@@ -88,11 +89,16 @@ public abstract class BaseDroidDriver implements DroidDriver {
this.poller = poller;
}
- protected abstract BaseUiElement getNewRootElement();
+ public abstract InputInjector getInjector();
- protected abstract DroidDriverContext getContext();
+ protected abstract E newRootElement();
- protected BaseUiElement getRootElement() {
+ /**
+ * Returns a new UiElement of type {@code E}.
+ */
+ protected abstract E newUiElement(R rawElement, E parent);
+
+ public E getRootElement() {
if (rootElement == null) {
refreshUiElementTree();
}
@@ -101,8 +107,7 @@ public abstract class BaseDroidDriver implements DroidDriver {
@Override
public void refreshUiElementTree() {
- getContext().clearData();
- rootElement = getNewRootElement();
+ rootElement = newRootElement();
}
@Override
diff --git a/src/com/google/android/droiddriver/base/BaseUiDevice.java b/src/com/google/android/droiddriver/base/BaseUiDevice.java
index 21e8aae..2ca501d 100644
--- a/src/com/google/android/droiddriver/base/BaseUiDevice.java
+++ b/src/com/google/android/droiddriver/base/BaseUiDevice.java
@@ -56,7 +56,7 @@ public abstract class BaseUiDevice implements UiDevice {
if (!isScreenOn()) {
// Cannot call perform(POWER_ON) because perform() checks the UiElement is
// visible.
- POWER_ON.perform(getContext().getInjector(), null);
+ POWER_ON.perform(getContext().getDriver().getInjector(), null);
getContext().tryWaitForIdleSync(POWER_ON.getTimeoutMillis());
}
}
@@ -112,5 +112,5 @@ public abstract class BaseUiDevice implements UiDevice {
protected abstract Bitmap takeScreenshot();
- protected abstract DroidDriverContext getContext();
+ protected abstract DroidDriverContext<?, ?> getContext();
}
diff --git a/src/com/google/android/droiddriver/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java
index ba25874..340f649 100644
--- a/src/com/google/android/droiddriver/base/BaseUiElement.java
+++ b/src/com/google/android/droiddriver/base/BaseUiElement.java
@@ -20,9 +20,9 @@ import android.graphics.Rect;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.actions.EventUiElementActor;
+import com.google.android.droiddriver.actions.UiElementActor;
import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
import com.google.android.droiddriver.finders.Attribute;
import com.google.android.droiddriver.finders.Predicate;
import com.google.android.droiddriver.finders.Predicates;
@@ -30,6 +30,7 @@ import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
import com.google.android.droiddriver.util.Logs;
import com.google.android.droiddriver.util.Strings;
import com.google.android.droiddriver.util.Strings.ToStringHelper;
+import com.google.android.droiddriver.validators.Validator;
import java.util.ArrayList;
import java.util.Collections;
@@ -41,18 +42,19 @@ import java.util.concurrent.FutureTask;
/**
* Base UiElement that implements the common operations.
+ *
+ * @param <R> the type of the raw element this class wraps, for example, View or
+ * AccessibilityNodeInfo
+ * @param <E> the type of the concrete subclass of BaseUiElement
*/
-public abstract class BaseUiElement implements UiElement {
+public abstract class BaseUiElement<R, E extends BaseUiElement<R, E>> implements UiElement {
// These two attribute names are used for debugging only.
// The two constants are used internally and must match to-uiautomator.xsl.
public static final String ATTRIB_VISIBLE_BOUNDS = "VisibleBounds";
public static final String ATTRIB_NOT_VISIBLE = "NotVisible";
- private final UiElementActor uiElementActor;
-
- protected BaseUiElement(UiElementActor UiElementActor) {
- this.uiElementActor = UiElementActor;
- }
+ private UiElementActor uiElementActor = EventUiElementActor.INSTANCE;
+ private Validator[] validators = {};
@SuppressWarnings("unchecked")
@Override
@@ -158,15 +160,14 @@ public abstract class BaseUiElement implements UiElement {
return selectionStart >= 0 && selectionStart != selectionEnd;
}
- /**
- * Gets the {@link InputInjector} for injecting InputEvent.
- */
- public abstract InputInjector getInjector();
-
@Override
public boolean perform(Action action) {
Logs.call(this, "perform", action);
- checkVisible();
+ for (Validator validator : validators) {
+ if (!validator.isValid(this)) {
+ throw new DroidDriverException(validator + " failed");
+ }
+ }
return performAndWait(action);
}
@@ -229,17 +230,11 @@ public abstract class BaseUiElement implements UiElement {
protected abstract Map<Attribute, Object> getAttributes();
- protected abstract List<? extends BaseUiElement> getChildren();
-
- private void checkVisible() {
- if (!isVisible()) {
- throw new ElementNotVisibleException(this);
- }
- }
+ protected abstract List<E> getChildren();
@Override
- public List<? extends BaseUiElement> getChildren(Predicate<? super UiElement> predicate) {
- List<? extends BaseUiElement> children = getChildren();
+ public List<E> getChildren(Predicate<? super UiElement> predicate) {
+ List<E> children = getChildren();
if (children == null) {
return Collections.emptyList();
}
@@ -247,8 +242,8 @@ public abstract class BaseUiElement implements UiElement {
return children;
}
- List<BaseUiElement> filteredChildren = new ArrayList<BaseUiElement>(children.size());
- for (BaseUiElement child : children) {
+ List<E> filteredChildren = new ArrayList<E>(children.size());
+ for (E child : children) {
if (predicate.apply(child)) {
filteredChildren.add(child);
}
@@ -283,4 +278,20 @@ public abstract class BaseUiElement implements UiElement {
}
}
}
+
+ /**
+ * Gets the raw element used to create this UiElement. The attributes of this
+ * UiElement are based on a snapshot of the raw element at construction time.
+ * If the raw element is updated later, the attributes may not match.
+ */
+ // TODO: expose in UiElement?
+ public abstract R getRawElement();
+
+ public void setUiElementActor(UiElementActor uiElementActor) {
+ this.uiElementActor = uiElementActor;
+ }
+
+ public void setValidators(Validator[] validators) {
+ this.validators = validators;
+ }
}
diff --git a/src/com/google/android/droiddriver/base/DroidDriverContext.java b/src/com/google/android/droiddriver/base/DroidDriverContext.java
index 1c4b21f..045a0b2 100644
--- a/src/com/google/android/droiddriver/base/DroidDriverContext.java
+++ b/src/com/google/android/droiddriver/base/DroidDriverContext.java
@@ -20,11 +20,13 @@ import android.app.Instrumentation;
import android.os.Looper;
import android.util.Log;
-import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.exceptions.DroidDriverException;
import com.google.android.droiddriver.exceptions.TimeoutException;
+import com.google.android.droiddriver.finders.ByXPath;
import com.google.android.droiddriver.util.Logs;
+import java.util.Map;
+import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
@@ -32,23 +34,45 @@ import java.util.concurrent.TimeUnit;
/**
* Internal helper for DroidDriver implementation.
*/
-public abstract class DroidDriverContext {
+public class DroidDriverContext<R, E extends BaseUiElement<R, E>> {
private final Instrumentation instrumentation;
+ private final BaseDroidDriver<R, E> driver;
+ private final Map<R, E> map;
- protected DroidDriverContext(Instrumentation instrumentation) {
+ public DroidDriverContext(Instrumentation instrumentation, BaseDroidDriver<R, E> driver) {
this.instrumentation = instrumentation;
+ this.driver = driver;
+ map = new WeakHashMap<R, E>();
}
public Instrumentation getInstrumentation() {
return instrumentation;
}
- public abstract BaseDroidDriver getDriver();
+ public BaseDroidDriver<R, E> getDriver() {
+ return driver;
+ }
+
+ public E getElement(R rawElement, E parent) {
+ E element = map.get(rawElement);
+ if (element == null) {
+ element = driver.newUiElement(rawElement, parent);
+ map.put(rawElement, element);
+ }
+ return element;
+ }
- public abstract InputInjector getInjector();
+ public E newRootElement(R rawRoot) {
+ clearData();
+ return getElement(rawRoot, null /* parent */);
+ }
+
+ private void clearData() {
+ map.clear();
+ ByXPath.clearData();
+ }
/** Clears UiElement instances in the context */
- public abstract void clearData();
/**
* Tries to wait for an idle state on the main thread on best-effort basis up
diff --git a/src/com/google/android/droiddriver/finders/ByXPath.java b/src/com/google/android/droiddriver/finders/ByXPath.java
index 30cc251..946261a 100644
--- a/src/com/google/android/droiddriver/finders/ByXPath.java
+++ b/src/com/google/android/droiddriver/finders/ByXPath.java
@@ -56,10 +56,10 @@ public class ByXPath implements Finder {
// on children they are in the same document to be appended.
private static Document document;
// The two maps should be kept in sync
- private static final Map<BaseUiElement, Element> TO_DOM_MAP =
- new HashMap<BaseUiElement, Element>();
- private static final Map<Element, BaseUiElement> FROM_DOM_MAP =
- new HashMap<Element, BaseUiElement>();
+ private static final Map<BaseUiElement<?, ?>, Element> TO_DOM_MAP =
+ new HashMap<BaseUiElement<?, ?>, Element>();
+ private static final Map<Element, BaseUiElement<?, ?>> FROM_DOM_MAP =
+ new HashMap<Element, BaseUiElement<?, ?>>();
public static void clearData() {
TO_DOM_MAP.clear();
@@ -86,7 +86,7 @@ public class ByXPath implements Finder {
@Override
public UiElement find(UiElement context) {
- Element domNode = getDomNode((BaseUiElement) context, UiElement.VISIBLE);
+ Element domNode = getDomNode((BaseUiElement<?, ?>) context, UiElement.VISIBLE);
try {
getDocument().appendChild(domNode);
Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE);
@@ -124,7 +124,8 @@ public class ByXPath implements Finder {
/**
* Returns the DOM node representing this UiElement.
*/
- private static Element getDomNode(BaseUiElement uiElement, Predicate<? super UiElement> predicate) {
+ private static Element getDomNode(BaseUiElement<?, ?> uiElement,
+ Predicate<? super UiElement> predicate) {
Element domNode = TO_DOM_MAP.get(uiElement);
if (domNode == null) {
domNode = buildDomNode(uiElement, predicate);
@@ -132,7 +133,7 @@ public class ByXPath implements Finder {
return domNode;
}
- private static Element buildDomNode(BaseUiElement uiElement,
+ private static Element buildDomNode(BaseUiElement<?, ?> uiElement,
Predicate<? super UiElement> predicate) {
String className = uiElement.getClassName();
if (className == null) {
@@ -175,7 +176,7 @@ public class ByXPath implements Finder {
}
}
- for (BaseUiElement child : uiElement.getChildren(predicate)) {
+ for (BaseUiElement<?, ?> child : uiElement.getChildren(predicate)) {
element.appendChild(getDomNode(child, predicate));
}
return element;
@@ -194,7 +195,7 @@ public class ByXPath implements Finder {
}
}
- public static boolean dumpDom(String path, BaseUiElement uiElement) {
+ public static boolean dumpDom(String path, BaseUiElement<?, ?> uiElement) {
BufferedOutputStream bos = null;
try {
bos = FileUtils.open(path);
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
deleted file mode 100644
index 52e423a..0000000
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.instrumentation;
-
-import android.app.Instrumentation;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.google.android.droiddriver.actions.InputInjector;
-import com.google.android.droiddriver.base.DroidDriverContext;
-import com.google.android.droiddriver.exceptions.ActionException;
-import com.google.android.droiddriver.finders.ByXPath;
-
-import java.util.Map;
-import java.util.WeakHashMap;
-
-class InstrumentationContext extends DroidDriverContext {
- private final Map<View, ViewElement> map = new WeakHashMap<View, ViewElement>();
- private final InstrumentationDriver driver;
- private final InputInjector injector;
-
- InstrumentationContext(final Instrumentation instrumentation, InstrumentationDriver driver) {
- super(instrumentation);
- this.driver = driver;
- this.injector = new InputInjector() {
- @Override
- public boolean injectInputEvent(InputEvent event) {
- if (event instanceof MotionEvent) {
- instrumentation.sendPointerSync((MotionEvent) event);
- } else if (event instanceof KeyEvent) {
- instrumentation.sendKeySync((KeyEvent) event);
- } else {
- throw new ActionException("Unknown input event type: " + event);
- }
- return true;
- }
- };
- }
-
- @Override
- public InstrumentationDriver getDriver() {
- return driver;
- }
-
- @Override
- public InputInjector getInjector() {
- return injector;
- }
-
- ViewElement getUiElement(View view, ViewElement parent) {
- ViewElement element = map.get(view);
- if (element == null) {
- element = new ViewElement(this, view, parent);
- map.put(view, element);
- }
- return element;
- }
-
- @Override
- public void clearData() {
- map.clear();
- ByXPath.clearData();
- }
-}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
index adf14e2..8804211 100644
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
@@ -17,16 +17,13 @@
package com.google.android.droiddriver.instrumentation;
import android.app.Instrumentation;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.RectF;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
+import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.base.BaseDroidDriver;
+import com.google.android.droiddriver.base.DroidDriverContext;
import com.google.android.droiddriver.exceptions.DroidDriverException;
import com.google.android.droiddriver.exceptions.TimeoutException;
import com.google.android.droiddriver.util.ActivityUtils;
@@ -35,23 +32,30 @@ import com.google.android.droiddriver.util.Logs;
/**
* Implementation of DroidDriver that is driven via instrumentation.
*/
-public class InstrumentationDriver extends BaseDroidDriver {
- private final InstrumentationContext context;
+public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> {
+ private final DroidDriverContext<View, ViewElement> context;
+ private final InputInjector injector;
private final InstrumentationUiDevice uiDevice;
public InstrumentationDriver(Instrumentation instrumentation) {
- this.context = new InstrumentationContext(instrumentation, this);
+ context = new DroidDriverContext<View, ViewElement>(instrumentation, this);
+ injector = new InstrumentationInputInjector(instrumentation);
uiDevice = new InstrumentationUiDevice(context);
}
@Override
- protected ViewElement getNewRootElement() {
- return context.getUiElement(findRootView(), null /* parent */);
+ public InputInjector getInjector() {
+ return injector;
}
@Override
- protected InstrumentationContext getContext() {
- return context;
+ protected ViewElement newRootElement() {
+ return context.newRootElement(findRootView());
+ }
+
+ @Override
+ protected ViewElement newUiElement(View rawElement, ViewElement parent) {
+ return new ViewElement(context, rawElement, parent);
}
private static class FindRootViewRunnable implements Runnable {
@@ -106,47 +110,6 @@ public class InstrumentationDriver extends BaseDroidDriver {
}
}
- private static class ScreenshotRunnable implements Runnable {
- private final View rootView;
- Bitmap screenshot;
-
- private ScreenshotRunnable(View rootView) {
- this.rootView = rootView;
- }
-
- @Override
- public void run() {
- try {
- rootView.destroyDrawingCache();
- rootView.buildDrawingCache(false);
- Bitmap drawingCache = rootView.getDrawingCache();
- int[] xy = new int[2];
- rootView.getLocationOnScreen(xy);
- if (xy[0] == 0 && xy[1] == 0) {
- screenshot = Bitmap.createBitmap(drawingCache);
- } else {
- Canvas canvas = new Canvas();
- Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
- rect.offset(xy[0], xy[1]);
- screenshot =
- Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
- canvas.setBitmap(screenshot);
- canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
- canvas.setBitmap(null);
- }
- rootView.destroyDrawingCache();
- } catch (Throwable e) {
- Logs.log(Log.ERROR, e);
- }
- }
- }
-
- Bitmap takeScreenshot() {
- ScreenshotRunnable screenshotRunnable = new ScreenshotRunnable(findRootView());
- context.runOnMainSync(screenshotRunnable);
- return screenshotRunnable.screenshot;
- }
-
@Override
public InstrumentationUiDevice getUiDevice() {
return uiDevice;
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java
new file mode 100644
index 0000000..7e47e90
--- /dev/null
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.google.android.droiddriver.instrumentation;
+
+import android.app.Instrumentation;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.exceptions.ActionException;
+
+public class InstrumentationInputInjector implements InputInjector {
+ private final Instrumentation instrumentation;
+
+ public InstrumentationInputInjector(Instrumentation instrumentation) {
+ this.instrumentation = instrumentation;
+ }
+
+ @Override
+ public boolean injectInputEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ instrumentation.sendPointerSync((MotionEvent) event);
+ } else if (event instanceof KeyEvent) {
+ instrumentation.sendKeySync((KeyEvent) event);
+ } else {
+ throw new ActionException("Unknown input event type: " + event);
+ }
+ return true;
+ }
+}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java
index a415918..f94137f 100644
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java
@@ -17,23 +17,69 @@
package com.google.android.droiddriver.instrumentation;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Bitmap.Config;
+import android.util.Log;
+import android.view.View;
import com.google.android.droiddriver.base.BaseUiDevice;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.util.Logs;
class InstrumentationUiDevice extends BaseUiDevice {
- private final InstrumentationContext context;
+ private final DroidDriverContext<View, ViewElement> context;
- InstrumentationUiDevice(InstrumentationContext context) {
+ InstrumentationUiDevice(DroidDriverContext<View, ViewElement> context) {
this.context = context;
}
@Override
protected Bitmap takeScreenshot() {
- return context.getDriver().takeScreenshot();
+ ScreenshotRunnable screenshotRunnable =
+ new ScreenshotRunnable(context.getDriver().getRootElement().getRawElement());
+ context.runOnMainSync(screenshotRunnable);
+ return screenshotRunnable.screenshot;
}
@Override
- protected InstrumentationContext getContext() {
+ protected DroidDriverContext<View, ViewElement> getContext() {
return context;
}
+
+ private static class ScreenshotRunnable implements Runnable {
+ private final View rootView;
+ Bitmap screenshot;
+
+ private ScreenshotRunnable(View rootView) {
+ this.rootView = rootView;
+ }
+
+ @Override
+ public void run() {
+ try {
+ rootView.destroyDrawingCache();
+ rootView.buildDrawingCache(false);
+ Bitmap drawingCache = rootView.getDrawingCache();
+ int[] xy = new int[2];
+ rootView.getLocationOnScreen(xy);
+ if (xy[0] == 0 && xy[1] == 0) {
+ screenshot = Bitmap.createBitmap(drawingCache);
+ } else {
+ Canvas canvas = new Canvas();
+ Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
+ rect.offset(xy[0], xy[1]);
+ screenshot =
+ Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
+ canvas.setBitmap(screenshot);
+ canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
+ canvas.setBitmap(null);
+ }
+ rootView.destroyDrawingCache();
+ } catch (Throwable e) {
+ Logs.log(Log.ERROR, e);
+ }
+ }
+ }
}
diff --git a/src/com/google/android/droiddriver/instrumentation/ViewElement.java b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
index fd4c39a..d1379cd 100644
--- a/src/com/google/android/droiddriver/instrumentation/ViewElement.java
+++ b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
@@ -26,9 +26,9 @@ import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.TextView;
-import com.google.android.droiddriver.actions.EventUiElementActor;
import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.base.BaseUiElement;
+import com.google.android.droiddriver.base.DroidDriverContext;
import com.google.android.droiddriver.exceptions.DroidDriverException;
import com.google.android.droiddriver.finders.Attribute;
import com.google.android.droiddriver.util.Preconditions;
@@ -44,7 +44,7 @@ import java.util.concurrent.FutureTask;
/**
* A UiElement that is backed by a View.
*/
-public class ViewElement extends BaseUiElement {
+public class ViewElement extends BaseUiElement<View, ViewElement> {
private static class SnapshotViewAttributesRunnable implements Runnable {
private final View view;
final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
@@ -205,7 +205,8 @@ public class ViewElement extends BaseUiElement {
CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName);
}
- private final InstrumentationContext context;
+ private final DroidDriverContext<View, ViewElement> context;
+ private final View view;
private final Map<Attribute, Object> attributes;
private final boolean visible;
private final Rect visibleBounds;
@@ -218,10 +219,9 @@ public class ViewElement extends BaseUiElement {
* updated, a new {@code ViewElement} instance will be created in
* {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
*/
- public ViewElement(final InstrumentationContext context, View view, ViewElement parent) {
- super(EventUiElementActor.INSTANCE);
+ public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) {
this.context = Preconditions.checkNotNull(context);
- Preconditions.checkNotNull(view);
+ this.view = Preconditions.checkNotNull(view);
this.parent = parent;
SnapshotViewAttributesRunnable attributesSnapshot = new SnapshotViewAttributesRunnable(view);
context.runOnMainSync(attributesSnapshot);
@@ -237,7 +237,7 @@ public class ViewElement extends BaseUiElement {
} else {
List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size());
for (View childView : attributesSnapshot.childViews) {
- children.add(context.getUiElement(childView, this));
+ children.add(context.getElement(childView, this));
}
this.children = Collections.unmodifiableList(children);
}
@@ -270,7 +270,7 @@ public class ViewElement extends BaseUiElement {
@Override
public InputInjector getInjector() {
- return context.getInjector();
+ return context.getDriver().getInjector();
}
@Override
@@ -278,4 +278,9 @@ public class ViewElement extends BaseUiElement {
futureTask.run();
context.tryWaitForIdleSync(timeoutMillis);
}
+
+ @Override
+ public View getRawElement() {
+ return view;
+ }
}
diff --git a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
index 6911fa6..fee4e2b 100644
--- a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
@@ -159,7 +159,7 @@ public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy
@Override
public String toString() {
- return String.format("AccessibilityEventScrollStepStrategy{scrollEventTimeoutMillis=%d}",
+ return String.format("%s{scrollEventTimeoutMillis=%d}", getClass().getSimpleName(),
scrollEventTimeoutMillis);
}
@@ -194,9 +194,10 @@ public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy
@Override
public void doScroll(final UiElement container, final PhysicalDirection direction) {
- // We do not call container.scroll(direction) because container.scroll internally calls
- // UiAutomation.executeAndWaitForEvent which clears the AccessibilityEvent Queue, preventing us
- // from fetching the last accessibility event to determine if scrolling has finished.
+ // We do not call container.scroll(direction) because container.scroll
+ // internally calls UiAutomation.executeAndWaitForEvent which clears the
+ // AccessibilityEvent Queue, preventing us from fetching the last
+ // accessibility event to determine if scrolling has finished.
SwipeAction.toScroll(direction).perform(container);
}
diff --git a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
index 2f2295c..63f6a59 100644
--- a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
@@ -14,23 +14,29 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.uiautomation.accessibility;
+package com.google.android.droiddriver.uiautomation;
import android.app.Instrumentation;
+import android.view.accessibility.AccessibilityNodeInfo;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationDriver;
+import com.google.android.droiddriver.validators.DefaultAccessibilityValidator;
+import com.google.android.droiddriver.validators.Validator;
/**
- * Implementation of DroidDriver that gets attributes via the Accessibility API
- * and is acted upon via the Accessibility API.
+ * A UiAutomationDriver that validates accessibility.
*/
-public class AccessibilityDriver extends BaseUiAutomationDriver<AccessibilityElement> {
+public class AccessibilityDriver extends UiAutomationDriver {
+ private static final Validator[] VALIDATORS = {new DefaultAccessibilityValidator()};
+
public AccessibilityDriver(Instrumentation instrumentation) {
super(instrumentation);
}
@Override
- protected AccessibilityContext newContext(Instrumentation instrumentation) {
- return new AccessibilityContext(instrumentation, this);
+ protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
+ UiAutomationElement parent) {
+ UiAutomationElement newUiElement = super.newUiElement(rawElement, parent);
+ newUiElement.setValidators(VALIDATORS);
+ return newUiElement;
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
index 04df113..2cdcedf 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
@@ -17,17 +17,40 @@
package com.google.android.droiddriver.uiautomation;
import android.app.Instrumentation;
+import android.app.UiAutomation;
import android.view.accessibility.AccessibilityNodeInfo;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationContext;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.exceptions.UnrecoverableException;
-class UiAutomationContext extends BaseUiAutomationContext<UiAutomationElement> {
- UiAutomationContext(Instrumentation instrumentation, UiAutomationDriver driver) {
+public class UiAutomationContext extends
+ DroidDriverContext<AccessibilityNodeInfo, UiAutomationElement> {
+ private final UiAutomation uiAutomation;
+
+ protected UiAutomationContext(Instrumentation instrumentation, UiAutomationDriver driver) {
super(instrumentation, driver);
+ this.uiAutomation = instrumentation.getUiAutomation();
}
@Override
- protected UiAutomationElement newUiElement(AccessibilityNodeInfo node, UiAutomationElement parent) {
- return new UiAutomationElement(this, node, parent);
+ public UiAutomationDriver getDriver() {
+ return (UiAutomationDriver) super.getDriver();
+ }
+
+ public interface UiAutomationCallable<T> {
+ T call(UiAutomation uiAutomation);
+ }
+
+ /**
+ * Wraps calls to UiAutomation API. Currently supports fail-fast if
+ * UiAutomation throws IllegalStateException, which occurs when the connection
+ * to UiAutomation service is lost.
+ */
+ public <T> T callUiAutomation(UiAutomationCallable<T> uiAutomationCallable) {
+ try {
+ return uiAutomationCallable.call(uiAutomation);
+ } catch (IllegalStateException e) {
+ throw new UnrecoverableException(e);
+ }
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
index 9291f13..f84ab5f 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
@@ -17,20 +17,122 @@
package com.google.android.droiddriver.uiautomation;
import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationDriver;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.base.BaseDroidDriver;
+import com.google.android.droiddriver.exceptions.TimeoutException;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
+import com.google.android.droiddriver.util.Logs;
/**
* Implementation of DroidDriver that gets attributes via the Accessibility API
* and is acted upon via synthesized events.
*/
-public class UiAutomationDriver extends BaseUiAutomationDriver<UiAutomationElement> {
+public class UiAutomationDriver extends BaseDroidDriver<AccessibilityNodeInfo, UiAutomationElement> {
+ // TODO: magic const from UiAutomator, but may not be useful
+ /**
+ * This value has the greatest bearing on the appearance of test execution
+ * speeds. This value is used as the minimum time to wait before considering
+ * the UI idle after each action.
+ */
+ private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;// ms
+
+ private final UiAutomationContext context;
+ private final InputInjector injector;
+ private final UiAutomationUiDevice uiDevice;
+
public UiAutomationDriver(Instrumentation instrumentation) {
- super(instrumentation);
+ context = new UiAutomationContext(instrumentation, this);
+ injector = new UiAutomationInputInjector(context);
+ uiDevice = new UiAutomationUiDevice(context);
+ }
+
+ @Override
+ public InputInjector getInjector() {
+ return injector;
+ }
+
+ @Override
+ protected UiAutomationElement newRootElement() {
+ return context.newRootElement(getRootNode());
+ }
+
+ @Override
+ protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
+ UiAutomationElement parent) {
+ return new UiAutomationElement(context, rawElement, parent);
+ }
+
+ private AccessibilityNodeInfo getRootNode() {
+ final long timeoutMillis = getPoller().getTimeoutMillis();
+ context.callUiAutomation(new UiAutomationCallable<Void>() {
+ @Override
+ public Void call(UiAutomation uiAutomation) {
+ try {
+ uiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeoutMillis);
+ return null;
+ } catch (java.util.concurrent.TimeoutException e) {
+ throw new TimeoutException(e);
+ }
+ }
+ });
+
+ long end = SystemClock.uptimeMillis() + timeoutMillis;
+ while (true) {
+ AccessibilityNodeInfo root =
+ context.callUiAutomation(new UiAutomationCallable<AccessibilityNodeInfo>() {
+ @Override
+ public AccessibilityNodeInfo call(UiAutomation uiAutomation) {
+ return uiAutomation.getRootInActiveWindow();
+ }
+ });
+ if (root != null) {
+ return root;
+ }
+ long remainingMillis = end - SystemClock.uptimeMillis();
+ if (remainingMillis < 0) {
+ throw new TimeoutException(
+ String.format("Timed out after %d milliseconds waiting for root AccessibilityNodeInfo",
+ timeoutMillis));
+ }
+ SystemClock.sleep(Math.min(250, remainingMillis));
+ }
+ }
+
+ /**
+ * Some widgets fail to trigger some AccessibilityEvent's after actions,
+ * resulting in stale AccessibilityNodeInfo's. As a work-around, force to
+ * clear the AccessibilityNodeInfoCache.
+ */
+ public void clearAccessibilityNodeInfoCache() {
+ Logs.call(this, "clearAccessibilityNodeInfoCache");
+ uiDevice.sleep();
+ uiDevice.wakeUp();
+ }
+
+ /**
+ * {@link #clearAccessibilityNodeInfoCache} causes the screen to blink. This
+ * method clears the cache without blinking by employing an implementation
+ * detail of AccessibilityNodeInfoCache. This is a hack; use it at your own
+ * discretion.
+ */
+ public void clearAccessibilityNodeInfoCacheHack() {
+ Logs.call(this, "clearAccessibilityNodeInfoCacheHack");
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getInstrumentation().getTargetContext()
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ accessibilityManager.sendAccessibilityEvent(AccessibilityEvent
+ .obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED));
}
@Override
- protected UiAutomationContext newContext(Instrumentation instrumentation) {
- return new UiAutomationContext(instrumentation, this);
+ public UiAutomationUiDevice getUiDevice() {
+ return uiDevice;
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
index 9a4309b..3fa8653 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
@@ -16,17 +16,198 @@
package com.google.android.droiddriver.uiautomation;
+import static com.google.android.droiddriver.util.Strings.charSequenceToString;
+
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.graphics.Rect;
+import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
-import com.google.android.droiddriver.actions.EventUiElementActor;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationElement;
+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.uiautomation.UiAutomationContext.UiAutomationCallable;
+import com.google.android.droiddriver.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeoutException;
/**
- * A BaseUiAutomationElement that is acted upon via synthesized events.
+ * A UiElement that gets attributes via the Accessibility API.
*/
-class UiAutomationElement extends BaseUiAutomationElement<UiAutomationElement> {
- UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
+public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, UiAutomationElement> {
+ private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
+ @Override
+ public boolean accept(AccessibilityEvent arg0) {
+ return true;
+ }
+ };
+
+ private final AccessibilityNodeInfo node;
+ private final UiAutomationContext context;
+ private final Map<Attribute, Object> attributes;
+ private final boolean visible;
+ private final Rect visibleBounds;
+ private final UiAutomationElement parent;
+ private final List<UiAutomationElement> children;
+
+ /**
+ * 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}.
+ */
+ protected UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
UiAutomationElement parent) {
- super(context, node, parent, EventUiElementActor.INSTANCE);
+ this.node = Preconditions.checkNotNull(node);
+ this.context = Preconditions.checkNotNull(context);
+ this.parent = parent;
+
+ Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(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());
+ if (node.getTextSelectionStart() >= 0
+ && node.getTextSelectionStart() != node.getTextSelectionEnd()) {
+ attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart());
+ attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd());
+ }
+ put(attribs, Attribute.SELECTED, node.isSelected());
+ put(attribs, Attribute.BOUNDS, getBounds(node));
+ attributes = Collections.unmodifiableMap(attribs);
+
+ // Order matters as getVisibleBounds depends on visible
+ visible = node.isVisibleToUser();
+ visibleBounds = getVisibleBounds(node);
+ List<UiAutomationElement> mutableChildren = buildChildren(node);
+ this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren);
+ }
+
+ private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
+ if (value != null) {
+ attribs.put(key, value);
+ }
+ }
+
+ private List<UiAutomationElement> buildChildren(AccessibilityNodeInfo node) {
+ List<UiAutomationElement> children;
+ int childCount = node.getChildCount();
+ if (childCount == 0) {
+ children = null;
+ } else {
+ children = new ArrayList<UiAutomationElement>(childCount);
+ for (int i = 0; i < childCount; i++) {
+ AccessibilityNodeInfo child = node.getChild(i);
+ if (child != null) {
+ children.add(context.getElement(child, this));
+ }
+ }
+ }
+ return children;
+ }
+
+ private Rect getBounds(AccessibilityNodeInfo node) {
+ Rect rect = new Rect();
+ node.getBoundsInScreen(rect);
+ return rect;
+ }
+
+ private Rect getVisibleBounds(AccessibilityNodeInfo node) {
+ if (!visible) {
+ return new Rect();
+ }
+ Rect visibleBounds = getBounds();
+ UiAutomationElement parent = getParent();
+ Rect parentBounds;
+ while (parent != null) {
+ parentBounds = parent.getBounds();
+ visibleBounds.intersect(parentBounds);
+ parent = parent.getParent();
+ }
+ return visibleBounds;
+ }
+
+ @Override
+ public Rect getVisibleBounds() {
+ return visibleBounds;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return visible;
+ }
+
+ @Override
+ public UiAutomationElement getParent() {
+ return parent;
+ }
+
+ @Override
+ protected List<UiAutomationElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ protected Map<Attribute, Object> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public InputInjector getInjector() {
+ return context.getDriver().getInjector();
+ }
+
+ /**
+ * Note: This implementation of {@code doPerformAndWait} clears the
+ * {@code AccessibilityEvent} queue.
+ */
+ @Override
+ protected void doPerformAndWait(final FutureTask<Boolean> futureTask, final long timeoutMillis) {
+ context.callUiAutomation(new UiAutomationCallable<Void>() {
+
+ @Override
+ public Void call(UiAutomation uiAutomation) {
+ try {
+ uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis);
+ } catch (TimeoutException e) {
+ // This is for sync'ing with Accessibility API on best-effort because
+ // it is not reliable.
+ // Exception is ignored here. Tests will fail anyways if this is
+ // critical.
+ // Actions should usually trigger some AccessibilityEvent's, but some
+ // widgets fail to do so, resulting in stale AccessibilityNodeInfo's.
+ // As a work-around, force to clear the AccessibilityNodeInfoCache.
+ // A legitimate case of no AccessibilityEvent is when scrolling has
+ // reached the end, but we cannot tell whether it's legitimate or the
+ // widget has bugs, so clearAccessibilityNodeInfoCache anyways.
+ context.getDriver().clearAccessibilityNodeInfoCacheHack();
+ }
+ return null;
+ }
+
+ });
+ }
+
+ @Override
+ public AccessibilityNodeInfo getRawElement() {
+ return node;
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java
index 5d13d2d..94d3ab4 100644
--- a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java
@@ -14,17 +14,18 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.uiautomation.base;
+package com.google.android.droiddriver.uiautomation;
import android.app.UiAutomation;
import android.view.InputEvent;
import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
public class UiAutomationInputInjector implements InputInjector {
- private final BaseUiAutomationContext<?> context;
+ private final UiAutomationContext context;
- public UiAutomationInputInjector(BaseUiAutomationContext<?> context) {
+ public UiAutomationInputInjector(UiAutomationContext context) {
this.context = context;
}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java
index 5c66ef5..a376cb6 100644
--- a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.uiautomation.base;
+package com.google.android.droiddriver.uiautomation;
import android.app.UiAutomation;
import android.graphics.Bitmap;
@@ -22,12 +22,13 @@ import android.util.Log;
import com.google.android.droiddriver.base.BaseUiDevice;
import com.google.android.droiddriver.exceptions.UnrecoverableException;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
import com.google.android.droiddriver.util.Logs;
class UiAutomationUiDevice extends BaseUiDevice {
- private final BaseUiAutomationContext<?> context;
+ private final UiAutomationContext context;
- UiAutomationUiDevice(BaseUiAutomationContext<?> context) {
+ UiAutomationUiDevice(UiAutomationContext context) {
this.context = context;
}
@@ -49,7 +50,7 @@ class UiAutomationUiDevice extends BaseUiDevice {
}
@Override
- protected BaseUiAutomationContext<?> getContext() {
+ protected UiAutomationContext getContext() {
return context;
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java
deleted file mode 100644
index 4c76d29..0000000
--- a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.uiautomation.accessibility;
-
-import android.app.Instrumentation;
-import android.view.InputEvent;
-import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.google.android.droiddriver.actions.InputInjector;
-import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationContext;
-import com.google.android.droiddriver.uiautomation.base.UiAutomationInputInjector;
-
-class AccessibilityContext extends BaseUiAutomationContext<AccessibilityElement> {
- AccessibilityContext(Instrumentation instrumentation, AccessibilityDriver driver) {
- super(instrumentation, driver);
- }
-
- @Override
- protected InputInjector newInputInjector() {
- return new UiAutomationInputInjector(this) {
- @Override
- public boolean injectInputEvent(InputEvent event) {
- if (event instanceof MotionEvent) {
- throw new DroidDriverException(
- "AccessibilityDriver forbids MotionEvent in order to detect accessibility issues");
- }
- return super.injectInputEvent(event);
- }
- };
- }
-
- @Override
- protected AccessibilityElement newUiElement(AccessibilityNodeInfo node,
- AccessibilityElement parent) {
- return new AccessibilityElement(this, node, parent);
- }
-}
diff --git a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java
deleted file mode 100644
index c208730..0000000
--- a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.uiautomation.accessibility;
-
-import android.text.TextUtils;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.accessibility.AccessibilityUiElementActor;
-import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationElement;
-
-/**
- * A UiElement that gets attributes via the Accessibility API and is acted upon
- * via the Accessibility API.
- */
-class AccessibilityElement extends BaseUiAutomationElement<AccessibilityElement> {
- AccessibilityElement(AccessibilityContext context, AccessibilityNodeInfo node,
- AccessibilityElement parent) {
- super(context, node, parent, AccessibilityUiElementActor.INSTANCE);
- }
-
- @Override
- public boolean perform(Action action) {
- checkAccessible();
- return super.perform(action);
- }
-
- private void checkAccessible() {
- if (getParent() != null // don't check root
- && TextUtils.isEmpty(this.getContentDescription()) && TextUtils.isEmpty(this.getText())) {
- throw new DroidDriverException(
- "Accessibility issue: either content description or text must be set for actionable"
- + " user interface controls");
- }
- }
-}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationContext.java
deleted file mode 100644
index 1412818..0000000
--- a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationContext.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.uiautomation.base;
-
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.google.android.droiddriver.actions.InputInjector;
-import com.google.android.droiddriver.base.DroidDriverContext;
-import com.google.android.droiddriver.exceptions.UnrecoverableException;
-import com.google.android.droiddriver.finders.ByXPath;
-
-import java.util.Map;
-import java.util.WeakHashMap;
-
-public abstract class BaseUiAutomationContext<E extends BaseUiAutomationElement<E>> extends
- DroidDriverContext {
- private final UiAutomation uiAutomation;
- private final BaseUiAutomationDriver<E> driver;
- private final InputInjector injector;
- private final Map<AccessibilityNodeInfo, E> map;
-
- protected BaseUiAutomationContext(Instrumentation instrumentation,
- BaseUiAutomationDriver<E> driver) {
- super(instrumentation);
- this.uiAutomation = instrumentation.getUiAutomation();
- this.driver = driver;
- this.map = new WeakHashMap<AccessibilityNodeInfo, E>();
- injector = newInputInjector();
- }
-
- /**
- * Subclasses can override to return a different InputInjector, for example,
- * forbidding MotionEvent in order to detect accessibility issues.
- */
- protected InputInjector newInputInjector() {
- return new UiAutomationInputInjector(this);
- }
-
- /**
- * Returns a new UiElement of type {@code E}.
- */
- protected abstract E newUiElement(AccessibilityNodeInfo node, E parent);
-
- @Override
- public BaseUiAutomationDriver<E> getDriver() {
- return driver;
- }
-
- @Override
- public InputInjector getInjector() {
- return injector;
- }
-
- E getUiElement(AccessibilityNodeInfo node, E parent) {
- E element = map.get(node);
- if (element == null) {
- element = newUiElement(node, parent);
- map.put(node, element);
- }
- return element;
- }
-
- @Override
- public void clearData() {
- map.clear();
- ByXPath.clearData();
- }
-
- /**
- * Wraps calls to UiAutomation API. Currently supports fail-fast if
- * UiAutomation throws IllegalStateException, which occurs when the connection
- * to UiAutomation service is lost.
- */
- public <T> T callUiAutomation(UiAutomationCallable<T> uiAutomationCallable) {
- try {
- return uiAutomationCallable.call(uiAutomation);
- } catch (IllegalStateException e) {
- throw new UnrecoverableException(e);
- }
- }
-}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationDriver.java
deleted file mode 100644
index fe223e3..0000000
--- a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationDriver.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.uiautomation.base;
-
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.os.SystemClock;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.google.android.droiddriver.base.BaseDroidDriver;
-import com.google.android.droiddriver.exceptions.TimeoutException;
-import com.google.android.droiddriver.util.Logs;
-
-/**
- * Base implementation of DroidDriver that gets attributes via the Accessibility
- * API.
- */
-public abstract class BaseUiAutomationDriver<E extends BaseUiAutomationElement<E>> extends
- BaseDroidDriver {
- // TODO: magic const from UiAutomator, but may not be useful
- /**
- * This value has the greatest bearing on the appearance of test execution
- * speeds. This value is used as the minimum time to wait before considering
- * the UI idle after each action.
- */
- private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;// ms
-
- private final BaseUiAutomationContext<E> context;
- private final UiAutomationUiDevice uiDevice;
-
- protected BaseUiAutomationDriver(Instrumentation instrumentation) {
- this.context = newContext(instrumentation);
- this.uiDevice = new UiAutomationUiDevice(context);
- }
-
- protected abstract BaseUiAutomationContext<E> newContext(Instrumentation instrumentation);
-
- @Override
- protected E getNewRootElement() {
- return context.getUiElement(getRootNode(), null /* parent */);
- }
-
- @Override
- protected BaseUiAutomationContext<E> getContext() {
- return context;
- }
-
- private AccessibilityNodeInfo getRootNode() {
- final long timeoutMillis = getPoller().getTimeoutMillis();
- context.callUiAutomation(new UiAutomationCallable<Void>() {
- @Override
- public Void call(UiAutomation uiAutomation) {
- try {
- uiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeoutMillis);
- return null;
- } catch (java.util.concurrent.TimeoutException e) {
- throw new TimeoutException(e);
- }
- }
- });
-
- long end = SystemClock.uptimeMillis() + timeoutMillis;
- while (true) {
- AccessibilityNodeInfo root =
- context.callUiAutomation(new UiAutomationCallable<AccessibilityNodeInfo>() {
- @Override
- public AccessibilityNodeInfo call(UiAutomation uiAutomation) {
- return uiAutomation.getRootInActiveWindow();
- }
- });
- if (root != null) {
- return root;
- }
- long remainingMillis = end - SystemClock.uptimeMillis();
- if (remainingMillis < 0) {
- throw new TimeoutException(
- String.format("Timed out after %d milliseconds waiting for root AccessibilityNodeInfo",
- timeoutMillis));
- }
- SystemClock.sleep(Math.min(250, remainingMillis));
- }
- }
-
- /**
- * Some widgets fail to trigger some AccessibilityEvent's after actions,
- * resulting in stale AccessibilityNodeInfo's. As a work-around, force to
- * clear the AccessibilityNodeInfoCache.
- */
- public void clearAccessibilityNodeInfoCache() {
- Logs.call(this, "clearAccessibilityNodeInfoCache");
- uiDevice.sleep();
- uiDevice.wakeUp();
- }
-
- /**
- * {@link #clearAccessibilityNodeInfoCache} causes the screen to blink. This
- * method clears the cache without blinking by employing an implementation
- * detail of AccessibilityNodeInfoCache. This is a hack; use it at your own
- * discretion.
- */
- public void clearAccessibilityNodeInfoCacheHack() {
- Logs.call(this, "clearAccessibilityNodeInfoCacheHack");
- AccessibilityManager accessibilityManager =
- (AccessibilityManager) context.getInstrumentation().getTargetContext()
- .getSystemService(Context.ACCESSIBILITY_SERVICE);
- accessibilityManager.sendAccessibilityEvent(AccessibilityEvent
- .obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED));
- }
-
- @Override
- public UiAutomationUiDevice getUiDevice() {
- return uiDevice;
- }
-}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationElement.java
deleted file mode 100644
index a27a3eb..0000000
--- a/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationElement.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright (C) 2013 DroidDriver committers
- *
- * 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.google.android.droiddriver.uiautomation.base;
-
-import static com.google.android.droiddriver.util.Strings.charSequenceToString;
-
-import android.app.UiAutomation;
-import android.app.UiAutomation.AccessibilityEventFilter;
-import android.graphics.Rect;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.google.android.droiddriver.actions.InputInjector;
-import com.google.android.droiddriver.base.BaseUiElement;
-import com.google.android.droiddriver.base.UiElementActor;
-import com.google.android.droiddriver.finders.Attribute;
-import com.google.android.droiddriver.util.Preconditions;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeoutException;
-
-/**
- * A UiElement that gets attributes via the Accessibility API.
- */
-public class BaseUiAutomationElement<E extends BaseUiAutomationElement<E>> extends BaseUiElement {
- private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
- @Override
- public boolean accept(AccessibilityEvent arg0) {
- return true;
- }
- };
-
- private final AccessibilityNodeInfo node;
- private final BaseUiAutomationContext<E> context;
- private final Map<Attribute, Object> attributes;
- private final boolean visible;
- private final Rect visibleBounds;
- private final E parent;
- private final List<E> children;
-
- /**
- * 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}.
- */
- protected BaseUiAutomationElement(BaseUiAutomationContext<E> context, AccessibilityNodeInfo node,
- E parent, UiElementActor UiElementActor) {
- super(UiElementActor);
- this.node = Preconditions.checkNotNull(node);
- this.context = Preconditions.checkNotNull(context);
- this.parent = parent;
-
- Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(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());
- if (node.getTextSelectionStart() >= 0
- && node.getTextSelectionStart() != node.getTextSelectionEnd()) {
- attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart());
- attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd());
- }
- put(attribs, Attribute.SELECTED, node.isSelected());
- put(attribs, Attribute.BOUNDS, getBounds(node));
- attributes = Collections.unmodifiableMap(attribs);
-
- // Order matters as getVisibleBounds depends on visible
- visible = node.isVisibleToUser();
- visibleBounds = getVisibleBounds(node);
- List<E> mutableChildren = buildChildren(node);
- this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren);
- }
-
- private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
- if (value != null) {
- attribs.put(key, value);
- }
- }
-
- @SuppressWarnings("unchecked")
- private List<E> buildChildren(AccessibilityNodeInfo node) {
- List<E> children;
- int childCount = node.getChildCount();
- if (childCount == 0) {
- children = null;
- } else {
- children = new ArrayList<E>(childCount);
- for (int i = 0; i < childCount; i++) {
- AccessibilityNodeInfo child = node.getChild(i);
- if (child != null) {
- children.add(context.getUiElement(child, (E) this));
- }
- }
- }
- return children;
- }
-
- private Rect getBounds(AccessibilityNodeInfo node) {
- Rect rect = new Rect();
- node.getBoundsInScreen(rect);
- return rect;
- }
-
- private Rect getVisibleBounds(AccessibilityNodeInfo node) {
- if (!visible) {
- return new Rect();
- }
- Rect visibleBounds = getBounds();
- E parent = getParent();
- Rect parentBounds;
- while (parent != null) {
- parentBounds = parent.getBounds();
- visibleBounds.intersect(parentBounds);
- parent = parent.getParent();
- }
- return visibleBounds;
- }
-
- @Override
- public Rect getVisibleBounds() {
- return visibleBounds;
- }
-
- @Override
- public boolean isVisible() {
- return visible;
- }
-
- @Override
- public E getParent() {
- return parent;
- }
-
- @Override
- protected List<E> getChildren() {
- return children;
- }
-
- @Override
- protected Map<Attribute, Object> getAttributes() {
- return attributes;
- }
-
- @Override
- public InputInjector getInjector() {
- return context.getInjector();
- }
-
- /**
- * Note: This implementation of {@code doPerformAndWait} clears the
- * {@code AccessibilityEvent} queue.
- */
- @Override
- protected void doPerformAndWait(final FutureTask<Boolean> futureTask, final long timeoutMillis) {
- context.callUiAutomation(new UiAutomationCallable<Void>() {
-
- @Override
- public Void call(UiAutomation uiAutomation) {
- try {
- uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis);
- } catch (TimeoutException e) {
- // This is for sync'ing with Accessibility API on best-effort because
- // it is not reliable.
- // Exception is ignored here. Tests will fail anyways if this is
- // critical.
- // Actions should usually trigger some AccessibilityEvent's, but some
- // widgets fail to do so, resulting in stale AccessibilityNodeInfo's.
- // As a work-around, force to clear the AccessibilityNodeInfoCache.
- // A legitimate case of no AccessibilityEvent is when scrolling has
- // reached the end, but we cannot tell whether it's legitimate or the
- // widget has bugs, so clearAccessibilityNodeInfoCache anyways.
- context.getDriver().clearAccessibilityNodeInfoCacheHack();
- }
- return null;
- }
-
- });
- }
-
- /**
- * Gets the AccessibilityNodeInfo used to create this UiElement. The
- * attributes of this UiElement are based on a snapshot of the
- * AccessibilityNodeInfo at construction time. If the Accessibility framework
- * updated it later, the attributes may not match.
- */
- public AccessibilityNodeInfo getNode() {
- return node;
- }
-}
diff --git a/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
new file mode 100644
index 0000000..ff1d095
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.google.android.droiddriver.validators;
+
+import android.text.TextUtils;
+
+import com.google.android.droiddriver.UiElement;
+
+/**
+ * Validates accessibility.
+ */
+// TODO: Treats various types of UiElement as TalkBack does.
+public class DefaultAccessibilityValidator implements Validator {
+ @Override
+ public boolean isValid(UiElement element) {
+ return element.getParent() != null // don't check root
+ && TextUtils.isEmpty(element.getContentDescription())
+ && TextUtils.isEmpty(element.getText());
+ }
+}
diff --git a/src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java b/src/com/google/android/droiddriver/validators/Validator.java
index b3ac86e..f7812ed 100644
--- a/src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java
+++ b/src/com/google/android/droiddriver/validators/Validator.java
@@ -14,17 +14,18 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.exceptions;
+package com.google.android.droiddriver.validators;
import com.google.android.droiddriver.UiElement;
/**
- * Thrown when an element is not visible on screen, therefore cannot be
- * interacted with.
+ * Interface for validating a UiElement, checked when an action is performed.
+ * For example, in general accessibility mandates that an actionable UiElement
+ * has content description or text.
*/
-@SuppressWarnings("serial")
-public class ElementNotVisibleException extends DroidDriverException {
- public ElementNotVisibleException(UiElement element) {
- super("Invisible on screen: " + element);
- }
+public interface Validator {
+ /**
+ * Returns true if {@code element} is valid.
+ */
+ boolean isValid(UiElement element);
}
diff --git a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationCallable.java b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
index 36a5292..44570cf 100644
--- a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationCallable.java
+++ b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
@@ -14,10 +14,16 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.uiautomation.base;
+package com.google.android.droiddriver.validators;
-import android.app.UiAutomation;
+import com.google.android.droiddriver.UiElement;
-public interface UiAutomationCallable<T> {
- T call(UiAutomation uiAutomation);
-} \ No newline at end of file
+/**
+ * Validates visibility.
+ */
+public class VisibilityValidator implements Validator {
+ @Override
+ public boolean isValid(UiElement element) {
+ return element.isVisible();
+ }
+}