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 */
16package io.appium.droiddriver.scroll;
17
18import android.util.Log;
19
20import io.appium.droiddriver.DroidDriver;
21import io.appium.droiddriver.Poller;
22import io.appium.droiddriver.UiElement;
23import io.appium.droiddriver.exceptions.ElementNotFoundException;
24import io.appium.droiddriver.exceptions.TimeoutException;
25import io.appium.droiddriver.finders.By;
26import io.appium.droiddriver.finders.Finder;
27import io.appium.droiddriver.scroll.Direction.Axis;
28import io.appium.droiddriver.scroll.Direction.DirectionConverter;
29import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
30import io.appium.droiddriver.util.Logs;
31
32import static io.appium.droiddriver.scroll.Direction.LogicalDirection.BACKWARD;
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 */
41public 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