• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Poller;
22 import io.appium.droiddriver.UiElement;
23 import io.appium.droiddriver.exceptions.ElementNotFoundException;
24 import io.appium.droiddriver.exceptions.TimeoutException;
25 import io.appium.droiddriver.finders.By;
26 import io.appium.droiddriver.finders.Finder;
27 import io.appium.droiddriver.scroll.Direction.Axis;
28 import io.appium.droiddriver.scroll.Direction.DirectionConverter;
29 import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
30 import io.appium.droiddriver.util.Logs;
31 
32 import 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  */
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    */
StepBasedScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis, ScrollStepStrategy scrollStepStrategy, boolean startFromBeginning)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    */
StepBasedScroller(ScrollStepStrategy scrollStepStrategy)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.
scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction, boolean scrollBack)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
scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction)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
scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder)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