blob: 11c42f487da4fe8fdb995a8acea2b95246df4227 [file] [log] [blame]
/*
* Copyright (C) 2013 DroidDriver committers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.appium.droiddriver.scroll;
import static io.appium.droiddriver.scroll.Direction.LogicalDirection.BACKWARD;
import android.util.Log;
import io.appium.droiddriver.DroidDriver;
import io.appium.droiddriver.Poller;
import io.appium.droiddriver.UiElement;
import io.appium.droiddriver.exceptions.ElementNotFoundException;
import io.appium.droiddriver.exceptions.TimeoutException;
import io.appium.droiddriver.finders.By;
import io.appium.droiddriver.finders.Finder;
import io.appium.droiddriver.scroll.Direction.Axis;
import io.appium.droiddriver.scroll.Direction.DirectionConverter;
import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
import io.appium.droiddriver.util.Logs;
/**
* A {@link Scroller} that looks for the desired item in the currently shown
* content of the scrollable container, otherwise scrolls the container one step
* at a time and looks again, until we cannot scroll any more. A
* {@link ScrollStepStrategy} is used to determine whether more scrolling is
* possible.
*/
public class StepBasedScroller implements Scroller {
private final int maxScrolls;
private final long perScrollTimeoutMillis;
private final Axis axis;
private final ScrollStepStrategy scrollStepStrategy;
private final boolean startFromBeginning;
/**
* @param maxScrolls the maximum number of scrolls. It should be large enough
* to allow any reasonable list size
* @param perScrollTimeoutMillis the timeout in millis that we poll for the
* item after each scroll. 1000L is usually safe; if there are no
* asynchronously updated views, 0L is also a reasonable value.
* @param axis the axis this scroller can scroll
* @param startFromBeginning if {@code true},
* {@link #scrollTo(DroidDriver, Finder, Finder)} starts from the
* beginning and scrolls forward, instead of starting from the current
* location and scrolling in both directions. It may not always work,
* but when it works, it is faster.
*/
public StepBasedScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis,
ScrollStepStrategy scrollStepStrategy, boolean startFromBeginning) {
this.maxScrolls = maxScrolls;
this.perScrollTimeoutMillis = perScrollTimeoutMillis;
this.axis = axis;
this.scrollStepStrategy = scrollStepStrategy;
this.startFromBeginning = startFromBeginning;
}
/**
* Constructs with default 100 maxScrolls, 1 second for
* perScrollTimeoutMillis, vertical axis, not startFromBegining.
*/
public StepBasedScroller(ScrollStepStrategy scrollStepStrategy) {
this(100, 1000L, Axis.VERTICAL, scrollStepStrategy, false);
}
// if scrollBack is true, scrolls back to starting location if not found, so
// that we can start search in the other direction w/o polling on pages we
// have tried.
protected UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
PhysicalDirection direction, boolean scrollBack) {
Logs.call(this, "scrollTo", driver, containerFinder, itemFinder, direction, scrollBack);
// Enforce itemFinder is relative to containerFinder.
// Combine with containerFinder to make itemFinder absolute.
itemFinder = By.chain(containerFinder, itemFinder);
int i = 0;
for (; i <= maxScrolls; i++) {
try {
return driver.getPoller()
.pollFor(driver, itemFinder, Poller.EXISTS, perScrollTimeoutMillis);
} catch (TimeoutException e) {
if (i < maxScrolls && !scrollStepStrategy.scroll(driver, containerFinder, direction)) {
break;
}
}
}
ElementNotFoundException exception = new ElementNotFoundException(itemFinder);
if (i == maxScrolls) {
// This is often a program error -- maxScrolls is a safety net; we should
// have either found itemFinder, or stopped scrolling b/c of reaching the
// end. If maxScrolls is reasonably large, ScrollStepStrategy must be
// wrong.
Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; ScrollStepStrategy=%s",
containerFinder, maxScrolls, scrollStepStrategy);
}
if (scrollBack) {
for (; i > 1; i--) {
driver.on(containerFinder).scroll(direction.reverse());
}
}
throw exception;
}
@Override
public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
PhysicalDirection direction) {
try {
scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, direction);
return scrollTo(driver, containerFinder, itemFinder, direction, false);
} finally {
scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, direction);
}
}
@Override
public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder) {
Logs.call(this, "scrollTo", driver, containerFinder, itemFinder);
DirectionConverter converter = scrollStepStrategy.getDirectionConverter();
PhysicalDirection backwardDirection = converter.toPhysicalDirection(axis, BACKWARD);
if (startFromBeginning) {
// First try w/o scrolling
try {
return driver.getPoller().pollFor(driver, By.chain(containerFinder, itemFinder),
Poller.EXISTS, perScrollTimeoutMillis);
} catch (TimeoutException unused) {
// fall through to scroll to find
}
// Fling to beginning is not reliable; scroll to beginning
// container.perform(SwipeAction.toFling(backwardDirection));
try {
scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, backwardDirection);
for (int i = 0; i < maxScrolls; i++) {
if (!scrollStepStrategy.scroll(driver, containerFinder, backwardDirection)) {
break;
}
}
} finally {
scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, backwardDirection);
}
} else {
// search backward first
try {
return scrollTo(driver, containerFinder, itemFinder, backwardDirection, true);
} catch (ElementNotFoundException e) {
// fall through to search forward
}
}
// search forward
return scrollTo(driver, containerFinder, itemFinder, backwardDirection.reverse(), false);
}
}