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