aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkjin <kjin@google.com>2016-11-04 17:14:06 -0700
committerKevin Jin <kjin@google.com>2017-02-17 14:42:16 -0800
commit91b56eb1cc3f3d971cd40b0f9e64d915eb00a88e (patch)
tree6a83c0f6eda115b78f26f28a4062355df84c333a
parent0fad39b89d74210ae77a2321d4a8afcf5a5511e2 (diff)
downloaddroiddriver-91b56eb1cc3f3d971cd40b0f9e64d915eb00a88e.tar.gz
Fix setText incomplete problem when the IME is slow
The fix is to add delay after each KeyEvent injection. Also removed the misleading ViewElement#overrideClassName method. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138252579
-rw-r--r--src/io/appium/droiddriver/UiElement.java208
-rw-r--r--src/io/appium/droiddriver/actions/TextAction.java28
-rw-r--r--src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java3
-rw-r--r--src/io/appium/droiddriver/instrumentation/ViewElement.java203
4 files changed, 182 insertions, 260 deletions
diff --git a/src/io/appium/droiddriver/UiElement.java b/src/io/appium/droiddriver/UiElement.java
index a003367..aa55d09 100644
--- a/src/io/appium/droiddriver/UiElement.java
+++ b/src/io/appium/droiddriver/UiElement.java
@@ -17,9 +17,7 @@
package io.appium.droiddriver;
import android.graphics.Rect;
-
-import java.util.List;
-
+import android.view.accessibility.AccessibilityNodeInfo;
import io.appium.droiddriver.actions.Action;
import io.appium.droiddriver.actions.InputInjector;
import io.appium.droiddriver.finders.Attribute;
@@ -27,113 +25,114 @@ import io.appium.droiddriver.finders.Predicate;
import io.appium.droiddriver.instrumentation.InstrumentationDriver;
import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
import io.appium.droiddriver.uiautomation.UiAutomationDriver;
+import java.util.List;
/**
* Represents an UI element within an Android App.
- * <p>
- * UI elements are generally views. Users can get attributes and perform
- * actions. Note that actions often update UiElement, so users are advised not
- * to store instances for later use -- the instances could become stale.
+ *
+ * <p>UI elements are generally views. Users can get attributes and perform actions. Note that
+ * actions often update UiElement, so users are advised not to store instances for later use -- the
+ * instances could become stale.
*/
public interface UiElement {
- /**
- * Gets the text of this element.
- */
+ /** Filters out invisible children. */
+ Predicate<UiElement> VISIBLE =
+ new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ return element.isVisible();
+ }
+
+ @Override
+ public String toString() {
+ return "VISIBLE";
+ }
+ };
+
+ /** Gets the text of this element. */
String getText();
/**
- * Gets the content description of this element.
+ * Sets the text of this element. The implementation may not work on all UiElements if the
+ * underlying view is not EditText.
+ *
+ * <p>If this element already has text, it is cleared first if the device has API 11 or higher.
+ *
+ * <p>TODO: Support this behavior on older devices.
+ *
+ * <p>The soft keyboard may be shown after this call. If the {@code text} ends with {@code '\n'},
+ * the IME may be closed automatically. If the soft keyboard is open, you can call {@link
+ * UiDevice#pressBack()} to close it.
+ *
+ * <p>If you are using {@link io.appium.droiddriver.instrumentation.InstrumentationDriver}, you
+ * may use {@link io.appium.droiddriver.actions.view.CloseKeyboardAction} to close it. The
+ * advantage of {@code CloseKeyboardAction} is that it is a no-op if the soft keyboard is hidden.
+ * This is useful when the state of the soft keyboard cannot be determined.
+ *
+ * @param text the text to enter
*/
+ void setText(String text);
+
+ /** Gets the content description of this element. */
String getContentDescription();
/**
- * Gets the class name of the underlying view. The actual name could be
- * overridden.
- *
- * @see io.appium.droiddriver.instrumentation.ViewElement#overrideClassName
+ * Gets the class name of the underlying view. The actual name could be overridden if viewed with
+ * uiautomatorviewer, which gets the name from {@link AccessibilityNodeInfo#getClassName}. If the
+ * app uses custom View classes that do not call {@link AccessibilityNodeInfo#setClassName} with
+ * the actual class name, uiautomatorviewer will report the wrong name.
*/
String getClassName();
- /**
- * Gets the resource id of this element.
- */
+ /** Gets the resource id of this element. */
String getResourceId();
- /**
- * Gets the package name of this element.
- */
+ /** Gets the package name of this element. */
String getPackageName();
- /**
- * @return whether or not this element is visible on the device's display.
- */
+ /** @return whether or not this element is visible on the device's display. */
boolean isVisible();
- /**
- * @return whether this element is checkable.
- */
+ /** @return whether this element is checkable. */
boolean isCheckable();
- /**
- * @return whether this element is checked.
- */
+ /** @return whether this element is checked. */
boolean isChecked();
- /**
- * @return whether this element is clickable.
- */
+ /** @return whether this element is clickable. */
boolean isClickable();
- /**
- * @return whether this element is enabled.
- */
+ /** @return whether this element is enabled. */
boolean isEnabled();
- /**
- * @return whether this element is focusable.
- */
+ /** @return whether this element is focusable. */
boolean isFocusable();
- /**
- * @return whether this element is focused.
- */
+ /** @return whether this element is focused. */
boolean isFocused();
- /**
- * @return whether this element is scrollable.
- */
+ /** @return whether this element is scrollable. */
boolean isScrollable();
- /**
- * @return whether this element is long-clickable.
- */
+ /** @return whether this element is long-clickable. */
boolean isLongClickable();
- /**
- * @return whether this element is password.
- */
+ /** @return whether this element is password. */
boolean isPassword();
- /**
- * @return whether this element is selected.
- */
+ /** @return whether this element is selected. */
boolean isSelected();
/**
- * Gets the UiElement bounds in screen coordinates. The coordinates may not be
- * visible on screen.
+ * Gets the UiElement bounds in screen coordinates. The coordinates may not be visible on screen.
*/
Rect getBounds();
- /**
- * Gets the UiElement bounds in screen coordinates. The coordinates will be
- * visible on screen.
- */
+ /** Gets the UiElement bounds in screen coordinates. The coordinates will be visible on screen. */
Rect getVisibleBounds();
- /**
- * @return value of the given attribute.
- */
+ /** @return value of the given attribute. */
+ @SuppressWarnings("TypeParameterUnusedInFormals")
<T> T get(Attribute attribute);
/**
@@ -144,37 +143,13 @@ public interface UiElement {
*/
boolean perform(Action action);
- /**
- * Sets the text of this element. The implementation may not work on all UiElements if the
- * underlying view is not EditText. <p> If this element already has text, it is cleared first if
- * the device has API 11 or higher. <p> TODO: Support this behavior on older devices. <p> The IME
- * (soft keyboard) may be shown after this call. If the {@code text} ends with {@code '\n'}, the
- * IME may be closed automatically. If the IME is open, you can call {@link UiDevice#pressBack()}
- * to close it. <p> If you are using {@link io.appium.droiddriver.instrumentation.InstrumentationDriver},
- * you may use {@link io.appium.droiddriver.actions.view.CloseKeyboardAction} to close it. The
- * advantage of {@code CloseKeyboardAction} is that it is a no-op if the IME is hidden. This is
- * useful when the state of the IME cannot be determined.
- *
- * @param text the text to enter
- */
- void setText(String text);
-
- /**
- * Clicks this element. The click will be at the center of the visible
- * element.
- */
+ /** Clicks this element. The click will be at the center of the visible element. */
void click();
- /**
- * Long-clicks this element. The click will be at the center of the visible
- * element.
- */
+ /** Long-clicks this element. The click will be at the center of the visible element. */
void longClick();
- /**
- * Double-clicks this element. The click will be at the center of the visible
- * element.
- */
+ /** Double-clicks this element. The click will be at the center of the visible element. */
void doubleClick();
/**
@@ -185,50 +160,27 @@ public interface UiElement {
void scroll(PhysicalDirection direction);
/**
- * Gets an immutable {@link List} of immediate children that satisfy
- * {@code predicate}. It always filters children that are null. This gives a
- * low level access to the underlying data. Do not use it unless you are sure
- * about the subtle details. Note the count may not be what you expect. For
- * instance, a dynamic list may show more items when scrolling beyond the end,
- * varying the count. The count also depends on the driver implementation:
+ * Gets an immutable {@link List} of immediate children that satisfy {@code predicate}. It always
+ * filters children that are null. This gives a low level access to the underlying data. Do not
+ * use it unless you are sure about the subtle details. Note the count may not be what you expect.
+ * For instance, a dynamic list may show more items when scrolling beyond the end, varying the
+ * count. The count also depends on the driver implementation:
+ *
* <ul>
- * <li>{@link InstrumentationDriver} includes all.</li>
- * <li>the Accessibility API (which {@link UiAutomationDriver} depends on)
- * does not include off-screen children, but may include invisible on-screen
- * children.</li>
+ * <li>{@link InstrumentationDriver} includes all.
+ * <li>the Accessibility API (which {@link UiAutomationDriver} depends on) does not include
+ * off-screen children, but may include invisible on-screen children.
* </ul>
- * <p>
- * Another discrepancy between {@link InstrumentationDriver}
- * {@link UiAutomationDriver} is the order of children. The Accessibility API
- * returns children in the order of layout (see
- * {@link android.view.ViewGroup#addChildrenForAccessibility}, which is added
- * in API16).
- * </p>
+ *
+ * <p>Another discrepancy between {@link InstrumentationDriver} {@link UiAutomationDriver} is the
+ * order of children. The Accessibility API returns children in the order of layout (see {@link
+ * android.view.ViewGroup#addChildrenForAccessibility}, which is added in API16).
*/
List<? extends UiElement> getChildren(Predicate<? super UiElement> predicate);
- /**
- * Filters out invisible children.
- */
- Predicate<UiElement> VISIBLE = new Predicate<UiElement>() {
- @Override
- public boolean apply(UiElement element) {
- return element.isVisible();
- }
-
- @Override
- public String toString() {
- return "VISIBLE";
- }
- };
-
- /**
- * Gets the parent.
- */
+ /** Gets the parent. */
UiElement getParent();
- /**
- * Gets the {@link InputInjector} for injecting InputEvent.
- */
+ /** Gets the {@link InputInjector} for injecting InputEvent. */
InputInjector getInjector();
}
diff --git a/src/io/appium/droiddriver/actions/TextAction.java b/src/io/appium/droiddriver/actions/TextAction.java
index 28565e8..18b28e8 100644
--- a/src/io/appium/droiddriver/actions/TextAction.java
+++ b/src/io/appium/droiddriver/actions/TextAction.java
@@ -21,29 +21,26 @@ import android.os.Build;
import android.os.SystemClock;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
-
+import android.view.ViewConfiguration;
import io.appium.droiddriver.UiElement;
import io.appium.droiddriver.exceptions.ActionException;
import io.appium.droiddriver.util.Preconditions;
import io.appium.droiddriver.util.Strings;
-/**
- * An action to type text.
- */
+/** An action to type text. */
public class TextAction extends KeyAction {
@SuppressLint("InlinedApi")
@SuppressWarnings("deprecation")
private static final KeyCharacterMap KEY_CHAR_MAP =
- Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? KeyCharacterMap
- .load(KeyCharacterMap.BUILT_IN_KEYBOARD) : KeyCharacterMap
- .load(KeyCharacterMap.VIRTUAL_KEYBOARD);
-
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
+ ? KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD)
+ : KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ // KeyRepeatDelay is a good heuristic for KeyInjectionDelay.
+ private static long keyInjectionDelayMillis = ViewConfiguration.getKeyRepeatDelay();
private final String text;
- /**
- * Defaults timeoutMillis to 100.
- */
+ /** Defaults timeoutMillis to 100. */
public TextAction(String text) {
this(text, 100L, false);
}
@@ -53,6 +50,14 @@ public class TextAction extends KeyAction {
this.text = Preconditions.checkNotNull(text);
}
+ public static long getKeyInjectionDelayMillis() {
+ return keyInjectionDelayMillis;
+ }
+
+ public static void setKeyInjectionDelayMillis(long keyInjectionDelayMillis) {
+ TextAction.keyInjectionDelayMillis = keyInjectionDelayMillis;
+ }
+
@Override
public boolean perform(InputInjector injector, UiElement element) {
maybeCheckFocused(element);
@@ -71,6 +76,7 @@ public class TextAction extends KeyAction {
if (!injector.injectInputEvent(modifiedEvent)) {
throw new ActionException("Failed to inject " + event);
}
+ SystemClock.sleep(keyInjectionDelayMillis);
}
} else {
throw new ActionException("The given text is not supported: " + text);
diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
index 0b17dc5..607da95 100644
--- a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
+++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
@@ -91,9 +91,6 @@ public abstract class BaseDroidDriverTest<T extends Activity> extends
* behavior - if multiple subclasses override this method, only the first override is executed.
* Other overrides are silently ignored. You can either use {@link SingleRun} in {@link #setUp},
* or override this method, which is a simpler alternative with the aforementioned catch.
- * <p>
- * If an InstrumentationDriver is used, this is a good place to call {@link
- * io.appium.droiddriver.instrumentation.ViewElement#overrideClassName}
*/
protected void classSetUp() {
DroidDriversInitializer.get(DroidDrivers.newDriver()).singleRun();
diff --git a/src/io/appium/droiddriver/instrumentation/ViewElement.java b/src/io/appium/droiddriver/instrumentation/ViewElement.java
index ab87817..da7f2d0 100644
--- a/src/io/appium/droiddriver/instrumentation/ViewElement.java
+++ b/src/io/appium/droiddriver/instrumentation/ViewElement.java
@@ -22,33 +22,102 @@ import android.content.res.Resources;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.TextView;
-
+import io.appium.droiddriver.actions.InputInjector;
+import io.appium.droiddriver.base.BaseUiElement;
+import io.appium.droiddriver.base.DroidDriverContext;
+import io.appium.droiddriver.finders.Attribute;
+import io.appium.droiddriver.util.InstrumentationUtils;
+import io.appium.droiddriver.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
-import io.appium.droiddriver.actions.InputInjector;
-import io.appium.droiddriver.base.BaseUiElement;
-import io.appium.droiddriver.base.DroidDriverContext;
-import io.appium.droiddriver.finders.Attribute;
-import io.appium.droiddriver.util.InstrumentationUtils;
-import io.appium.droiddriver.util.Preconditions;
-
-/**
- * A UiElement that is backed by a View.
- */
+/** A UiElement that is backed by a View. */
public class ViewElement extends BaseUiElement<View, ViewElement> {
+ private final DroidDriverContext<View, ViewElement> context;
+ private final View view;
+ private final Map<Attribute, Object> attributes;
+ private final boolean visible;
+ private final Rect visibleBounds;
+ private final ViewElement parent;
+ private final List<ViewElement> children;
+
+ /**
+ * A snapshot of all attributes is taken at construction. The attributes of a {@code ViewElement}
+ * instance are immutable. If the underlying view is updated, a new {@code ViewElement} instance
+ * will be created in {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}.
+ */
+ public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) {
+ this.context = Preconditions.checkNotNull(context);
+ this.view = Preconditions.checkNotNull(view);
+ this.parent = parent;
+ AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view);
+ InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot);
+
+ attributes = Collections.unmodifiableMap(attributesSnapshot.attribs);
+ this.visibleBounds = attributesSnapshot.visibleBounds;
+ this.visible = attributesSnapshot.visible;
+ if (attributesSnapshot.childViews == null) {
+ this.children = null;
+ } else {
+ List<ViewElement> children = new ArrayList<>(attributesSnapshot.childViews.size());
+ for (View childView : attributesSnapshot.childViews) {
+ children.add(context.getElement(childView, this));
+ }
+ this.children = Collections.unmodifiableList(children);
+ }
+ }
+
+ @Override
+ public Rect getVisibleBounds() {
+ return visibleBounds;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return visible;
+ }
+
+ @Override
+ public ViewElement getParent() {
+ return parent;
+ }
+
+ @Override
+ protected List<ViewElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ protected Map<Attribute, Object> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public InputInjector getInjector() {
+ return context.getDriver().getInjector();
+ }
+
+ @Override
+ protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
+ futureTask.run();
+ InstrumentationUtils.tryWaitForIdleSync(timeoutMillis);
+ }
+
+ @Override
+ public View getRawElement() {
+ return view;
+ }
+
private static class AttributesSnapshot implements Callable<Void> {
+ final Map<Attribute, Object> attribs = new EnumMap<>(Attribute.class);
private final View view;
- final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
boolean visible;
Rect visibleBounds;
List<View> childViews;
@@ -107,9 +176,7 @@ public class ViewElement extends BaseUiElement<View, ViewElement> {
}
private String getClassName() {
- String className = view.getClass().getName();
- return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className)
- : className;
+ return view.getClass().getName();
}
private String getResourceId() {
@@ -168,7 +235,7 @@ public class ViewElement extends BaseUiElement<View, ViewElement> {
}
ViewGroup group = (ViewGroup) view;
int childCount = group.getChildCount();
- childViews = new ArrayList<View>(childCount);
+ childViews = new ArrayList<>(childCount);
for (int i = 0; i < childCount; i++) {
View child = group.getChildAt(i);
if (child != null) {
@@ -177,104 +244,4 @@ public class ViewElement extends BaseUiElement<View, ViewElement> {
}
}
}
-
- private static final Map<String, String> CLASS_NAME_OVERRIDES = new HashMap<String, String>();
-
- /**
- * Typically users find the class name to use in tests using SDK tool
- * uiautomatorviewer. This name is returned by
- * {@link AccessibilityNodeInfo#getClassName}. If the app uses custom View
- * classes that do not call {@link AccessibilityNodeInfo#setClassName} with
- * the actual class name, different types of drivers see different class names
- * (InstrumentationDriver sees the actual class name, while UiAutomationDriver
- * sees {@link AccessibilityNodeInfo#getClassName}).
- * <p>
- * If tests fail with InstrumentationDriver, find the actual class name by
- * examining app code or by calling
- * {@link io.appium.droiddriver.DroidDriver#dumpUiElementTree}, then
- * call this method in setUp to override it with the class name seen in
- * uiautomatorviewer.
- * </p>
- * A better solution is to use resource-id instead of classname, which is an
- * implementation detail and subject to change.
- */
- public static void overrideClassName(String actualClassName, String overridingClassName) {
- CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName);
- }
-
- private final DroidDriverContext<View, ViewElement> context;
- private final View view;
- private final Map<Attribute, Object> attributes;
- private final boolean visible;
- private final Rect visibleBounds;
- private final ViewElement parent;
- private final List<ViewElement> children;
-
- /**
- * A snapshot of all attributes is taken at construction. The attributes of a
- * {@code ViewElement} instance are immutable. If the underlying view is
- * updated, a new {@code ViewElement} instance will be created in
- * {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}.
- */
- public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) {
- this.context = Preconditions.checkNotNull(context);
- this.view = Preconditions.checkNotNull(view);
- this.parent = parent;
- AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view);
- InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot);
-
- attributes = Collections.unmodifiableMap(attributesSnapshot.attribs);
- this.visibleBounds = attributesSnapshot.visibleBounds;
- this.visible = attributesSnapshot.visible;
- if (attributesSnapshot.childViews == null) {
- this.children = null;
- } else {
- List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size());
- for (View childView : attributesSnapshot.childViews) {
- children.add(context.getElement(childView, this));
- }
- this.children = Collections.unmodifiableList(children);
- }
- }
-
- @Override
- public Rect getVisibleBounds() {
- return visibleBounds;
- }
-
- @Override
- public boolean isVisible() {
- return visible;
- }
-
- @Override
- public ViewElement getParent() {
- return parent;
- }
-
- @Override
- protected List<ViewElement> getChildren() {
- return children;
- }
-
- @Override
- protected Map<Attribute, Object> getAttributes() {
- return attributes;
- }
-
- @Override
- public InputInjector getInjector() {
- return context.getDriver().getInjector();
- }
-
- @Override
- protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
- futureTask.run();
- InstrumentationUtils.tryWaitForIdleSync(timeoutMillis);
- }
-
- @Override
- public View getRawElement() {
- return view;
- }
}