AccessibilityEventScrollStepStrategy.java revision 74676fdd3c8a9e599eddd13bea56898674d9916a
1ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com/*
2ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com * Copyright (C) 2013 DroidDriver committers
3ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com *
4ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com * Licensed under the Apache License, Version 2.0 (the "License");
5ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com * you may not use this file except in compliance with the License.
6ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com * You may obtain a copy of the License at
7ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com *
8ec3ed6a5ebf6f2c406d7bcf94b6bc34fcaeb976eepoger@google.com *      http://www.apache.org/licenses/LICENSE-2.0
98a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com *
108a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com * Unless required by applicable law or agreed to in writing, software
118a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com * distributed under the License is distributed on an "AS IS" BASIS,
128a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
138a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com * See the License for the specific language governing permissions and
142e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * limitations under the License.
152e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org */
162e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgpackage com.google.android.droiddriver.scroll;
172e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org
18945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport android.app.UiAutomation;
19945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport android.app.UiAutomation.AccessibilityEventFilter;
20945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport android.util.Log;
212e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgimport android.view.accessibility.AccessibilityEvent;
222e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org
232e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgimport com.google.android.droiddriver.DroidDriver;
242e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgimport com.google.android.droiddriver.UiElement;
25945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.actions.SwipeAction;
262e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgimport com.google.android.droiddriver.exceptions.UnrecoverableException;
27945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.finders.Finder;
28945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.scroll.Direction.Axis;
29945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.scroll.Direction.DirectionConverter;
30945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
31945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport com.google.android.droiddriver.util.Logs;
32945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com
33945a139553a9c9da03766213661d7f5fd6ed3042reed@android.comimport java.util.concurrent.TimeoutException;
34945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com
35945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com/**
362e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * A {@link ScrollStepStrategy} that determines whether more scrolling is
37945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com * possible by checking the {@link AccessibilityEvent} returned by
38945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com * {@link android.app.UiAutomation}.
392e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * <p>
402e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * This implementation behaves just like the <a href=
412e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * "http://developer.android.com/tools/help/uiautomator/UiScrollable.html"
422e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * >UiScrollable</a> class. It may not work in all cases. For instance,
432e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * sometimes {@link android.support.v4.widget.DrawerLayout} does not send
442e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * correct {@link AccessibilityEvent}s after scrolling.
452e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org * </p>
462e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org */
472e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.orgpublic class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy {
482e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org  /**
492e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org   * Stores the data if we reached end at the last {@link #scroll}. If the data
502e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org   * match when a new scroll is requested, we can return immediately.
512e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org   */
522e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org  private static class EndData {
532e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org    private Finder containerFinderAtEnd;
542e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org    private PhysicalDirection directionAtEnd;
552e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org
562e086190e55a01dc2f7b74df6f2828e8cac2b9abkbr@chromium.org    public boolean match(Finder containerFinder, PhysicalDirection direction) {
57945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com      return containerFinderAtEnd == containerFinder && directionAtEnd == direction;
58945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com    }
59945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com
60945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com    public void set(Finder containerFinder, PhysicalDirection direction) {
61945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com      containerFinderAtEnd = containerFinder;
62945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com      directionAtEnd = direction;
63945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com    }
64945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com
65945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com    public void reset() {
66945a139553a9c9da03766213661d7f5fd6ed3042reed@android.com      set(null, null);
678a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
688a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
698a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
708a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  /**
718a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   * This filter allows us to grab the last accessibility event generated for a
728a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   * scroll up to {@code scrollEventTimeoutMillis}.
738a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   */
748a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  private static class LastScrollEventFilter implements AccessibilityEventFilter {
758a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    private AccessibilityEvent lastEvent;
768a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
778a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    @Override
788a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    public boolean accept(AccessibilityEvent event) {
798a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      if ((event.getEventType() & AccessibilityEvent.TYPE_VIEW_SCROLLED) != 0) {
808a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        // Recycle the current last event.
818a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        if (lastEvent != null) {
828a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com          lastEvent.recycle();
838a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        }
848a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        lastEvent = AccessibilityEvent.obtain(event);
858a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      }
868a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      // Return false to collect events until scrollEventTimeoutMillis has
878a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      // elapsed.
888a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return false;
898a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
908a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
918a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    public AccessibilityEvent getLastEvent() {
928a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return lastEvent;
938a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
948a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
958a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
968a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  private final UiAutomation uiAutomation;
978a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  private final long scrollEventTimeoutMillis;
988a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  private final DirectionConverter directionConverter;
998a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  private final EndData endData = new EndData();
1008a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1018a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
1028a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      long scrollEventTimeoutMillis, DirectionConverter converter) {
1038a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    this.uiAutomation = uiAutomation;
1048a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    this.scrollEventTimeoutMillis = scrollEventTimeoutMillis;
1058a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    this.directionConverter = converter;
1068a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1078a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1088a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1098a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public boolean scroll(DroidDriver driver, Finder containerFinder,
1108a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      final PhysicalDirection direction) {
1118a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // Check if we've reached end after last scroll.
1128a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (endData.match(containerFinder, direction)) {
1138a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return false;
1148a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
1158a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1168a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    AccessibilityEvent event = doScrollAndReturnEvent(driver.on(containerFinder), direction);
1178a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (detectEnd(event, direction.axis())) {
1188a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      endData.set(containerFinder, direction);
11915161620bee33efcb706685486c9ce0fb51a72bbreed@android.com      Logs.log(Log.DEBUG, "reached scroll end with event: " + event);
12015161620bee33efcb706685486c9ce0fb51a72bbreed@android.com    }
12115161620bee33efcb706685486c9ce0fb51a72bbreed@android.com
1228a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // Clean up the event after use.
1238a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (event != null) {
1248a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      event.recycle();
1258a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
1268a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1278a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // Even if event == null, that does not mean scroll has no effect!
1288a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // Some views may not emit correct events when the content changed.
1298a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    return true;
1308a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1318a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1328a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  // Copied from UiAutomator.
1338a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  // AdapterViews have indices we can use to check for the beginning.
1348a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
1358a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (event == null) {
1368a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return true;
1378a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
1388a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1398a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
1408a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex();
1418a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
1428a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    if (event.getScrollX() != -1 && event.getScrollY() != -1) {
1438a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      if (axis == Axis.VERTICAL) {
1448a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        return event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY();
1458a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      } else if (axis == Axis.HORIZONTAL) {
14615161620bee33efcb706685486c9ce0fb51a72bbreed@android.com        return event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX();
1478a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      }
14815161620bee33efcb706685486c9ce0fb51a72bbreed@android.com    }
1498a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1508a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // This case is different from UiAutomator.
1518a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    return event.getFromIndex() == -1 && event.getToIndex() == -1 && event.getItemCount() == -1
1528a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        && event.getScrollX() == -1 && event.getScrollY() == -1;
1538a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1548a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1558a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1568a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public final DirectionConverter getDirectionConverter() {
1578a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    return directionConverter;
1588a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1598a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1608a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1618a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public String toString() {
1628a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    return String.format("%s{scrollEventTimeoutMillis=%d}", getClass().getSimpleName(),
1638a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        scrollEventTimeoutMillis);
1648a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1658a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1668a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1678a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
1688a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      PhysicalDirection direction) {
1698a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    endData.reset();
1708a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1718a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1728a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1738a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
1748a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      PhysicalDirection direction) {}
1758a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1768a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  protected AccessibilityEvent doScrollAndReturnEvent(final UiElement container,
1778a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      final PhysicalDirection direction) {
1788a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    LastScrollEventFilter filter = new LastScrollEventFilter();
1798a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    try {
1808a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      uiAutomation.executeAndWaitForEvent(new Runnable() {
1818a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        @Override
1828a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        public void run() {
1838a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com          doScroll(container, direction);
1848a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        }
1858a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      }, filter, scrollEventTimeoutMillis);
1868a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    } catch (IllegalStateException e) {
1878a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      throw new UnrecoverableException(e);
1888a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    } catch (TimeoutException e) {
1898a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      // We expect this because LastScrollEventFilter.accept always returns
1908a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      // false.
1918a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
1928a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    return filter.getLastEvent();
1938a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
1948a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
1958a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  @Override
1968a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public void doScroll(final UiElement container, final PhysicalDirection direction) {
1978a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // We do not call container.scroll(direction) because container.scroll
1988a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // internally calls UiAutomation.executeAndWaitForEvent which clears the
1998a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // AccessibilityEvent Queue, preventing us from fetching the last
2008a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    // accessibility event to determine if scrolling has finished.
2018a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    SwipeAction.toScroll(direction).perform(container);
2028a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
2038a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
2048a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  /**
2058a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   * Some widgets may not always fire correct {@link AccessibilityEvent}.
2068a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   * Detecting end by null event is safer (at the cost of a extra scroll) than
2078a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   * examining indices.
2088a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com   */
2098a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  public static class NullAccessibilityEventScrollStepStrategy extends
2108a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      AccessibilityEventScrollStepStrategy {
2118a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
2128a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    public NullAccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
2138a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com        long scrollEventTimeoutMillis, DirectionConverter converter) {
2148a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      super(uiAutomation, scrollEventTimeoutMillis, converter);
2158a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
2168a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com
2178a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    @Override
2188a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
2198a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com      return event == null;
2208a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com    }
2218a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com  }
2228a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com}
2238a1c16ff38322f0210116fa7293eb8817c7e477ereed@android.com