diff options
author | Kevin Jin <kjin@google.com> | 2013-11-05 18:05:39 -0800 |
---|---|---|
committer | Kevin Jin <kjin@google.com> | 2013-11-06 10:06:48 -0800 |
commit | 9c92f46280cf3943701e75349833c68b584992e2 (patch) | |
tree | dab04f0fd3b4f9951d066066e4e3dde08161f6f4 | |
parent | 2131c9b43a72432c5c2ac6636433c12050141221 (diff) | |
download | droiddriver-9c92f46280cf3943701e75349833c68b584992e2.tar.gz |
introduce AccessibilityEventScrollStepStrategy which is
a simple ScrollStepStrategy for UiAutomation and behaves like UiScrollable.
rename SentinelScroller to StepBasedScroller
Change-Id: I424140817d53c63165a66a5fffb5cae24c47288b
14 files changed, 474 insertions, 282 deletions
diff --git a/src/com/google/android/droiddriver/UiElement.java b/src/com/google/android/droiddriver/UiElement.java index 1ae6254..924197f 100644 --- a/src/com/google/android/droiddriver/UiElement.java +++ b/src/com/google/android/droiddriver/UiElement.java @@ -19,6 +19,7 @@ package com.google.android.droiddriver; import android.graphics.Rect; import com.google.android.droiddriver.actions.Action; +import com.google.android.droiddriver.actions.InputInjector; import com.google.android.droiddriver.exceptions.ElementNotVisibleException; import com.google.android.droiddriver.finders.Attribute; import com.google.android.droiddriver.instrumentation.InstrumentationDriver; @@ -225,4 +226,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/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java index 6a2f88c..9cddc71 100644 --- a/src/com/google/android/droiddriver/base/BaseUiElement.java +++ b/src/com/google/android/droiddriver/base/BaseUiElement.java @@ -211,8 +211,6 @@ public abstract class BaseUiElement implements UiElement { protected abstract List<? extends BaseUiElement> getChildren(); - protected abstract InputInjector getInjector(); - private void checkVisible() { if (!isVisible()) { throw new ElementNotVisibleException(this); diff --git a/src/com/google/android/droiddriver/helpers/ScrollerHelper.java b/src/com/google/android/droiddriver/helpers/ScrollerHelper.java new file mode 100644 index 0000000..dc946e7 --- /dev/null +++ b/src/com/google/android/droiddriver/helpers/ScrollerHelper.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.droiddriver.helpers; + +import com.google.android.droiddriver.DroidDriver; +import com.google.android.droiddriver.UiElement; +import com.google.android.droiddriver.exceptions.ElementNotFoundException; +import com.google.android.droiddriver.finders.Finder; +import com.google.android.droiddriver.scroll.Scroller; + +/** + * Helper for Scroller. + */ +public class ScrollerHelper { + private final DroidDriver driver; + private final Finder containerFinder; + private final Scroller scroller; + + public ScrollerHelper(Scroller scroller, DroidDriver driver, Finder containerFinder) { + this.scroller = scroller; + this.driver = driver; + this.containerFinder = containerFinder; + } + + public UiElement scrollTo(Finder itemFinder) { + return scroller.scrollTo(driver, containerFinder, itemFinder); + } + + public boolean canScrollTo(Finder itemFinder) { + try { + scrollTo(itemFinder); + return true; + } catch (ElementNotFoundException e) { + return false; + } + } +} diff --git a/src/com/google/android/droiddriver/instrumentation/ViewElement.java b/src/com/google/android/droiddriver/instrumentation/ViewElement.java index 186681f..070d14f 100644 --- a/src/com/google/android/droiddriver/instrumentation/ViewElement.java +++ b/src/com/google/android/droiddriver/instrumentation/ViewElement.java @@ -255,7 +255,7 @@ public class ViewElement extends BaseUiElement { } @Override - protected InputInjector getInjector() { + public InputInjector getInjector() { return context.getInjector(); } } diff --git a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java deleted file mode 100644 index 2c339ad..0000000 --- a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java +++ /dev/null @@ -1,202 +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.scroll; - -import android.util.Log; - -import com.google.android.droiddriver.DroidDriver; -import com.google.android.droiddriver.UiElement; -import com.google.android.droiddriver.exceptions.ElementNotFoundException; -import com.google.android.droiddriver.finders.By; -import com.google.android.droiddriver.finders.Finder; -import com.google.android.droiddriver.scroll.Direction.DirectionConverter; -import com.google.android.droiddriver.scroll.Direction.LogicalDirection; -import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; -import com.google.android.droiddriver.util.Logs; -import com.google.common.base.Predicate; -import com.google.common.base.Predicates; - -import java.util.List; - -/** - * Base {@link SentinelStrategy} for common code. - */ -public abstract class AbstractSentinelStrategy implements SentinelStrategy { - - /** - * Gets sentinel based on {@link Predicate}. - */ - public static abstract class GetStrategy { - protected final Predicate<? super UiElement> predicate; - protected final String description; - - protected GetStrategy(Predicate<? super UiElement> predicate, String description) { - this.predicate = predicate; - this.description = description; - } - - /** - * Gets the sentinel, which must be an immediate child of {@code container} - * -- not a descendant. Note this could be null if {@code container} has not - * finished updating. - */ - public UiElement getSentinel(UiElement container) { - return getSentinel(container.getChildren(predicate)); - } - - protected abstract UiElement getSentinel(List<? extends UiElement> children); - - @Override - public String toString() { - return description; - } - } - - /** - * Decorates an existing {@link GetStrategy} by adding another - * {@link Predicate}. - */ - public static class MorePredicateGetStrategy extends GetStrategy { - private final GetStrategy original; - - public MorePredicateGetStrategy(GetStrategy original, - Predicate<? super UiElement> extraPredicate, String extraDescription) { - super(Predicates.and(original.predicate, extraPredicate), extraDescription - + original.description); - this.original = original; - } - - @Override - protected UiElement getSentinel(List<? extends UiElement> children) { - return original.getSentinel(children); - } - } - - /** - * Returns the first child as the sentinel. - */ - public static final GetStrategy FIRST_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(), - "FIRST_CHILD") { - @Override - protected UiElement getSentinel(List<? extends UiElement> children) { - return children.isEmpty() ? null : children.get(0); - } - }; - - /** - * Returns the last child as the sentinel. - */ - public static final GetStrategy LAST_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(), - "LAST_CHILD") { - @Override - protected UiElement getSentinel(List<? extends UiElement> children) { - return children.isEmpty() ? null : children.get(children.size() - 1); - } - }; - - /** - * Returns the second last child as the sentinel. Useful when the activity - * always shows the last child as an anchor (for example a footer). - * <p> - * Sometimes uiautomatorviewer may not show the anchor as the last child, due - * to the reordering by layout described in {@link UiElement#getChildren}. - * This is not a problem with UiAutomationDriver because it sees the same as - * uiautomatorviewer does, but could be a problem with InstrumentationDriver. - * </p> - */ - public static final GetStrategy SECOND_LAST_CHILD_GETTER = new GetStrategy( - Predicates.alwaysTrue(), "SECOND_LAST_CHILD") { - @Override - protected UiElement getSentinel(List<? extends UiElement> children) { - return children.size() < 2 ? null : children.get(children.size() - 2); - } - }; - - /** - * Returns the second child as the sentinel. Useful when the activity shows a - * fixed first child. - */ - public static final GetStrategy SECOND_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(), - "SECOND_CHILD") { - @Override - protected UiElement getSentinel(List<? extends UiElement> children) { - return children.size() <= 1 ? null : children.get(1); - } - }; - - // Make sure sentinel exists in container - private static class SentinelFinder implements Finder { - private final GetStrategy getStrategy; - - public SentinelFinder(GetStrategy getStrategy) { - this.getStrategy = getStrategy; - } - - @Override - public UiElement find(UiElement container) { - UiElement sentinel = getStrategy.getSentinel(container); - if (sentinel == null) { - throw new ElementNotFoundException(this); - } - Logs.log(Log.INFO, "Found match: " + sentinel); - return sentinel; - } - - @Override - public String toString() { - return String.format("SentinelFinder{%s}", getStrategy); - } - } - - private final GetStrategy backwardGetStrategy; - private final GetStrategy forwardGetStrategy; - private final DirectionConverter directionConverter; - private final SentinelFinder backwardSentinelFinder; - private final SentinelFinder forwardSentinelFinder; - - public AbstractSentinelStrategy(GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy, - DirectionConverter directionConverter) { - this.backwardGetStrategy = backwardGetStrategy; - this.forwardGetStrategy = forwardGetStrategy; - this.directionConverter = directionConverter; - this.backwardSentinelFinder = new SentinelFinder(backwardGetStrategy); - this.forwardSentinelFinder = new SentinelFinder(forwardGetStrategy); - } - - protected UiElement getSentinel(DroidDriver driver, Finder containerFinder, - PhysicalDirection direction) { - Logs.call(this, "getSentinel", driver, containerFinder, direction); - Finder sentinelFinder; - LogicalDirection logicalDirection = directionConverter.toLogicalDirection(direction); - if (logicalDirection == LogicalDirection.BACKWARD) { - sentinelFinder = By.chain(containerFinder, backwardSentinelFinder); - } else { - sentinelFinder = By.chain(containerFinder, forwardSentinelFinder); - } - return driver.on(sentinelFinder); - } - - @Override - public final DirectionConverter getDirectionConverter() { - return directionConverter; - } - - @Override - public String toString() { - return String.format("{backwardGetStrategy=%s, forwardGetStrategy=%s}", backwardGetStrategy, - forwardGetStrategy); - } -} diff --git a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java new file mode 100644 index 0000000..0333d77 --- /dev/null +++ b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java @@ -0,0 +1,91 @@ +/* + * 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.scroll; + +import android.app.UiAutomation; +import android.app.UiAutomation.AccessibilityEventFilter; +import android.view.accessibility.AccessibilityEvent; + +import com.google.android.droiddriver.DroidDriver; +import com.google.android.droiddriver.UiElement; +import com.google.android.droiddriver.actions.SwipeAction; +import com.google.android.droiddriver.finders.Finder; +import com.google.android.droiddriver.scroll.Direction.DirectionConverter; +import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; + +import java.util.concurrent.TimeoutException; + +/** + * A {@link ScrollStepStrategy} that determines whether more scrolling is + * possible by checking the {@link AccessibilityEvent} returned by + * {@link android.app.UiAutomation}. + * <p> + * This implementation behaves just like the <a href= + * "http://developer.android.com/tools/help/uiautomator/UiScrollable.html" + * >UiScrollable</a> class. It may not work in all cases. For instance, + * sometimes {@link android.support.v4.widget.DrawerLayout} does not send + * correct {@link AccessibilityEvent}s after scrolling. + * </p> + */ +public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy { + private static final AccessibilityEventFilter SCROLL_EVENT_FILTER = + new AccessibilityEventFilter() { + @Override + public boolean accept(AccessibilityEvent arg0) { + return (arg0.getEventType() & AccessibilityEvent.TYPE_VIEW_SCROLLED) != 0; + } + }; + + private final UiAutomation uiAutomation; + private final long scrollEventTimeoutMillis; + private final DirectionConverter directionConverter; + + public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation, + long scrollEventTimeoutMillis, DirectionConverter converter) { + this.uiAutomation = uiAutomation; + this.scrollEventTimeoutMillis = scrollEventTimeoutMillis; + this.directionConverter = converter; + } + + @Override + public boolean scroll(DroidDriver driver, Finder containerFinder, + final PhysicalDirection direction) { + final UiElement container = driver.on(containerFinder); + try { + uiAutomation.executeAndWaitForEvent(new Runnable() { + @Override + public void run() { + SwipeAction.toScroll(direction).perform(container.getInjector(), container); + } + }, SCROLL_EVENT_FILTER, scrollEventTimeoutMillis); + } catch (TimeoutException e) { + // If no TYPE_VIEW_SCROLLED event, no more scrolling is possible + return false; + } + return true; + } + + @Override + public final DirectionConverter getDirectionConverter() { + return directionConverter; + } + + @Override + public String toString() { + return String.format("AccessibilityEventScrollStepStrategy{scrollEventTimeoutMillis=%d}", + scrollEventTimeoutMillis); + } +} diff --git a/src/com/google/android/droiddriver/scroll/BaseSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/BaseSentinelStrategy.java new file mode 100644 index 0000000..709a068 --- /dev/null +++ b/src/com/google/android/droiddriver/scroll/BaseSentinelStrategy.java @@ -0,0 +1,96 @@ +/* + * 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.scroll; + +import android.util.Log; + +import com.google.android.droiddriver.DroidDriver; +import com.google.android.droiddriver.UiElement; +import com.google.android.droiddriver.exceptions.ElementNotFoundException; +import com.google.android.droiddriver.finders.By; +import com.google.android.droiddriver.finders.Finder; +import com.google.android.droiddriver.scroll.Direction.DirectionConverter; +import com.google.android.droiddriver.scroll.Direction.LogicalDirection; +import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; +import com.google.android.droiddriver.util.Logs; + +/** + * Base class for {@link SentinelStrategy}. + */ +public abstract class BaseSentinelStrategy implements SentinelStrategy { + + // Make sure sentinel exists in container + private static class SentinelFinder implements Finder { + private final Getter getter; + + public SentinelFinder(Getter getter) { + this.getter = getter; + } + + @Override + public UiElement find(UiElement container) { + UiElement sentinel = getter.getSentinel(container); + if (sentinel == null) { + throw new ElementNotFoundException(this); + } + Logs.log(Log.INFO, "Found match: " + sentinel); + return sentinel; + } + + @Override + public String toString() { + return String.format("SentinelFinder{%s}", getter); + } + } + + private final Getter backwardGetter; + private final Getter forwardGetter; + private final DirectionConverter directionConverter; + private final SentinelFinder backwardSentinelFinder; + private final SentinelFinder forwardSentinelFinder; + + protected BaseSentinelStrategy(Getter backwardGetter, Getter forwardGetter, + DirectionConverter directionConverter) { + this.backwardGetter = backwardGetter; + this.forwardGetter = forwardGetter; + this.directionConverter = directionConverter; + this.backwardSentinelFinder = new SentinelFinder(backwardGetter); + this.forwardSentinelFinder = new SentinelFinder(forwardGetter); + } + + protected UiElement getSentinel(DroidDriver driver, Finder containerFinder, + PhysicalDirection direction) { + Logs.call(this, "getSentinel", driver, containerFinder, direction); + Finder sentinelFinder; + LogicalDirection logicalDirection = directionConverter.toLogicalDirection(direction); + if (logicalDirection == LogicalDirection.BACKWARD) { + sentinelFinder = By.chain(containerFinder, backwardSentinelFinder); + } else { + sentinelFinder = By.chain(containerFinder, forwardSentinelFinder); + } + return driver.on(sentinelFinder); + } + + @Override + public final DirectionConverter getDirectionConverter() { + return directionConverter; + } + + @Override + public String toString() { + return String.format("{backwardGetter=%s, forwardGetter=%s}", backwardGetter, forwardGetter); + } +} diff --git a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java index 3ae0e50..bf8c6d3 100644 --- a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java +++ b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java @@ -34,7 +34,7 @@ import com.google.common.base.Objects; * used, which skips invisible children, or in the case of dynamic list, which * shows more items when scrolling beyond the end. */ -public class DynamicSentinelStrategy extends AbstractSentinelStrategy { +public class DynamicSentinelStrategy extends BaseSentinelStrategy { /** * Interface for determining whether sentinel is updated. @@ -100,7 +100,7 @@ public class DynamicSentinelStrategy extends AbstractSentinelStrategy { String newString = getUniqueStringFromSentinel(newSentinel); // A legitimate case for newString being null is when newSentinel is // partially shown. We return true to allow further scrolling. But program - // error could also cause this, e.g. a bad choice of GetStrategy, which + // error could also cause this, e.g. a bad choice of Getter, which // results in unnecessary scroll actions that have no visual effect. This // log helps troubleshooting in the latter case. if (newString == null) { @@ -176,36 +176,32 @@ public class DynamicSentinelStrategy extends AbstractSentinelStrategy { private final IsUpdatedStrategy isUpdatedStrategy; /** - * Constructs with {@code GetStrategy}s that decorate the given - * {@code GetStrategy}s with {@link UiElement#VISIBLE}, and the given - * {@code isUpdatedStrategy} and {@code directionConverter}. Be careful with - * {@code GetStrategy}s: the sentinel after each scroll should be unique. + * Constructs with {@code Getter}s that decorate the given {@code Getter}s + * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and + * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel + * after each scroll should be unique. */ - public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, - GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy, - DirectionConverter directionConverter) { - super(new MorePredicateGetStrategy(backwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"), - new MorePredicateGetStrategy(forwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"), - directionConverter); + public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, + Getter forwardGetter, DirectionConverter directionConverter) { + super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE, "VISIBLE_"), + new MorePredicateGetter(forwardGetter, UiElement.VISIBLE, "VISIBLE_"), directionConverter); this.isUpdatedStrategy = isUpdatedStrategy; } /** * Defaults to the standard {@link DirectionConverter}. */ - public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, - GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy) { - this(isUpdatedStrategy, backwardGetStrategy, forwardGetStrategy, - DirectionConverter.STANDARD_CONVERTER); + public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, + Getter forwardGetter) { + this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER); } /** * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard * {@link DirectionConverter}. */ - public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, - GetStrategy backwardGetStrategy) { - this(isUpdatedStrategy, backwardGetStrategy, LAST_CHILD_GETTER, + public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) { + this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER, DirectionConverter.STANDARD_CONVERTER); } diff --git a/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java new file mode 100644 index 0000000..5bdeb83 --- /dev/null +++ b/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java @@ -0,0 +1,52 @@ +/* + * 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.scroll; + +import com.google.android.droiddriver.DroidDriver; +import com.google.android.droiddriver.finders.Finder; +import com.google.android.droiddriver.scroll.Direction.DirectionConverter; +import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; + +/** + * Interface for determining whether scrolling is possible. + */ +public interface ScrollStepStrategy { + /** + * Tries to scroll {@code containerFinder} in {@code direction}. Returns + * whether scrolling is effective. + * + * @param driver + * @param containerFinder Finder for the container that can scroll, for + * instance a ListView + * @param direction + * @return whether scrolling is effective + */ + boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction); + + /** + * Returns the {@link DirectionConverter}. + */ + DirectionConverter getDirectionConverter(); + + /** + * {@inheritDoc} + * + * <p> + * It is recommended that this method return a description to help debugging. + */ + @Override + String toString(); +} diff --git a/src/com/google/android/droiddriver/scroll/Scrollers.java b/src/com/google/android/droiddriver/scroll/Scrollers.java new file mode 100644 index 0000000..ddf9610 --- /dev/null +++ b/src/com/google/android/droiddriver/scroll/Scrollers.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.droiddriver.scroll; + +import android.app.UiAutomation; + +import com.google.android.droiddriver.scroll.Direction.DirectionConverter; + +/** + * Static utility methods pertaining to {@link Scroller} instances. + */ +public class Scrollers { + /** + * Returns a new default Scroller that works in simple cases. In complex cases + * you may try a {@link StepBasedScroller} with a custom + * {@link ScrollStepStrategy}: + * <ul> + * <li>If the Scroller is used with InstrumentationDriver, + * StaticSentinelStrategy may work and it's the simplest.</li> + * <li>Otherwise, DynamicSentinelStrategy should work in all cases, including + * the case of dynamic list, which shows more items when scrolling beyond the + * end. On the other hand, it's complex and needs more configuration.</li> + * </ul> + */ + public static Scroller newScroller(UiAutomation uiAutomation) { + if (uiAutomation != null) { + return new StepBasedScroller(new AccessibilityEventScrollStepStrategy(uiAutomation, 1000L, + DirectionConverter.STANDARD_CONVERTER)); + } + // TODO: A {@link Scroller} that directly jumps to the view if an + // InstrumentationDriver is used. + return new StepBasedScroller(StaticSentinelStrategy.DEFAULT); + } +} diff --git a/src/com/google/android/droiddriver/scroll/SentinelStrategy.java b/src/com/google/android/droiddriver/scroll/SentinelStrategy.java index 33c19e3..0330844 100644 --- a/src/com/google/android/droiddriver/scroll/SentinelStrategy.java +++ b/src/com/google/android/droiddriver/scroll/SentinelStrategy.java @@ -15,38 +15,110 @@ */ package com.google.android.droiddriver.scroll; -import com.google.android.droiddriver.DroidDriver; -import com.google.android.droiddriver.finders.Finder; -import com.google.android.droiddriver.scroll.Direction.DirectionConverter; -import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; +import com.google.android.droiddriver.UiElement; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +import java.util.List; /** * Interface for determining whether scrolling is possible based on a sentinel. */ -public interface SentinelStrategy { +public interface SentinelStrategy extends ScrollStepStrategy { + /** - * Tries to scroll {@code containerFinder} in {@code direction}. Returns - * whether scrolling is effective. - * - * @param driver - * @param containerFinder Finder for the container that can scroll, for - * instance a ListView - * @param direction - * @return whether scrolling is effective + * Gets sentinel based on {@link Predicate}. */ - boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction); + public static abstract class Getter { + protected final Predicate<? super UiElement> predicate; + protected final String description; + + protected Getter(Predicate<? super UiElement> predicate, String description) { + this.predicate = predicate; + this.description = description; + } + + /** + * Gets the sentinel, which must be an immediate child of {@code container} + * -- not a descendant. Note this could be null if {@code container} has not + * finished updating. + */ + public UiElement getSentinel(UiElement container) { + return getSentinel(container.getChildren(predicate)); + } + + protected abstract UiElement getSentinel(List<? extends UiElement> children); + + @Override + public String toString() { + return description; + } + } /** - * Returns the {@link DirectionConverter}. + * Decorates an existing {@link Getter} by adding another {@link Predicate}. */ - DirectionConverter getDirectionConverter(); + public static class MorePredicateGetter extends Getter { + private final Getter original; + + public MorePredicateGetter(Getter original, Predicate<? super UiElement> extraPredicate, + String extraDescription) { + super(Predicates.and(original.predicate, extraPredicate), extraDescription + + original.description); + this.original = original; + } + @Override + protected UiElement getSentinel(List<? extends UiElement> children) { + return original.getSentinel(children); + } + } + + /** + * Returns the first child as the sentinel. + */ + public static final Getter FIRST_CHILD_GETTER = + new Getter(Predicates.alwaysTrue(), "FIRST_CHILD") { + @Override + protected UiElement getSentinel(List<? extends UiElement> children) { + return children.isEmpty() ? null : children.get(0); + } + }; /** - * {@inheritDoc} - * + * Returns the last child as the sentinel. + */ + public static final Getter LAST_CHILD_GETTER = new Getter(Predicates.alwaysTrue(), "LAST_CHILD") { + @Override + protected UiElement getSentinel(List<? extends UiElement> children) { + return children.isEmpty() ? null : children.get(children.size() - 1); + } + }; + /** + * Returns the second last child as the sentinel. Useful when the activity + * always shows the last child as an anchor (for example a footer). * <p> - * It is recommended that this method return a description to help debugging. + * Sometimes uiautomatorviewer may not show the anchor as the last child, due + * to the reordering by layout described in {@link UiElement#getChildren}. + * This is not a problem with UiAutomationDriver because it sees the same as + * uiautomatorviewer does, but could be a problem with InstrumentationDriver. + * </p> + */ + public static final Getter SECOND_LAST_CHILD_GETTER = new Getter(Predicates.alwaysTrue(), + "SECOND_LAST_CHILD") { + @Override + protected UiElement getSentinel(List<? extends UiElement> children) { + return children.size() < 2 ? null : children.get(children.size() - 2); + } + }; + /** + * Returns the second child as the sentinel. Useful when the activity shows a + * fixed first child. */ - @Override - String toString(); + public static final Getter SECOND_CHILD_GETTER = new Getter(Predicates.alwaysTrue(), + "SECOND_CHILD") { + @Override + protected UiElement getSentinel(List<? extends UiElement> children) { + return children.size() <= 1 ? null : children.get(1); + } + }; } diff --git a/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java index 35e6f29..3393e43 100644 --- a/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java +++ b/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java @@ -34,18 +34,17 @@ import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; * This does not work if a child is larger than the physical size of the * container. */ -public class StaticSentinelStrategy extends AbstractSentinelStrategy { +public class StaticSentinelStrategy extends BaseSentinelStrategy { /** * Defaults to FIRST_CHILD_GETTER for backward scrolling, LAST_CHILD_GETTER * for forward scrolling, and the standard {@link DirectionConverter}. */ - public StaticSentinelStrategy() { - super(FIRST_CHILD_GETTER, LAST_CHILD_GETTER, DirectionConverter.STANDARD_CONVERTER); - } + public static final StaticSentinelStrategy DEFAULT = new StaticSentinelStrategy( + FIRST_CHILD_GETTER, LAST_CHILD_GETTER, DirectionConverter.STANDARD_CONVERTER); - public StaticSentinelStrategy(GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy, + public StaticSentinelStrategy(Getter backwardGetter, Getter forwardGetter, DirectionConverter directionConverter) { - super(backwardGetStrategy, forwardGetStrategy, directionConverter); + super(backwardGetter, forwardGetter, directionConverter); } @Override diff --git a/src/com/google/android/droiddriver/scroll/SentinelScroller.java b/src/com/google/android/droiddriver/scroll/StepBasedScroller.java index 273d6fa..07d7fb6 100644 --- a/src/com/google/android/droiddriver/scroll/SentinelScroller.java +++ b/src/com/google/android/droiddriver/scroll/StepBasedScroller.java @@ -37,19 +37,11 @@ import com.google.android.droiddriver.util.Logs; /** * A {@link Scroller} that looks for the desired item in the currently shown * content of the scrollable container, otherwise scrolls the container one step - * at a time and look again, until we cannot scroll any more. A - * {@link SentinelStrategy} is used to determine whether more scrolling is + * at a time and looks again, until we cannot scroll any more. A + * {@link ScrollStepStrategy} is used to determine whether more scrolling is * possible. - * <p> - * This implementation may not work well with InstrumentationDriver if - * {@link UiElement#getChildren} returns children in an order different from the - * Accessibility API. In this case you may try - * {@link AbstractSentinelStrategy#SECOND_LAST_CHILD_GETTER}. - * </p> - * TODO: A {@link Scroller} that directly jumps to the item if an - * InstrumentationDriver is used. */ -public class SentinelScroller implements Scroller { +public class StepBasedScroller implements Scroller { private static final SingleKeyAction MOVE_HOME = new SingleKeyAction(KeyEvent.KEYCODE_MOVE_HOME, 1000L, false); @@ -57,7 +49,7 @@ public class SentinelScroller implements Scroller { private final int maxScrolls; private final long perScrollTimeoutMillis; private final Axis axis; - private final SentinelStrategy sentinelStrategy; + private final ScrollStepStrategy scrollStepStrategy; private final boolean startFromBeginning; /** @@ -72,29 +64,21 @@ public class SentinelScroller implements Scroller { * location and scrolling in both directions. It may not always work, * but when it works, it is faster. */ - public SentinelScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis, - SentinelStrategy sentinelStrategy, boolean startFromBeginning) { + public StepBasedScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis, + ScrollStepStrategy scrollStepStrategy, boolean startFromBeginning) { this.maxScrolls = maxScrolls; this.perScrollTimeoutMillis = perScrollTimeoutMillis; this.axis = axis; - this.sentinelStrategy = sentinelStrategy; + this.scrollStepStrategy = scrollStepStrategy; this.startFromBeginning = startFromBeginning; } /** - * Constructs with default not startFromBegining. - */ - public SentinelScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis, - SentinelStrategy sentinelStrategy) { - this(maxScrolls, perScrollTimeoutMillis, axis, sentinelStrategy, false); - } - - /** * Constructs with default 100 maxScrolls, 1 second for * perScrollTimeoutMillis, vertical axis, not startFromBegining. */ - public SentinelScroller(SentinelStrategy sentinelStrategy) { - this(100, 1000L, Axis.VERTICAL, sentinelStrategy, false); + public StepBasedScroller(ScrollStepStrategy scrollStepStrategy) { + this(100, 1000L, Axis.VERTICAL, scrollStepStrategy, false); } // if scrollBack is true, scrolls back to starting location if not found, so @@ -113,7 +97,7 @@ public class SentinelScroller implements Scroller { return driver.getPoller() .pollFor(driver, itemFinder, Poller.EXISTS, perScrollTimeoutMillis); } catch (TimeoutException e) { - if (i < maxScrolls && !sentinelStrategy.scroll(driver, containerFinder, direction)) { + if (i < maxScrolls && !scrollStepStrategy.scroll(driver, containerFinder, direction)) { break; } } @@ -123,9 +107,10 @@ public class SentinelScroller implements Scroller { if (i == maxScrolls) { // This is often a program error -- maxScrolls is a safety net; we should // have either found itemFinder, or stopped to scroll b/c of reaching the - // end. If maxScrolls is reasonably large, sentinelStrategy must be wrong. - Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; sentinelStrategy=%s", - containerFinder, maxScrolls, sentinelStrategy); + // end. If maxScrolls is reasonably large, ScrollStepStrategy must be + // wrong. + Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; ScrollStepStrategy=%s", + containerFinder, maxScrolls, scrollStepStrategy); } if (scrollBack) { @@ -145,7 +130,7 @@ public class SentinelScroller implements Scroller { @Override public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder) { Logs.call(this, "scrollTo", driver, containerFinder, itemFinder); - DirectionConverter converter = sentinelStrategy.getDirectionConverter(); + DirectionConverter converter = scrollStepStrategy.getDirectionConverter(); PhysicalDirection backwardDirection = converter.toPhysicalDirection(axis, BACKWARD); if (startFromBeginning) { diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java index f80f33d..7fc5056 100644 --- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java +++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java @@ -168,7 +168,7 @@ public class UiAutomationElement extends BaseUiElement { } @Override - protected InputInjector getInjector() { + public InputInjector getInjector() { return context.getInjector(); } |