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 static io.appium.droiddriver.scroll.Direction.LogicalDirection.BACKWARD;
     19 
     20 import android.util.Log;
     21 
     22 import io.appium.droiddriver.DroidDriver;
     23 import io.appium.droiddriver.Poller;
     24 import io.appium.droiddriver.UiElement;
     25 import io.appium.droiddriver.exceptions.ElementNotFoundException;
     26 import io.appium.droiddriver.exceptions.TimeoutException;
     27 import io.appium.droiddriver.finders.By;
     28 import io.appium.droiddriver.finders.Finder;
     29 import io.appium.droiddriver.scroll.Direction.Axis;
     30 import io.appium.droiddriver.scroll.Direction.DirectionConverter;
     31 import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
     32 import io.appium.droiddriver.util.Logs;
     33 
     34 /**
     35  * A {@link Scroller} that looks for the desired item in the currently shown
     36  * content of the scrollable container, otherwise scrolls the container one step
     37  * at a time and looks again, until we cannot scroll any more. A
     38  * {@link ScrollStepStrategy} is used to determine whether more scrolling is
     39  * possible.
     40  */
     41 public class StepBasedScroller implements Scroller {
     42   private final int maxScrolls;
     43   private final long perScrollTimeoutMillis;
     44   private final Axis axis;
     45   private final ScrollStepStrategy scrollStepStrategy;
     46   private final boolean startFromBeginning;
     47 
     48   /**
     49    * @param maxScrolls the maximum number of scrolls. It should be large enough
     50    *        to allow any reasonable list size
     51    * @param perScrollTimeoutMillis the timeout in millis that we poll for the
     52    *        item after each scroll. 1000L is usually safe; if there are no
     53    *        asynchronously updated views, 0L is also a reasonable value.
     54    * @param axis the axis this scroller can scroll
     55    * @param startFromBeginning if {@code true},
     56    *        {@link #scrollTo(DroidDriver, Finder, Finder)} starts from the
     57    *        beginning and scrolls forward, instead of starting from the current
     58    *        location and scrolling in both directions. It may not always work,
     59    *        but when it works, it is faster.
     60    */
     61   public StepBasedScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis,
     62       ScrollStepStrategy scrollStepStrategy, boolean startFromBeginning) {
     63     this.maxScrolls = maxScrolls;
     64     this.perScrollTimeoutMillis = perScrollTimeoutMillis;
     65     this.axis = axis;
     66     this.scrollStepStrategy = scrollStepStrategy;
     67     this.startFromBeginning = startFromBeginning;
     68   }
     69 
     70   /**
     71    * Constructs with default 100 maxScrolls, 1 second for
     72    * perScrollTimeoutMillis, vertical axis, not startFromBegining.
     73    */
     74   public StepBasedScroller(ScrollStepStrategy scrollStepStrategy) {
     75     this(100, 1000L, Axis.VERTICAL, scrollStepStrategy, false);
     76   }
     77 
     78   // if scrollBack is true, scrolls back to starting location if not found, so
     79   // that we can start search in the other direction w/o polling on pages we
     80   // have tried.
     81   protected UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
     82       PhysicalDirection direction, boolean scrollBack) {
     83     Logs.call(this, "scrollTo", driver, containerFinder, itemFinder, direction, scrollBack);
     84     // Enforce itemFinder is relative to containerFinder.
     85     // Combine with containerFinder to make itemFinder absolute.
     86     itemFinder = By.chain(containerFinder, itemFinder);
     87 
     88     int i = 0;
     89     for (; i <= maxScrolls; i++) {
     90       try {
     91         return driver.getPoller()
     92             .pollFor(driver, itemFinder, Poller.EXISTS, perScrollTimeoutMillis);
     93       } catch (TimeoutException e) {
     94         if (i < maxScrolls && !scrollStepStrategy.scroll(driver, containerFinder, direction)) {
     95           break;
     96         }
     97       }
     98     }
     99 
    100     ElementNotFoundException exception = new ElementNotFoundException(itemFinder);
    101     if (i == maxScrolls) {
    102       // This is often a program error -- maxScrolls is a safety net; we should
    103       // have either found itemFinder, or stopped scrolling b/c of reaching the
    104       // end. If maxScrolls is reasonably large, ScrollStepStrategy must be
    105       // wrong.
    106       Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; ScrollStepStrategy=%s",
    107           containerFinder, maxScrolls, scrollStepStrategy);
    108     }
    109 
    110     if (scrollBack) {
    111       for (; i > 1; i--) {
    112         driver.on(containerFinder).scroll(direction.reverse());
    113       }
    114     }
    115     throw exception;
    116   }
    117 
    118   @Override
    119   public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
    120       PhysicalDirection direction) {
    121     try {
    122       scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, direction);
    123       return scrollTo(driver, containerFinder, itemFinder, direction, false);
    124     } finally {
    125       scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, direction);
    126     }
    127   }
    128 
    129   @Override
    130   public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder) {
    131     Logs.call(this, "scrollTo", driver, containerFinder, itemFinder);
    132     DirectionConverter converter = scrollStepStrategy.getDirectionConverter();
    133     PhysicalDirection backwardDirection = converter.toPhysicalDirection(axis, BACKWARD);
    134 
    135     if (startFromBeginning) {
    136       // First try w/o scrolling
    137       try {
    138         return driver.getPoller().pollFor(driver, By.chain(containerFinder, itemFinder),
    139             Poller.EXISTS, perScrollTimeoutMillis);
    140       } catch (TimeoutException unused) {
    141         // fall through to scroll to find
    142       }
    143 
    144       // Fling to beginning is not reliable; scroll to beginning
    145       // container.perform(SwipeAction.toFling(backwardDirection));
    146       try {
    147         scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, backwardDirection);
    148         for (int i = 0; i < maxScrolls; i++) {
    149           if (!scrollStepStrategy.scroll(driver, containerFinder, backwardDirection)) {
    150             break;
    151           }
    152         }
    153       } finally {
    154         scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, backwardDirection);
    155       }
    156     } else {
    157       // search backward first
    158       try {
    159         return scrollTo(driver, containerFinder, itemFinder, backwardDirection, true);
    160       } catch (ElementNotFoundException e) {
    161         // fall through to search forward
    162       }
    163     }
    164 
    165     // search forward
    166     return scrollTo(driver, containerFinder, itemFinder, backwardDirection.reverse(), false);
    167   }
    168 }
    169