diff options
Diffstat (limited to 'src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java')
-rw-r--r-- | src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java b/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java new file mode 100644 index 0000000..051cfa7 --- /dev/null +++ b/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java @@ -0,0 +1,241 @@ +/* + * 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 io.appium.droiddriver.scroll; + +import android.util.Log; + +import io.appium.droiddriver.DroidDriver; +import io.appium.droiddriver.UiElement; +import io.appium.droiddriver.exceptions.ElementNotFoundException; +import io.appium.droiddriver.finders.By; +import io.appium.droiddriver.finders.Finder; +import io.appium.droiddriver.scroll.Direction.DirectionConverter; +import io.appium.droiddriver.scroll.Direction.PhysicalDirection; +import io.appium.droiddriver.util.Logs; +import io.appium.droiddriver.util.Strings; + +/** + * Determines whether scrolling is possible by checking whether the sentinel + * child is updated after scrolling. Use this when {@link UiElement#getChildren} + * is not reliable. This can happen, for instance, when UiAutomationDriver is + * 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 SentinelStrategy { + + /** + * Interface for determining whether sentinel is updated. + */ + public static interface IsUpdatedStrategy { + /** + * Returns whether {@code newSentinel} is updated from {@code oldSentinel}. + */ + boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel); + + /** + * {@inheritDoc} + * + * <p> + * It is recommended that this method return a description to help + * debugging. + */ + @Override + String toString(); + } + + /** + * Determines whether the sentinel is updated by checking a single unique + * String attribute of a descendant element of the sentinel (or itself). + */ + public static abstract class SingleStringUpdated implements IsUpdatedStrategy { + private final Finder uniqueStringFinder; + + /** + * @param uniqueStringFinder a Finder relative to the sentinel that finds + * its descendant or self which contains a unique String. + */ + public SingleStringUpdated(Finder uniqueStringFinder) { + this.uniqueStringFinder = uniqueStringFinder; + } + + /** + * @param uniqueStringElement the descendant or self that contains the + * unique String + * @return the unique String + */ + protected abstract String getUniqueString(UiElement uniqueStringElement); + + private String getUniqueStringFromSentinel(UiElement sentinel) { + try { + return getUniqueString(uniqueStringFinder.find(sentinel)); + } catch (ElementNotFoundException e) { + return null; + } + } + + @Override + public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) { + // If the sentinel moved, scrolling has some effect. This is both an + // optimization - getBounds is cheaper than find - and necessary in + // certain cases, e.g. user is looking for a sibling of the unique string; + // the scroll is close to the end therefore the unique string does not + // change, but the target could be revealed. + if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) { + return true; + } + + String newString = getUniqueStringFromSentinel(newSentinel); + // 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 Getter, which + // results in unnecessary scroll actions that have no visual effect. This + // log helps troubleshooting in the latter case. + if (newString == null) { + Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s", + newSentinel, uniqueStringFinder); + return true; + } + if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) { + Logs.log(Log.INFO, "Unique String is not updated: " + newString); + return false; + } + return true; + } + + @Override + public String toString() { + return Strings.toStringHelper(this).addValue(uniqueStringFinder).toString(); + } + } + + /** + * Determines whether the sentinel is updated by checking the text of a + * descendant element of the sentinel (or itself). + */ + public static class TextUpdated extends SingleStringUpdated { + public TextUpdated(Finder uniqueStringFinder) { + super(uniqueStringFinder); + } + + @Override + protected String getUniqueString(UiElement uniqueStringElement) { + return uniqueStringElement.getText(); + } + } + + /** + * Determines whether the sentinel is updated by checking the content + * description of a descendant element of the sentinel (or itself). + */ + public static class ContentDescriptionUpdated extends SingleStringUpdated { + public ContentDescriptionUpdated(Finder uniqueStringFinder) { + super(uniqueStringFinder); + } + + @Override + protected String getUniqueString(UiElement uniqueStringElement) { + return uniqueStringElement.getContentDescription(); + } + } + + /** + * Determines whether the sentinel is updated by checking the resource-id of a + * descendant element of the sentinel (often itself). This is useful when the + * children of the container are heterogeneous -- they don't have a common + * pattern to get a unique string. + */ + public static class ResourceIdUpdated extends SingleStringUpdated { + /** + * Uses the resource-id of the sentinel itself. + */ + public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any()); + + public ResourceIdUpdated(Finder uniqueStringFinder) { + super(uniqueStringFinder); + } + + @Override + protected String getUniqueString(UiElement uniqueStringElement) { + return uniqueStringElement.getResourceId(); + } + } + + private final IsUpdatedStrategy isUpdatedStrategy; + private UiElement lastSentinel; + + /** + * 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, Getter backwardGetter, + Getter forwardGetter, DirectionConverter directionConverter) { + super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE), new MorePredicateGetter( + forwardGetter, UiElement.VISIBLE), directionConverter); + this.isUpdatedStrategy = isUpdatedStrategy; + } + + /** + * Defaults to the standard {@link DirectionConverter}. + */ + 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, Getter backwardGetter) { + this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER, + DirectionConverter.STANDARD_CONVERTER); + } + + @Override + public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) { + UiElement oldSentinel = getOldSentinel(driver, containerFinder, direction); + doScroll(oldSentinel.getParent(), direction); + UiElement newSentinel = getSentinel(driver, containerFinder, direction); + lastSentinel = newSentinel; + return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel); + } + + private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder, + PhysicalDirection direction) { + return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction); + } + + @Override + public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, + PhysicalDirection direction) { + lastSentinel = null; + } + + @Override + public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, + PhysicalDirection direction) { + // Prevent memory leak + lastSentinel = null; + } + + @Override + public String toString() { + return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(), + isUpdatedStrategy); + } +} |