diff options
author | Kevin Jin <kjin@google.com> | 2013-08-07 16:16:45 -0700 |
---|---|---|
committer | Kevin Jin <kjin@google.com> | 2013-08-08 10:40:10 -0700 |
commit | 70e34108e0fc19277e642aef3b36b65b8e254899 (patch) | |
tree | 7738804f314cf84a5bdc54606f47657d2c56c530 /src/com | |
parent | 21a0001e2426644dd68e6140b5873ebaeafcc3dc (diff) | |
download | droiddriver-70e34108e0fc19277e642aef3b36b65b8e254899.tar.gz |
add UiDevice for global actions
add UiAutomationDriver#clearAccessibilityNodeInfoCache
to work around an Accessibility bug
Change-Id: I42db1d61944240520cc34f1ccb4537f572adecf9
Diffstat (limited to 'src/com')
17 files changed, 289 insertions, 33 deletions
diff --git a/src/com/google/android/droiddriver/DroidDriver.java b/src/com/google/android/droiddriver/DroidDriver.java index 6daed71..d43e982 100644 --- a/src/com/google/android/droiddriver/DroidDriver.java +++ b/src/com/google/android/droiddriver/DroidDriver.java @@ -20,6 +20,9 @@ import com.google.android.droiddriver.exceptions.ElementNotFoundException; import com.google.android.droiddriver.exceptions.TimeoutException; import com.google.android.droiddriver.finders.Finder; +/** + * The entry interface for using droiddriver. + */ public interface DroidDriver { /** * Returns whether a matching element exists without polling. Use this if the @@ -106,6 +109,11 @@ public interface DroidDriver { void setPoller(Poller poller); /** + * Returns a {@link UiDevice} for device-wide interaction. + */ + UiDevice getUiDevice(); + + /** * Dumps the UiElement tree to a file to help debug. The tree is based on the * last used root UiElement if it exists. Screenshot is always current. If * they do not match, the UiElement tree must be stale, indicating that you diff --git a/src/com/google/android/droiddriver/UiDevice.java b/src/com/google/android/droiddriver/UiDevice.java new file mode 100644 index 0000000..381aeab --- /dev/null +++ b/src/com/google/android/droiddriver/UiDevice.java @@ -0,0 +1,43 @@ +/* + * 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; + +import com.google.android.droiddriver.actions.Action; + +/** + * Interface for device-wide interaction. + */ +public interface UiDevice { + /** + * Returns whether the screen is on. + */ + boolean isScreenOn(); + + /** Wakes up device if the screen is off */ + void wakeUp(); + + /** Puts device to sleep if the screen is on */ + void sleep(); + + /** + * Executes a global action without the context of a certain UiElement. + * + * @param action The action to execute + * @return true if the action is successful + */ + boolean perform(Action action); +} diff --git a/src/com/google/android/droiddriver/actions/KeyAction.java b/src/com/google/android/droiddriver/actions/KeyAction.java index 1cafe0e..1855b21 100644 --- a/src/com/google/android/droiddriver/actions/KeyAction.java +++ b/src/com/google/android/droiddriver/actions/KeyAction.java @@ -16,11 +16,23 @@ package com.google.android.droiddriver.actions; +import com.google.android.droiddriver.UiElement; +import com.google.android.droiddriver.exceptions.ActionException; + /** * Base class for {@link Action} that injects key events. */ public abstract class KeyAction extends BaseAction { - protected KeyAction(long timeoutMillis) { + private final boolean checkFocused; + + protected KeyAction(long timeoutMillis, boolean checkFocused) { super(timeoutMillis); + this.checkFocused = checkFocused; + } + + protected void maybeCheckFocused(UiElement element) { + if (checkFocused && element != null && !element.isFocused()) { + throw new ActionException(element + " is not focused"); + } } } diff --git a/src/com/google/android/droiddriver/actions/PressKeyAction.java b/src/com/google/android/droiddriver/actions/PressKeyAction.java index 14fd060..a08dbef 100644 --- a/src/com/google/android/droiddriver/actions/PressKeyAction.java +++ b/src/com/google/android/droiddriver/actions/PressKeyAction.java @@ -25,25 +25,39 @@ import com.google.android.droiddriver.util.Events; import com.google.common.base.Objects; /** - * An action to press a single key. TODO: rename to SingleKeyAction + * An action to press a single key. While it is convenient for navigating the + * UI, do not overuse it -- the application may interpret key codes in a custom + * way and, more importantly, application users may not have access to it + * because the device (physical or virtual keyboard) may not support all key + * codes. TODO: rename to SingleKeyAction */ public class PressKeyAction extends KeyAction { + /** + * Common instances for convenience and memory preservation. + */ + public static final PressKeyAction MENU = new PressKeyAction(KeyEvent.KEYCODE_MENU); + public static final PressKeyAction SEARCH = new PressKeyAction(KeyEvent.KEYCODE_SEARCH); + public static final PressKeyAction BACK = new PressKeyAction(KeyEvent.KEYCODE_BACK); + public static final PressKeyAction DELETE = new PressKeyAction(KeyEvent.KEYCODE_DEL); + private final int keyCode; /** - * Defaults timeoutMillis to 0. + * Defaults timeoutMillis to 100. */ public PressKeyAction(int keyCode) { - this(keyCode, 0L); + this(keyCode, 100L, false); } - public PressKeyAction(int keyCode, long timeoutMillis) { - super(timeoutMillis); + public PressKeyAction(int keyCode, long timeoutMillis, boolean checkFocused) { + super(timeoutMillis, checkFocused); this.keyCode = keyCode; } @Override public boolean perform(InputInjector injector, UiElement element) { + maybeCheckFocused(element); + final long downTime = SystemClock.uptimeMillis(); KeyEvent downEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_DOWN, keyCode); KeyEvent upEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_UP, keyCode); diff --git a/src/com/google/android/droiddriver/actions/SwipeAction.java b/src/com/google/android/droiddriver/actions/SwipeAction.java index 433f6de..a7ea7ea 100644 --- a/src/com/google/android/droiddriver/actions/SwipeAction.java +++ b/src/com/google/android/droiddriver/actions/SwipeAction.java @@ -24,20 +24,43 @@ import com.google.android.droiddriver.InputInjector; import com.google.android.droiddriver.UiElement; import com.google.android.droiddriver.exceptions.ActionException; import com.google.android.droiddriver.util.Events; +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; /** * A {@link ScrollAction} that swipes the touch screen. */ public class SwipeAction extends ScrollAction { + /** Common instances for convenience */ + public static final SwipeAction SCROLL_UP = new SwipeAction(ScrollDirection.UP, false); + public static final SwipeAction SCROLL_DOWN = new SwipeAction(ScrollDirection.DOWN, false); + public static final SwipeAction SCROLL_LEFT = new SwipeAction(ScrollDirection.LEFT, false); + public static final SwipeAction SCROLL_RIGHT = new SwipeAction(ScrollDirection.RIGHT, false); + + /** Gets canned common instances */ + public static SwipeAction toScroll(ScrollDirection direction) { + switch (direction) { + case UP: + return SCROLL_UP; + case DOWN: + return SCROLL_DOWN; + case LEFT: + return SCROLL_LEFT; + case RIGHT: + return SCROLL_RIGHT; + default: + throw new ActionException("Unknown scroll direction: " + direction); + } + } private final ScrollDirection direction; private final boolean drag; /** - * Defaults timeoutMillis to 0. + * Defaults timeoutMillis to 1000. */ public SwipeAction(ScrollDirection direction, boolean drag) { - this(direction, drag, 0L); + this(direction, drag, 1000L); } public SwipeAction(ScrollDirection direction, boolean drag, long timeoutMillis) { @@ -106,4 +129,13 @@ public class SwipeAction extends ScrollAction { Events.touchUp(injector, downTime, endX, endY); return true; } + + @Override + public String toString() { + ToStringHelper toStringHelper = Objects.toStringHelper(this).addValue(direction); + if (drag) { + toStringHelper.addValue("drag"); + } + return toStringHelper.toString(); + } } diff --git a/src/com/google/android/droiddriver/actions/TypeAction.java b/src/com/google/android/droiddriver/actions/TypeAction.java index 55e8dea..912b58f 100644 --- a/src/com/google/android/droiddriver/actions/TypeAction.java +++ b/src/com/google/android/droiddriver/actions/TypeAction.java @@ -37,19 +37,21 @@ public class TypeAction extends KeyAction { private final String text; /** - * Defaults timeoutMillis to 0. + * Defaults timeoutMillis to 100. */ public TypeAction(String text) { - this(text, 0L); + this(text, 100L, false); } - public TypeAction(String text, long timeoutMillis) { - super(timeoutMillis); + public TypeAction(String text, long timeoutMillis, boolean checkFocused) { + super(timeoutMillis, checkFocused); this.text = Preconditions.checkNotNull(text); } @Override public boolean perform(InputInjector injector, UiElement element) { + maybeCheckFocused(element); + // TODO: recycle events? KeyEvent[] events = KEY_CHAR_MAP.getEvents(text.toCharArray()); boolean success = false; diff --git a/src/com/google/android/droiddriver/base/AbstractContext.java b/src/com/google/android/droiddriver/base/AbstractContext.java index 2c831ee..a0b8b4a 100644 --- a/src/com/google/android/droiddriver/base/AbstractContext.java +++ b/src/com/google/android/droiddriver/base/AbstractContext.java @@ -16,18 +16,33 @@ package com.google.android.droiddriver.base; +import android.app.Instrumentation; + import com.google.android.droiddriver.InputInjector; /** * Internal helper for managing all instances. */ public abstract class AbstractContext { + protected final Instrumentation instrumentation; + protected final AbstractDroidDriver driver; protected final InputInjector injector; - protected AbstractContext(InputInjector injector) { + protected AbstractContext(Instrumentation instrumentation, AbstractDroidDriver driver, + InputInjector injector) { + this.instrumentation = instrumentation; + this.driver = driver; this.injector = injector; } + public Instrumentation getInstrumentation() { + return instrumentation; + } + + public AbstractDroidDriver getDriver() { + return driver; + } + public InputInjector getInjector() { return injector; } diff --git a/src/com/google/android/droiddriver/base/AbstractDroidDriver.java b/src/com/google/android/droiddriver/base/AbstractDroidDriver.java index ed78b46..0b0f9c6 100644 --- a/src/com/google/android/droiddriver/base/AbstractDroidDriver.java +++ b/src/com/google/android/droiddriver/base/AbstractDroidDriver.java @@ -16,7 +16,6 @@ package com.google.android.droiddriver.base; -import android.app.Instrumentation; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.util.Log; @@ -32,7 +31,6 @@ import com.google.android.droiddriver.finders.Finder; import com.google.android.droiddriver.util.DefaultPoller; import com.google.android.droiddriver.util.FileUtils; import com.google.android.droiddriver.util.Logs; -import com.google.common.base.Preconditions; import java.io.BufferedOutputStream; @@ -42,14 +40,9 @@ import java.io.BufferedOutputStream; */ public abstract class AbstractDroidDriver implements DroidDriver, Screenshotter { - protected final Instrumentation instrumentation; private Poller poller = new DefaultPoller(); private AbstractUiElement rootElement; - protected AbstractDroidDriver(Instrumentation instrumentation) { - this.instrumentation = Preconditions.checkNotNull(instrumentation); - } - @Override public UiElement find(Finder finder) { Logs.call(this, "find", finder); diff --git a/src/com/google/android/droiddriver/base/AbstractUiElement.java b/src/com/google/android/droiddriver/base/AbstractUiElement.java index 65048f2..780d4ac 100644 --- a/src/com/google/android/droiddriver/base/AbstractUiElement.java +++ b/src/com/google/android/droiddriver/base/AbstractUiElement.java @@ -90,7 +90,6 @@ public abstract class AbstractUiElement implements UiElement { @Override public void setText(String text) { - // TODO: Define common actions as a const. perform(new TypeAction(text)); // TypeAction may not be effective immediately and reflected bygetText(), // so the following will fail. @@ -120,7 +119,7 @@ public abstract class AbstractUiElement implements UiElement { @Override public void scroll(ScrollDirection direction) { - perform(new SwipeAction(direction, false)); + perform(SwipeAction.toScroll(direction)); } @Override diff --git a/src/com/google/android/droiddriver/base/BaseUiDevice.java b/src/com/google/android/droiddriver/base/BaseUiDevice.java new file mode 100644 index 0000000..29320b2 --- /dev/null +++ b/src/com/google/android/droiddriver/base/BaseUiDevice.java @@ -0,0 +1,70 @@ +/* + * 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 android.app.Service; +import android.os.PowerManager; +import android.view.KeyEvent; + +import com.google.android.droiddriver.UiDevice; +import com.google.android.droiddriver.actions.Action; +import com.google.android.droiddriver.actions.PressKeyAction; + +/** + * Base implementation of {@link UiDevice}. + */ +public class BaseUiDevice implements UiDevice { + // power off may not trigger new events + private static final PressKeyAction POWER_OFF = new PressKeyAction(KeyEvent.KEYCODE_POWER, 0, + false); + // power on should always trigger new events + private static final PressKeyAction POWER_ON = new PressKeyAction(KeyEvent.KEYCODE_POWER, 1000L, + false); + + private final AbstractContext abstractContext; + + public BaseUiDevice(AbstractContext abstractContext) { + this.abstractContext = abstractContext; + } + + @Override + public boolean isScreenOn() { + PowerManager pm = + (PowerManager) abstractContext.instrumentation.getTargetContext().getSystemService( + Service.POWER_SERVICE); + return pm.isScreenOn(); + } + + @Override + public void wakeUp() { + if (!isScreenOn()) { + perform(POWER_ON); + } + } + + @Override + public void sleep() { + if (isScreenOn()) { + perform(POWER_OFF); + } + } + + @Override + public boolean perform(Action action) { + return abstractContext.driver.getRootElement().perform(action); + } +} diff --git a/src/com/google/android/droiddriver/finders/XPaths.java b/src/com/google/android/droiddriver/finders/XPaths.java index c401582..b671aca 100644 --- a/src/com/google/android/droiddriver/finders/XPaths.java +++ b/src/com/google/android/droiddriver/finders/XPaths.java @@ -95,6 +95,11 @@ public class XPaths { return attr(Attribute.TEXT, value); } + /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */ + public static String resourceId(String value) { + return attr(Attribute.RESOURCE_ID, value); + } + /** * Adapted from http://stackoverflow.com/questions/1341847/. * <p> diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java index 0c4e9dc..3ed7e20 100644 --- a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java +++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java @@ -35,8 +35,8 @@ import java.util.Map; public class InstrumentationContext extends AbstractContext { private final Map<View, ViewElement> map = new MapMaker().weakKeys().weakValues().makeMap(); - InstrumentationContext(final Instrumentation instrumentation) { - super(new InputInjector() { + InstrumentationContext(final Instrumentation instrumentation, InstrumentationDriver driver) { + super(instrumentation, driver, new InputInjector() { @Override public boolean injectInputEvent(InputEvent event) { if (event instanceof MotionEvent) { diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java index c4f1fbb..078c4bf 100644 --- a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java +++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java @@ -22,9 +22,12 @@ import android.graphics.Bitmap; import android.os.SystemClock; import android.view.View; +import com.google.android.droiddriver.UiDevice; import com.google.android.droiddriver.base.AbstractDroidDriver; +import com.google.android.droiddriver.base.BaseUiDevice; import com.google.android.droiddriver.exceptions.TimeoutException; import com.google.android.droiddriver.util.ActivityUtils; +import com.google.common.base.Preconditions; import com.google.common.primitives.Longs; /** @@ -32,10 +35,13 @@ import com.google.common.primitives.Longs; */ public class InstrumentationDriver extends AbstractDroidDriver { private final InstrumentationContext context; + private final Instrumentation instrumentation; + private final BaseUiDevice uiDevice; public InstrumentationDriver(Instrumentation instrumentation) { - super(instrumentation); - this.context = new InstrumentationContext(instrumentation); + this.instrumentation = Preconditions.checkNotNull(instrumentation); + this.context = new InstrumentationContext(instrumentation, this); + uiDevice = new BaseUiDevice(context); } @Override @@ -102,4 +108,9 @@ public class InstrumentationDriver extends AbstractDroidDriver { instrumentation.runOnMainSync(screenshotRunnable); return screenshotRunnable.screenshot; } + + @Override + public UiDevice getUiDevice() { + return uiDevice; + } } diff --git a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java index c166c1e..779a41f 100644 --- a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java +++ b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java @@ -88,6 +88,15 @@ public class DynamicSentinelStrategy extends AbstractSentinelStrategy { @Override public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) { + // If the sentinel moved, scrolling has some effect. This is both an + // optimization - getBounds is cheaper than find - and necessary in + // certain cases, e.g. user is looking for a sibling of the unique string; + // the scroll is close to the end therefore the unique string does not + // change, but the target could be revealed. + if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) { + return true; + } + String newString = getUniqueStringFromSentinel(newSentinel); // If newString is null, newSentinel must be partially shown. In this case // we return true to allow further scrolling. But program error could also diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java index 7efa48e..57a64d5 100644 --- a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java +++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java @@ -16,6 +16,7 @@ package com.google.android.droiddriver.uiautomation; +import android.app.Instrumentation; import android.app.UiAutomation; import android.view.InputEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -35,14 +36,14 @@ public class UiAutomationContext extends AbstractContext { .weakValues().makeMap(); private final UiAutomation uiAutomation; - UiAutomationContext(final UiAutomation uiAutomation) { - super(new InputInjector() { + UiAutomationContext(final Instrumentation instrumentation, UiAutomationDriver driver) { + super(instrumentation, driver, new InputInjector() { @Override public boolean injectInputEvent(InputEvent event) { - return uiAutomation.injectInputEvent(event, true /* sync */); + return instrumentation.getUiAutomation().injectInputEvent(event, true /* sync */); } }); - this.uiAutomation = uiAutomation; + this.uiAutomation = instrumentation.getUiAutomation(); } public UiAutomationElement getUiElement(AccessibilityNodeInfo node) { diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java index 7d21233..8b2baaf 100644 --- a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java +++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java @@ -16,13 +16,18 @@ package com.google.android.droiddriver.uiautomation; -import android.app.UiAutomation; import android.app.Instrumentation; +import android.app.UiAutomation; +import android.content.Context; import android.graphics.Bitmap; import android.os.SystemClock; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import com.google.android.droiddriver.UiDevice; import com.google.android.droiddriver.base.AbstractDroidDriver; +import com.google.android.droiddriver.base.BaseUiDevice; import com.google.android.droiddriver.exceptions.TimeoutException; import com.google.common.primitives.Longs; @@ -40,11 +45,12 @@ public class UiAutomationDriver extends AbstractDroidDriver { private final UiAutomationContext context; private final UiAutomation uiAutomation; + private final BaseUiDevice uiDevice; public UiAutomationDriver(Instrumentation instrumentation) { - super(instrumentation); this.uiAutomation = instrumentation.getUiAutomation(); - this.context = new UiAutomationContext(uiAutomation); + this.context = new UiAutomationContext(instrumentation, this); + uiDevice = new BaseUiDevice(context); } @Override @@ -84,4 +90,33 @@ public class UiAutomationDriver extends AbstractDroidDriver { protected Bitmap takeScreenshot() { return uiAutomation.takeScreenshot(); } + + /** + * 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() { + 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() { + AccessibilityManager accessibilityManager = + (AccessibilityManager) context.getInstrumentation().getTargetContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + accessibilityManager.sendAccessibilityEvent(AccessibilityEvent + .obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)); + } + + @Override + public UiDevice getUiDevice() { + return uiDevice; + } } diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java index a399360..a28498c 100644 --- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java +++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java @@ -189,6 +189,13 @@ public class UiAutomationElement extends AbstractUiElement { // 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. + ((UiAutomationDriver) context.getDriver()).clearAccessibilityNodeInfoCacheHack(); } } } |