aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java')
-rw-r--r--src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java220
1 files changed, 220 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
new file mode 100644
index 0000000..6050575
--- /dev/null
+++ b/src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
@@ -0,0 +1,220 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.concurrent.TimeoutException;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.actions.SwipeAction;
+import io.appium.droiddriver.exceptions.UnrecoverableException;
+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 ScrollStepStrategy} that determines whether more scrolling is
+ * possible by checking the {@link AccessibilityEvent} returned by
+ * {@link android.app.UiAutomation}.
+ * <p>
+ * This implementation behaves just like the <a href=
+ * "http://developer.android.com/tools/help/uiautomator/UiScrollable.html"
+ * >UiScrollable</a> class. It may not work in all cases. For instance,
+ * sometimes {@code android.support.v4.widget.DrawerLayout} does not send
+ * correct {@link AccessibilityEvent}s after scrolling.
+ * </p>
+ */
+@TargetApi(18)
+public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy {
+ /**
+ * Stores the data if we reached end at the last {@link #scroll}. If the data
+ * match when a new scroll is requested, we can return immediately.
+ */
+ private static class EndData {
+ private Finder containerFinderAtEnd;
+ private PhysicalDirection directionAtEnd;
+
+ public boolean match(Finder containerFinder, PhysicalDirection direction) {
+ return containerFinderAtEnd == containerFinder && directionAtEnd == direction;
+ }
+
+ public void set(Finder containerFinder, PhysicalDirection direction) {
+ containerFinderAtEnd = containerFinder;
+ directionAtEnd = direction;
+ }
+
+ public void reset() {
+ set(null, null);
+ }
+ }
+
+ /**
+ * This filter allows us to grab the last accessibility event generated for a
+ * scroll up to {@code scrollEventTimeoutMillis}.
+ */
+ private static class LastScrollEventFilter implements AccessibilityEventFilter {
+ private AccessibilityEvent lastEvent;
+
+ @Override
+ public boolean accept(AccessibilityEvent event) {
+ if ((event.getEventType() & AccessibilityEvent.TYPE_VIEW_SCROLLED) != 0) {
+ // Recycle the current last event.
+ if (lastEvent != null) {
+ lastEvent.recycle();
+ }
+ lastEvent = AccessibilityEvent.obtain(event);
+ }
+ // Return false to collect events until scrollEventTimeoutMillis has
+ // elapsed.
+ return false;
+ }
+
+ public AccessibilityEvent getLastEvent() {
+ return lastEvent;
+ }
+ }
+
+ private final UiAutomation uiAutomation;
+ private final long scrollEventTimeoutMillis;
+ private final DirectionConverter directionConverter;
+ private final EndData endData = new EndData();
+
+ public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
+ long scrollEventTimeoutMillis, DirectionConverter converter) {
+ this.uiAutomation = uiAutomation;
+ this.scrollEventTimeoutMillis = scrollEventTimeoutMillis;
+ this.directionConverter = converter;
+ }
+
+ @Override
+ public boolean scroll(DroidDriver driver, Finder containerFinder,
+ final PhysicalDirection direction) {
+ // Check if we've reached end after last scroll.
+ if (endData.match(containerFinder, direction)) {
+ return false;
+ }
+
+ AccessibilityEvent event = doScrollAndReturnEvent(driver.on(containerFinder), direction);
+ if (detectEnd(event, direction.axis())) {
+ endData.set(containerFinder, direction);
+ Logs.log(Log.DEBUG, "reached scroll end with event: " + event);
+ }
+
+ // Clean up the event after use.
+ if (event != null) {
+ event.recycle();
+ }
+
+ // Even if event == null, that does not mean scroll has no effect!
+ // Some views may not emit correct events when the content changed.
+ return true;
+ }
+
+ // Copied from UiAutomator.
+ // AdapterViews have indices we can use to check for the beginning.
+ protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
+ if (event == null) {
+ return true;
+ }
+
+ if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
+ return event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex();
+ }
+ if (event.getScrollX() != -1 && event.getScrollY() != -1) {
+ if (axis == Axis.VERTICAL) {
+ return event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY();
+ } else if (axis == Axis.HORIZONTAL) {
+ return event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX();
+ }
+ }
+
+ // This case is different from UiAutomator.
+ return event.getFromIndex() == -1 && event.getToIndex() == -1 && event.getItemCount() == -1
+ && event.getScrollX() == -1 && event.getScrollY() == -1;
+ }
+
+ @Override
+ public final DirectionConverter getDirectionConverter() {
+ return directionConverter;
+ }
+
+ @Override
+ public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+ PhysicalDirection direction) {
+ endData.reset();
+ }
+
+ @Override
+ public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+ PhysicalDirection direction) {}
+
+ protected AccessibilityEvent doScrollAndReturnEvent(final UiElement container,
+ final PhysicalDirection direction) {
+ LastScrollEventFilter filter = new LastScrollEventFilter();
+ try {
+ uiAutomation.executeAndWaitForEvent(new Runnable() {
+ @Override
+ public void run() {
+ doScroll(container, direction);
+ }
+ }, filter, scrollEventTimeoutMillis);
+ } catch (IllegalStateException e) {
+ throw new UnrecoverableException(e);
+ } catch (TimeoutException e) {
+ // We expect this because LastScrollEventFilter.accept always returns
+ // false.
+ }
+ return filter.getLastEvent();
+ }
+
+ @Override
+ public void doScroll(final UiElement container, final PhysicalDirection direction) {
+ // We do not call container.scroll(direction) because it uses a SwipeAction
+ // with positive tTimeoutMillis. That path calls
+ // UiAutomation.executeAndWaitForEvent which clears the
+ // AccessibilityEvent Queue, preventing us from fetching the last
+ // accessibility event to determine if scrolling has finished.
+ container
+ .perform(new SwipeAction(direction, SwipeAction.getScrollSteps(), false /* drag */, 0L/* timeoutMillis */));
+ }
+
+ /**
+ * Some widgets may not always fire correct {@link AccessibilityEvent}.
+ * Detecting end by null event is safer (at the cost of a extra scroll) than
+ * examining indices.
+ */
+ public static class NullAccessibilityEventScrollStepStrategy extends
+ AccessibilityEventScrollStepStrategy {
+
+ public NullAccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
+ long scrollEventTimeoutMillis, DirectionConverter converter) {
+ super(uiAutomation, scrollEventTimeoutMillis, converter);
+ }
+
+ @Override
+ protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
+ return event == null;
+ }
+ }
+}