diff options
Diffstat (limited to 'src/io/appium/droiddriver')
-rw-r--r-- | src/io/appium/droiddriver/UiElement.java | 208 | ||||
-rw-r--r-- | src/io/appium/droiddriver/actions/TextAction.java | 28 | ||||
-rw-r--r-- | src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java | 3 | ||||
-rw-r--r-- | src/io/appium/droiddriver/instrumentation/ViewElement.java | 203 |
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; - } } |