aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2014-05-29 15:35:31 -0700
committerKevin Jin <kjin@google.com>2014-06-05 14:01:34 -0700
commita738fe74f57f48dde2dd7a28479bab3f5441dadb (patch)
tree6bcb449c5545660d5057a6323e0e95c5eda0cf5c /src
parentb4e825291041d963c5bda0349638565949d999f6 (diff)
downloaddroiddriver-a738fe74f57f48dde2dd7a28479bab3f5441dadb.tar.gz
add AccessibilityDriver for testing Accessibility
This is the first cut with known issues, e.g. needs to handle click on EditText differently, etc. Thic cl sets up the architecture of AccessibilityDriver; details will be filled in follow-up cls. Change-Id: I2881b28075eba478a5aad9d7e945b5d55e78da89
Diffstat (limited to 'src')
-rw-r--r--src/com/google/android/droiddriver/UiElement.java13
-rw-r--r--src/com/google/android/droiddriver/actions/Action.java10
-rw-r--r--src/com/google/android/droiddriver/actions/ClickAction.java6
-rw-r--r--src/com/google/android/droiddriver/actions/EventAction.java47
-rw-r--r--src/com/google/android/droiddriver/actions/EventUiElementActor.java54
-rw-r--r--src/com/google/android/droiddriver/actions/KeyAction.java2
-rw-r--r--src/com/google/android/droiddriver/actions/SwipeAction.java4
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java48
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java82
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java61
-rw-r--r--src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java54
-rw-r--r--src/com/google/android/droiddriver/base/BaseUiElement.java49
-rw-r--r--src/com/google/android/droiddriver/base/UiElementActor.java58
-rw-r--r--src/com/google/android/droiddriver/helpers/DroidDrivers.java65
-rw-r--r--src/com/google/android/droiddriver/instrumentation/ViewElement.java2
-rw-r--r--src/com/google/android/droiddriver/runner/TestRunner.java2
-rw-r--r--src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java2
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java76
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java106
-rw-r--r--src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java188
-rw-r--r--src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java53
-rw-r--r--src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java36
-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/uiautomation/base/UiAutomationCallable.java (renamed from src/com/google/android/droiddriver/actions/ScrollAction.java)15
-rw-r--r--src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java40
-rw-r--r--src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java (renamed from src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java)9
29 files changed, 1169 insertions, 412 deletions
diff --git a/src/com/google/android/droiddriver/UiElement.java b/src/com/google/android/droiddriver/UiElement.java
index a2e7017..fc75e5b 100644
--- a/src/com/google/android/droiddriver/UiElement.java
+++ b/src/com/google/android/droiddriver/UiElement.java
@@ -19,7 +19,6 @@ package com.google.android.droiddriver;
import android.graphics.Rect;
import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.InputInjector;
import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
import com.google.android.droiddriver.finders.Attribute;
import com.google.android.droiddriver.finders.Predicate;
@@ -152,7 +151,6 @@ public interface UiElement {
* @param text The text to enter.
* @throws ElementNotVisibleException when the element is not visible
*/
- // TODO: Should this clear the text before setting?
void setText(String text);
/**
@@ -180,7 +178,11 @@ public interface UiElement {
void doubleClick();
/**
- * Scrolls in the given direction. Scrolling down means swiping upwards.
+ * Scrolls in the given direction.
+ *
+ * @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);
@@ -226,9 +228,4 @@ 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/Action.java b/src/com/google/android/droiddriver/actions/Action.java
index 86dfc24..fd5068e 100644
--- a/src/com/google/android/droiddriver/actions/Action.java
+++ b/src/com/google/android/droiddriver/actions/Action.java
@@ -16,25 +16,23 @@
package com.google.android.droiddriver.actions;
-import android.view.InputEvent;
-
import com.google.android.droiddriver.UiElement;
/**
* Interface for performing action on a UiElement. An action is a high-level
- * user interaction that consists of a series of {@link InputEvent}s.
+ * user interaction that can be performed in various ways, for example, via
+ * synthesized events, or the Accessibility API.
*/
public interface Action {
/**
* Performs the action.
*
- * @param injector the injector to inject {@link InputEvent}s
* @param element the Ui element to perform the action on
* @return Whether the action is successful. Some actions throw exceptions in
* case of failure, when that behavior is more appropriate. For
* example, if event injection returns false.
*/
- boolean perform(InputInjector injector, UiElement element);
+ boolean perform(UiElement element);
/**
* Gets the timeout to wait for an indicator that the action has been carried
@@ -50,7 +48,7 @@ public interface Action {
*
* <p>
* It is recommended that this method return the description of the action,
- * for example, "TypeAction{text to type}".
+ * for example, "SwipeAction{DOWN}".
*/
@Override
String toString();
diff --git a/src/com/google/android/droiddriver/actions/ClickAction.java b/src/com/google/android/droiddriver/actions/ClickAction.java
index 9050b19..0d7350d 100644
--- a/src/com/google/android/droiddriver/actions/ClickAction.java
+++ b/src/com/google/android/droiddriver/actions/ClickAction.java
@@ -26,7 +26,7 @@ import com.google.android.droiddriver.util.Events;
/**
* An action that does clicks on an UiElement.
*/
-public abstract class ClickAction extends BaseAction {
+public abstract class ClickAction extends EventAction {
public static final ClickAction SINGLE = new SingleClick(1000L);
public static final ClickAction LONG = new LongClick(1000L);
@@ -41,8 +41,8 @@ public abstract class ClickAction extends BaseAction {
@Override
public boolean perform(InputInjector injector, UiElement element) {
- SINGLE.perform(injector, element);
- SINGLE.perform(injector, element);
+ SINGLE.perform(element);
+ SINGLE.perform(element);
return true;
}
}
diff --git a/src/com/google/android/droiddriver/actions/EventAction.java b/src/com/google/android/droiddriver/actions/EventAction.java
new file mode 100644
index 0000000..71ceb6f
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/EventAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.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.
+ */
+public abstract class EventAction extends BaseAction {
+ protected EventAction(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ @Override
+ public boolean perform(UiElement element) {
+ return perform(((BaseUiElement) element).getInjector(), element);
+ }
+
+ /**
+ * Performs the action by injecting synthesized events.
+ *
+ * @param injector the injector to inject {@link InputEvent}s
+ * @param element the UiElement to perform the action on
+ * @return Whether the action is successful. Some actions throw exceptions in
+ * case of failure, when that behavior is more appropriate. For
+ * example, if event injection returns false.
+ */
+ protected abstract boolean perform(InputInjector injector, UiElement element);
+}
diff --git a/src/com/google/android/droiddriver/actions/EventUiElementActor.java b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
new file mode 100644
index 0000000..dbee143
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
@@ -0,0 +1,54 @@
+/*
+ * 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.actions;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.base.UiElementActor;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * A {@link UiElementActor} that performs actions by injecting synthesized
+ * events.
+ */
+public class EventUiElementActor implements UiElementActor {
+ public static final EventUiElementActor INSTANCE = new EventUiElementActor();
+
+ @Override
+ public void setText(UiElement uiElement, String text) {
+ uiElement.perform(new TextAction(text));
+ }
+
+ @Override
+ public void click(UiElement uiElement) {
+ uiElement.perform(ClickAction.SINGLE);
+ }
+
+ @Override
+ public void longClick(UiElement uiElement) {
+ uiElement.perform(ClickAction.LONG);
+ }
+
+ @Override
+ public void doubleClick(UiElement uiElement) {
+ uiElement.perform(ClickAction.DOUBLE);
+ }
+
+ @Override
+ public void scroll(UiElement uiElement, PhysicalDirection direction) {
+ uiElement.perform(SwipeAction.toScroll(direction));
+ }
+}
diff --git a/src/com/google/android/droiddriver/actions/KeyAction.java b/src/com/google/android/droiddriver/actions/KeyAction.java
index 1855b21..18bbd4c 100644
--- a/src/com/google/android/droiddriver/actions/KeyAction.java
+++ b/src/com/google/android/droiddriver/actions/KeyAction.java
@@ -22,7 +22,7 @@ import com.google.android.droiddriver.exceptions.ActionException;
/**
* Base class for {@link Action} that injects key events.
*/
-public abstract class KeyAction extends BaseAction {
+public abstract class KeyAction extends EventAction {
private final boolean checkFocused;
protected KeyAction(long timeoutMillis, boolean checkFocused) {
diff --git a/src/com/google/android/droiddriver/actions/SwipeAction.java b/src/com/google/android/droiddriver/actions/SwipeAction.java
index 9e2cfbe..a939536 100644
--- a/src/com/google/android/droiddriver/actions/SwipeAction.java
+++ b/src/com/google/android/droiddriver/actions/SwipeAction.java
@@ -28,9 +28,9 @@ import com.google.android.droiddriver.util.Strings;
import com.google.android.droiddriver.util.Strings.ToStringHelper;
/**
- * A {@link ScrollAction} that swipes the touch screen.
+ * An action that swipes the touch screen.
*/
-public class SwipeAction extends ScrollAction {
+public class SwipeAction extends EventAction {
// Milliseconds between synthesized ACTION_MOVE events.
// Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events;
// the actual interval typically is longer.
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
new file mode 100644
index 0000000..4d47eac
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
@@ -0,0 +1,48 @@
+/*
+ * 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.actions.accessibility;
+
+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;
+
+/**
+ * Implements {@link Action} via the Accessibility API.
+ */
+public abstract class AccessibilityAction extends BaseAction {
+ protected AccessibilityAction(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ @Override
+ public boolean perform(UiElement element) {
+ return perform(((BaseUiAutomationElement<?>) element).getNode(), element);
+ }
+
+ /**
+ * Performs the action via the Accessibility API.
+ *
+ * @param node the AccessibilityNodeInfo used to create the UiElement
+ * @param element the UiElement to perform the action on
+ * @return Whether the action is successful. Some actions throw exceptions in
+ * case of failure, when that behavior is more appropriate.
+ */
+ protected abstract boolean perform(AccessibilityNodeInfo node, UiElement element);
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
new file mode 100644
index 0000000..4a4033c
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
@@ -0,0 +1,82 @@
+/*
+ * 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.actions.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ActionException;
+
+/**
+ * An {@link AccessibilityAction} that clicks on an UiElement.
+ */
+public abstract class AccessibilityClickAction extends AccessibilityAction {
+
+ public static final AccessibilityClickAction SINGLE = new SingleClick(1000L);
+ public static final AccessibilityClickAction LONG = new LongClick(1000L);
+ public static final AccessibilityClickAction DOUBLE = new DoubleClick(1000L);
+
+ protected AccessibilityClickAction(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ public static class DoubleClick extends AccessibilityClickAction {
+ public DoubleClick(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ @Override
+ protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+ return SINGLE.perform(element) && SINGLE.perform(element);
+ }
+ }
+
+ public static class LongClick extends AccessibilityClickAction {
+ public LongClick(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ @Override
+ protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+ if (!element.isLongClickable()) {
+ throw new ActionException(element
+ + " is not long-clickable; maybe there is a clickable element in the same location?");
+ }
+ return node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ }
+ }
+
+ public static class SingleClick extends AccessibilityClickAction {
+ public SingleClick(long timeoutMillis) {
+ super(timeoutMillis);
+ }
+
+ @Override
+ protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+ if (!element.isClickable()) {
+ throw new ActionException(element
+ + " is not clickable; maybe there is a clickable element in the same location?");
+ }
+ return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
new file mode 100644
index 0000000..88e7517
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
@@ -0,0 +1,61 @@
+/*
+ * 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.actions.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Strings;
+
+/**
+ * An {@link AccessibilityAction} that scrolls an UiElement.
+ */
+public class AccessibilityScrollAction extends AccessibilityAction {
+ private final PhysicalDirection direction;
+
+ public AccessibilityScrollAction(PhysicalDirection direction) {
+ this(direction, 1000L);
+ }
+
+ public AccessibilityScrollAction(PhysicalDirection direction, long timeoutMillis) {
+ super(timeoutMillis);
+ this.direction = direction;
+ }
+
+ @Override
+ protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+ if (!element.isScrollable()) {
+ return false;
+ }
+
+ switch (direction) {
+ case UP:
+ case LEFT:
+ return node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ case DOWN:
+ case RIGHT:
+ return node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return Strings.toStringHelper(this).addValue(direction).toString();
+ }
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
new file mode 100644
index 0000000..491ad7a
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
@@ -0,0 +1,54 @@
+/*
+ * 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.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.scroll.Direction.PhysicalDirection;
+
+/**
+ * A {@link UiElementActor} that performs actions via the Accessibility API.
+ */
+public class AccessibilityUiElementActor implements UiElementActor {
+ public static final AccessibilityUiElementActor INSTANCE = new AccessibilityUiElementActor();
+
+ @Override
+ public void setText(UiElement uiElement, String text) {
+ uiElement.perform(new TextAction(text));
+ }
+
+ @Override
+ public void click(UiElement uiElement) {
+ uiElement.perform(AccessibilityClickAction.SINGLE);
+ }
+
+ @Override
+ public void longClick(UiElement uiElement) {
+ uiElement.perform(AccessibilityClickAction.LONG);
+ }
+
+ @Override
+ public void doubleClick(UiElement uiElement) {
+ uiElement.perform(AccessibilityClickAction.DOUBLE);
+ }
+
+ @Override
+ public void scroll(UiElement uiElement, PhysicalDirection direction) {
+ uiElement.perform(new AccessibilityScrollAction(direction));
+ }
+}
diff --git a/src/com/google/android/droiddriver/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java
index 1478505..ba25874 100644
--- a/src/com/google/android/droiddriver/base/BaseUiElement.java
+++ b/src/com/google/android/droiddriver/base/BaseUiElement.java
@@ -20,9 +20,8 @@ import android.graphics.Rect;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.ClickAction;
-import com.google.android.droiddriver.actions.SwipeAction;
-import com.google.android.droiddriver.actions.TextAction;
+import com.google.android.droiddriver.actions.InputInjector;
+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;
@@ -37,6 +36,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
@@ -48,6 +48,12 @@ public abstract class BaseUiElement implements UiElement {
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;
+ }
+
@SuppressWarnings("unchecked")
@Override
public <T> T get(Attribute attribute) {
@@ -152,6 +158,11 @@ 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);
@@ -160,7 +171,7 @@ public abstract class BaseUiElement implements UiElement {
}
protected boolean doPerform(Action action) {
- return action.perform(getInjector(), this);
+ return action.perform(this);
}
protected abstract void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis);
@@ -180,44 +191,40 @@ public abstract class BaseUiElement implements UiElement {
doPerformAndWait(futureTask, action.getTimeoutMillis());
try {
return futureTask.get();
- } catch (Exception e) {
- // should not reach here b/c futureTask has run
- return false;
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ throw new DroidDriverException(cause);
+ } catch (InterruptedException e) {
+ throw new DroidDriverException(e);
}
}
@Override
public void setText(String text) {
- perform(new TextAction(text));
- // TextAction may not be effective immediately and reflected by getText(),
- // so the following will fail.
- // if (Logs.DEBUG) {
- // String actual = getText();
- // if (!text.equals(actual)) {
- // throw new DroidDriverException(String.format(
- // "setText failed: expected=\"%s\", actual=\"%s\"", text, actual));
- // }
- // }
+ uiElementActor.setText(this, text);
}
@Override
public void click() {
- perform(ClickAction.SINGLE);
+ uiElementActor.click(this);
}
@Override
public void longClick() {
- perform(ClickAction.LONG);
+ uiElementActor.longClick(this);
}
@Override
public void doubleClick() {
- perform(ClickAction.DOUBLE);
+ uiElementActor.doubleClick(this);
}
@Override
public void scroll(PhysicalDirection direction) {
- perform(SwipeAction.toScroll(direction));
+ uiElementActor.scroll(this, direction);
}
protected abstract Map<Attribute, Object> getAttributes();
diff --git a/src/com/google/android/droiddriver/base/UiElementActor.java b/src/com/google/android/droiddriver/base/UiElementActor.java
new file mode 100644
index 0000000..b7ffe47
--- /dev/null
+++ b/src/com/google/android/droiddriver/base/UiElementActor.java
@@ -0,0 +1,58 @@
+/*
+ * 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.base;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * Interface for performing actions on a {@link UiElement}.
+ */
+public interface UiElementActor {
+ /**
+ * Sets the text of this element.
+ *
+ * @param text The text to enter.
+ */
+ void setText(UiElement uiElement, String text);
+
+ /**
+ * Clicks this element. The click will be at the center of the visible
+ * element.
+ */
+ void click(UiElement uiElement);
+
+ /**
+ * Long-clicks this element. The click will be at the center of the visible
+ * element.
+ */
+ void longClick(UiElement uiElement);
+
+ /**
+ * Double-clicks this element. The click will be at the center of the visible
+ * element.
+ */
+ void doubleClick(UiElement uiElement);
+
+ /**
+ * Scrolls in the given direction.
+ *
+ * @param direction specifies where the view port will move, instead of the
+ * finger.
+ */
+ void scroll(UiElement uiElement, PhysicalDirection direction);
+}
diff --git a/src/com/google/android/droiddriver/helpers/DroidDrivers.java b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
index a2a195b..c281075 100644
--- a/src/com/google/android/droiddriver/helpers/DroidDrivers.java
+++ b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
@@ -18,17 +18,22 @@ package com.google.android.droiddriver.helpers;
import android.app.Instrumentation;
import android.os.Build;
+import android.os.Bundle;
import com.google.android.droiddriver.DroidDriver;
import com.google.android.droiddriver.exceptions.DroidDriverException;
import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
import com.google.android.droiddriver.uiautomation.UiAutomationDriver;
+import java.lang.reflect.InvocationTargetException;
+
/**
* Static utility methods pertaining to {@link DroidDriver} instances.
*/
public class DroidDrivers {
private static DroidDriver driver;
+ private static Instrumentation instrumentation;
+ private static Bundle options;
/**
* Gets the singleton driver. Throws if {@link #init} has not been called.
@@ -54,6 +59,34 @@ public class DroidDrivers {
}
/**
+ * Initializes for the convenience methods {@link #getInstrumentation()} and
+ * {@link #getOptions()}. Called by
+ * {@link com.google.android.droiddriver.runner.TestRunner}. If a custom
+ * runner is used, this method must be called appropriately, otherwise the two
+ * convenience methods won't work.
+ */
+ public static void initInstrumentation(Instrumentation instrumentation, Bundle arguments) {
+ if (DroidDrivers.instrumentation != null) {
+ throw new DroidDriverException("DroidDrivers.initInstrumentation() can only be called once");
+ }
+ DroidDrivers.instrumentation = instrumentation;
+ DroidDrivers.options = arguments;
+ }
+
+ public static Instrumentation getInstrumentation() {
+ return instrumentation;
+ }
+
+ /**
+ * Gets the <a href=
+ * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax"
+ * >am instrument options</a>.
+ */
+ public static Bundle getOptions() {
+ return options;
+ }
+
+ /**
* Returns whether the running target (device or emulator) has
* {@link android.app.UiAutomation} API, which is introduced in SDK API 18
* (JELLY_BEAN_MR2).
@@ -63,10 +96,33 @@ public class DroidDrivers {
}
/**
- * Returns a new UiAutomationDriver if {@link android.app.UiAutomation} is
- * available; otherwise a new InstrumentationDriver.
+ * Returns a new DroidDriver instance. If am instrument options have "driver",
+ * treat it as the fully-qualified-class-name and create a new instance of it
+ * with {@code instrumentation} as the argument; otherwise a new
+ * platform-dependent default DroidDriver instance.
*/
public static DroidDriver newDriver(Instrumentation instrumentation) {
+ String driverClass = options == null ? null : options.getString("driver");
+ if (driverClass != null) {
+ try {
+ return (DroidDriver) Class.forName(driverClass).getConstructor(Instrumentation.class)
+ .newInstance(instrumentation);
+ } catch (ClassNotFoundException e) {
+ throw new DroidDriverException(e);
+ } catch (NoSuchMethodException e) {
+ throw new DroidDriverException(e);
+ } catch (InstantiationException e) {
+ throw new DroidDriverException(e);
+ } catch (IllegalAccessException e) {
+ throw new DroidDriverException(e);
+ } catch (IllegalArgumentException e) {
+ throw new DroidDriverException(e);
+ } catch (InvocationTargetException e) {
+ throw new DroidDriverException(e);
+ }
+ }
+
+ // If "driver" is not specified, return default.
if (hasUiAutomation()) {
return newUiAutomationDriver(instrumentation);
}
@@ -81,9 +137,8 @@ public class DroidDrivers {
/** Returns a new UiAutomationDriver */
public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) {
if (!hasUiAutomation()) {
- throw new DroidDriverException(
- "http://developer.android.com/reference/android/app/UiAutomation.html" +
- " is not available below API 18");
+ throw new DroidDriverException("UiAutomation is not available below API 18. "
+ + "See http://developer.android.com/reference/android/app/UiAutomation.html");
}
if (instrumentation.getUiAutomation() == null) {
throw new DroidDriverException(
diff --git a/src/com/google/android/droiddriver/instrumentation/ViewElement.java b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
index e38929a..fd4c39a 100644
--- a/src/com/google/android/droiddriver/instrumentation/ViewElement.java
+++ b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
@@ -26,6 +26,7 @@ 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.exceptions.DroidDriverException;
@@ -218,6 +219,7 @@ public class ViewElement extends BaseUiElement {
* {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
*/
public ViewElement(final InstrumentationContext context, View view, ViewElement parent) {
+ super(EventUiElementActor.INSTANCE);
this.context = Preconditions.checkNotNull(context);
Preconditions.checkNotNull(view);
this.parent = parent;
diff --git a/src/com/google/android/droiddriver/runner/TestRunner.java b/src/com/google/android/droiddriver/runner/TestRunner.java
index 8e86a20..e6277fb 100644
--- a/src/com/google/android/droiddriver/runner/TestRunner.java
+++ b/src/com/google/android/droiddriver/runner/TestRunner.java
@@ -66,6 +66,8 @@ public class TestRunner extends InstrumentationTestRunner {
*/
@Override
public void onStart() {
+ DroidDrivers.initInstrumentation(this, getArguments());
+
getAndroidTestRunner().addTestListener(new TestListener() {
@Override
public void endTest(Test test) {
diff --git a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
index 7929798..6911fa6 100644
--- a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
@@ -197,7 +197,7 @@ public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy
// 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.getInjector(), container);
+ SwipeAction.toScroll(direction).perform(container);
}
/**
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
index fcb7a94..04df113 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
@@ -17,81 +17,17 @@
package com.google.android.droiddriver.uiautomation;
import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.view.InputEvent;
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 com.google.android.droiddriver.uiautomation.base.BaseUiAutomationContext;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-class UiAutomationContext extends DroidDriverContext {
- private final Map<AccessibilityNodeInfo, UiAutomationElement> map =
- new WeakHashMap<AccessibilityNodeInfo, UiAutomationElement>();
- private final UiAutomation uiAutomation;
- private final InputInjector injector;
- private final UiAutomationDriver driver;
-
- UiAutomationContext(final Instrumentation instrumentation, UiAutomationDriver driver) {
- super(instrumentation);
- this.uiAutomation = instrumentation.getUiAutomation();
- this.driver = driver;
- this.injector = new InputInjector() {
- @Override
- public boolean injectInputEvent(final InputEvent event) {
- return callUiAutomation(new UiAutomationCallable<Boolean>() {
- @Override
- public Boolean call(UiAutomation uiAutomation) {
- return uiAutomation.injectInputEvent(event, true /* sync */);
- }
- });
- }
- };
- }
-
- @Override
- public UiAutomationDriver getDriver() {
- return driver;
- }
-
- @Override
- public InputInjector getInjector() {
- return injector;
- }
-
- UiAutomationElement getUiElement(AccessibilityNodeInfo node, UiAutomationElement parent) {
- UiAutomationElement element = map.get(node);
- if (element == null) {
- element = new UiAutomationElement(this, node, parent);
- map.put(node, element);
- }
- return element;
+class UiAutomationContext extends BaseUiAutomationContext<UiAutomationElement> {
+ UiAutomationContext(Instrumentation instrumentation, UiAutomationDriver driver) {
+ super(instrumentation, driver);
}
@Override
- public void clearData() {
- map.clear();
- ByXPath.clearData();
- }
-
- 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.
- */
- <T> T callUiAutomation(UiAutomationCallable<T> uiAutomationCallable) {
- try {
- return uiAutomationCallable.call(uiAutomation);
- } catch (IllegalStateException e) {
- throw new UnrecoverableException(e);
- }
+ protected UiAutomationElement newUiElement(AccessibilityNodeInfo node, UiAutomationElement parent) {
+ return new UiAutomationElement(this, node, parent);
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
index d465816..9291f13 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
@@ -17,112 +17,20 @@
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.base.BaseDroidDriver;
-import com.google.android.droiddriver.exceptions.TimeoutException;
-import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
-import com.google.android.droiddriver.util.Logs;
+import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationDriver;
/**
- * Implementation of DroidDriver that is driven via the accessibility layer.
+ * Implementation of DroidDriver that gets attributes via the Accessibility API
+ * and is acted upon via synthesized events.
*/
-public class UiAutomationDriver 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 UiAutomationContext context;
- private final UiAutomationUiDevice uiDevice;
-
+public class UiAutomationDriver extends BaseUiAutomationDriver<UiAutomationElement> {
public UiAutomationDriver(Instrumentation instrumentation) {
- this.context = new UiAutomationContext(instrumentation, this);
- this.uiDevice = new UiAutomationUiDevice(context);
- }
-
- @Override
- protected UiAutomationElement getNewRootElement() {
- return context.getUiElement(getRootNode(), null /* parent */);
- }
-
- @Override
- protected UiAutomationContext 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));
+ super(instrumentation);
}
@Override
- public UiAutomationUiDevice getUiDevice() {
- return uiDevice;
+ protected UiAutomationContext newContext(Instrumentation instrumentation) {
+ return new UiAutomationContext(instrumentation, this);
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
index 5d69814..9a4309b 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
@@ -16,193 +16,17 @@
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.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;
+import com.google.android.droiddriver.actions.EventUiElementActor;
+import com.google.android.droiddriver.uiautomation.base.BaseUiAutomationElement;
/**
- * A UiElement that is backed by an {@link AccessibilityNodeInfo}.
+ * A BaseUiAutomationElement that is acted upon via synthesized events.
*/
-public class UiAutomationElement extends BaseUiElement {
- private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
- @Override
- public boolean accept(AccessibilityEvent arg0) {
- return true;
- }
- };
-
- 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}.
- */
- public UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
+class UiAutomationElement extends BaseUiAutomationElement<UiAutomationElement> {
+ UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
UiAutomationElement parent) {
- this.context = Preconditions.checkNotNull(context);
- Preconditions.checkNotNull(node);
- 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(context, 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(UiAutomationContext context,
- 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.getUiElement(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.getInjector();
- }
-
- /**
- * Note: The {@code UiAutomationElement} 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;
- }
-
- });
+ super(context, node, parent, EventUiElementActor.INSTANCE);
}
}
diff --git a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java
new file mode 100644
index 0000000..4c76d29
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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/AccessibilityDriver.java b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java
new file mode 100644
index 0000000..2f2295c
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityDriver.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.google.android.droiddriver.uiautomation.base.BaseUiAutomationDriver;
+
+/**
+ * Implementation of DroidDriver that gets attributes via the Accessibility API
+ * and is acted upon via the Accessibility API.
+ */
+public class AccessibilityDriver extends BaseUiAutomationDriver<AccessibilityElement> {
+ public AccessibilityDriver(Instrumentation instrumentation) {
+ super(instrumentation);
+ }
+
+ @Override
+ protected AccessibilityContext newContext(Instrumentation instrumentation) {
+ return new AccessibilityContext(instrumentation, this);
+ }
+}
diff --git a/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java
new file mode 100644
index 0000000..c208730
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/accessibility/AccessibilityElement.java
@@ -0,0 +1,51 @@
+/*
+ * 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
new file mode 100644
index 0000000..1412818
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationContext.java
@@ -0,0 +1,97 @@
+/*
+ * 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
new file mode 100644
index 0000000..fe223e3
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationDriver.java
@@ -0,0 +1,131 @@
+/*
+ * 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
new file mode 100644
index 0000000..a27a3eb
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/base/BaseUiAutomationElement.java
@@ -0,0 +1,220 @@
+/*
+ * 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/actions/ScrollAction.java b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationCallable.java
index e616e09..36a5292 100644
--- a/src/com/google/android/droiddriver/actions/ScrollAction.java
+++ b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationCallable.java
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.actions;
+package com.google.android.droiddriver.uiautomation.base;
-/**
- * Base class for {@link Action} that scrolls.
- */
-public abstract class ScrollAction extends BaseAction {
- protected ScrollAction(long timeoutMillis) {
- super(timeoutMillis);
- }
-}
+import android.app.UiAutomation;
+
+public interface UiAutomationCallable<T> {
+ T call(UiAutomation uiAutomation);
+} \ No newline at end of file
diff --git a/src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java
new file mode 100644
index 0000000..5d13d2d
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationInputInjector.java
@@ -0,0 +1,40 @@
+/*
+ * 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.UiAutomation;
+import android.view.InputEvent;
+
+import com.google.android.droiddriver.actions.InputInjector;
+
+public class UiAutomationInputInjector implements InputInjector {
+ private final BaseUiAutomationContext<?> context;
+
+ public UiAutomationInputInjector(BaseUiAutomationContext<?> context) {
+ this.context = context;
+ }
+
+ @Override
+ public boolean injectInputEvent(final InputEvent event) {
+ return context.callUiAutomation(new UiAutomationCallable<Boolean>() {
+ @Override
+ public Boolean call(UiAutomation uiAutomation) {
+ return uiAutomation.injectInputEvent(event, true /* sync */);
+ }
+ });
+ }
+}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java
index a376cb6..5c66ef5 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java
+++ b/src/com/google/android/droiddriver/uiautomation/base/UiAutomationUiDevice.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.droiddriver.uiautomation;
+package com.google.android.droiddriver.uiautomation.base;
import android.app.UiAutomation;
import android.graphics.Bitmap;
@@ -22,13 +22,12 @@ 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 UiAutomationContext context;
+ private final BaseUiAutomationContext<?> context;
- UiAutomationUiDevice(UiAutomationContext context) {
+ UiAutomationUiDevice(BaseUiAutomationContext<?> context) {
this.context = context;
}
@@ -50,7 +49,7 @@ class UiAutomationUiDevice extends BaseUiDevice {
}
@Override
- protected UiAutomationContext getContext() {
+ protected BaseUiAutomationContext<?> getContext() {
return context;
}
}