| /* |
| * 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 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); |
| } |
| } |