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