diff options
Diffstat (limited to 'src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java')
-rw-r--r-- | src/io/appium/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java | 220 |
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; + } + } +} |