Home | History | Annotate | Download | only in scroll
      1 /*
      2  * Copyright (C) 2013 DroidDriver committers
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package io.appium.droiddriver.scroll;
     17 
     18 import android.util.Log;
     19 
     20 import io.appium.droiddriver.DroidDriver;
     21 import io.appium.droiddriver.UiElement;
     22 import io.appium.droiddriver.exceptions.ElementNotFoundException;
     23 import io.appium.droiddriver.finders.By;
     24 import io.appium.droiddriver.finders.Finder;
     25 import io.appium.droiddriver.scroll.Direction.DirectionConverter;
     26 import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
     27 import io.appium.droiddriver.util.Logs;
     28 import io.appium.droiddriver.util.Strings;
     29 
     30 /**
     31  * Determines whether scrolling is possible by checking whether the sentinel
     32  * child is updated after scrolling. Use this when {@link UiElement#getChildren}
     33  * is not reliable. This can happen, for instance, when UiAutomationDriver is
     34  * used, which skips invisible children, or in the case of dynamic list, which
     35  * shows more items when scrolling beyond the end.
     36  */
     37 public class DynamicSentinelStrategy extends SentinelStrategy {
     38 
     39   /**
     40    * Interface for determining whether sentinel is updated.
     41    */
     42   public interface IsUpdatedStrategy {
     43     /**
     44      * Returns whether {@code newSentinel} is updated from {@code oldSentinel}.
     45      */
     46     boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel);
     47 
     48     /**
     49      * {@inheritDoc}
     50      *
     51      * <p>
     52      * It is recommended that this method return a description to help
     53      * debugging.
     54      */
     55     @Override
     56     String toString();
     57   }
     58 
     59   /**
     60    * Determines whether the sentinel is updated by checking a single unique
     61    * String attribute of a descendant element of the sentinel (or itself).
     62    */
     63   public static abstract class SingleStringUpdated implements IsUpdatedStrategy {
     64     private final Finder uniqueStringFinder;
     65 
     66     /**
     67      * @param uniqueStringFinder a Finder relative to the sentinel that finds
     68      *        its descendant or self which contains a unique String.
     69      */
     70     public SingleStringUpdated(Finder uniqueStringFinder) {
     71       this.uniqueStringFinder = uniqueStringFinder;
     72     }
     73 
     74     /**
     75      * @param uniqueStringElement the descendant or self that contains the
     76      *        unique String
     77      * @return the unique String
     78      */
     79     protected abstract String getUniqueString(UiElement uniqueStringElement);
     80 
     81     private String getUniqueStringFromSentinel(UiElement sentinel) {
     82       try {
     83         return getUniqueString(uniqueStringFinder.find(sentinel));
     84       } catch (ElementNotFoundException e) {
     85         return null;
     86       }
     87     }
     88 
     89     @Override
     90     public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) {
     91       // If the sentinel moved, scrolling has some effect. This is both an
     92       // optimization - getBounds is cheaper than find - and necessary in
     93       // certain cases, e.g. user is looking for a sibling of the unique string;
     94       // the scroll is close to the end therefore the unique string does not
     95       // change, but the target could be revealed.
     96       if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) {
     97         return true;
     98       }
     99 
    100       String newString = getUniqueStringFromSentinel(newSentinel);
    101       // A legitimate case for newString being null is when newSentinel is
    102       // partially shown. We return true to allow further scrolling. But program
    103       // error could also cause this, e.g. a bad choice of Getter, which
    104       // results in unnecessary scroll actions that have no visual effect. This
    105       // log helps troubleshooting in the latter case.
    106       if (newString == null) {
    107         Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s",
    108             newSentinel, uniqueStringFinder);
    109         return true;
    110       }
    111       if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) {
    112         Logs.log(Log.INFO, "Unique String is not updated: " + newString);
    113         return false;
    114       }
    115       return true;
    116     }
    117 
    118     @Override
    119     public String toString() {
    120       return Strings.toStringHelper(this).addValue(uniqueStringFinder).toString();
    121     }
    122   }
    123 
    124   /**
    125    * Determines whether the sentinel is updated by checking the text of a
    126    * descendant element of the sentinel (or itself).
    127    */
    128   public static class TextUpdated extends SingleStringUpdated {
    129     public TextUpdated(Finder uniqueStringFinder) {
    130       super(uniqueStringFinder);
    131     }
    132 
    133     @Override
    134     protected String getUniqueString(UiElement uniqueStringElement) {
    135       return uniqueStringElement.getText();
    136     }
    137   }
    138 
    139   /**
    140    * Determines whether the sentinel is updated by checking the content
    141    * description of a descendant element of the sentinel (or itself).
    142    */
    143   public static class ContentDescriptionUpdated extends SingleStringUpdated {
    144     public ContentDescriptionUpdated(Finder uniqueStringFinder) {
    145       super(uniqueStringFinder);
    146     }
    147 
    148     @Override
    149     protected String getUniqueString(UiElement uniqueStringElement) {
    150       return uniqueStringElement.getContentDescription();
    151     }
    152   }
    153 
    154   /**
    155    * Determines whether the sentinel is updated by checking the resource-id of a
    156    * descendant element of the sentinel (often itself). This is useful when the
    157    * children of the container are heterogeneous -- they don't have a common
    158    * pattern to get a unique string.
    159    */
    160   public static class ResourceIdUpdated extends SingleStringUpdated {
    161     /**
    162      * Uses the resource-id of the sentinel itself.
    163      */
    164     public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any());
    165 
    166     public ResourceIdUpdated(Finder uniqueStringFinder) {
    167       super(uniqueStringFinder);
    168     }
    169 
    170     @Override
    171     protected String getUniqueString(UiElement uniqueStringElement) {
    172       return uniqueStringElement.getResourceId();
    173     }
    174   }
    175 
    176   private final IsUpdatedStrategy isUpdatedStrategy;
    177   private UiElement lastSentinel;
    178 
    179   /**
    180    * Constructs with {@code Getter}s that decorate the given {@code Getter}s
    181    * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and
    182    * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel
    183    * after each scroll should be unique.
    184    */
    185   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
    186       Getter forwardGetter, DirectionConverter directionConverter) {
    187     super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE), new MorePredicateGetter(
    188         forwardGetter, UiElement.VISIBLE), directionConverter);
    189     this.isUpdatedStrategy = isUpdatedStrategy;
    190   }
    191 
    192   /**
    193    * Defaults to the standard {@link DirectionConverter}.
    194    */
    195   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
    196       Getter forwardGetter) {
    197     this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER);
    198   }
    199 
    200   /**
    201    * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard
    202    * {@link DirectionConverter}.
    203    */
    204   public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) {
    205     this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER,
    206         DirectionConverter.STANDARD_CONVERTER);
    207   }
    208 
    209   @Override
    210   public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
    211     UiElement oldSentinel = getOldSentinel(driver, containerFinder, direction);
    212     doScroll(oldSentinel.getParent(), direction);
    213     UiElement newSentinel = getSentinel(driver, containerFinder, direction);
    214     lastSentinel = newSentinel;
    215     return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel);
    216   }
    217 
    218   private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder,
    219       PhysicalDirection direction) {
    220     return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction);
    221   }
    222 
    223   @Override
    224   public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
    225       PhysicalDirection direction) {
    226     lastSentinel = null;
    227   }
    228 
    229   @Override
    230   public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
    231       PhysicalDirection direction) {
    232     // Prevent memory leak
    233     lastSentinel = null;
    234   }
    235 
    236   @Override
    237   public String toString() {
    238     return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(),
    239         isUpdatedStrategy);
    240   }
    241 }
    242