AccessibilityEventScrollStepStrategy.java revision 147f9cda75b8f258435c53bf73f058f09ca0134d
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 com.google.android.droiddriver.scroll;
17
18import android.app.UiAutomation;
19import android.app.UiAutomation.AccessibilityEventFilter;
20import android.util.Log;
21import android.view.accessibility.AccessibilityEvent;
22
23import com.google.android.droiddriver.DroidDriver;
24import com.google.android.droiddriver.UiElement;
25import com.google.android.droiddriver.actions.SwipeAction;
26import com.google.android.droiddriver.exceptions.UnrecoverableException;
27import com.google.android.droiddriver.finders.Finder;
28import com.google.android.droiddriver.scroll.Direction.Axis;
29import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
30import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
31import com.google.android.droiddriver.util.Logs;
32
33import java.util.concurrent.TimeoutException;
34
35/**
36 * A {@link ScrollStepStrategy} that determines whether more scrolling is
37 * possible by checking the {@link AccessibilityEvent} returned by
38 * {@link android.app.UiAutomation}.
39 * <p>
40 * This implementation behaves just like the <a href=
41 * "http://developer.android.com/tools/help/uiautomator/UiScrollable.html"
42 * >UiScrollable</a> class. It may not work in all cases. For instance,
43 * sometimes {@link android.support.v4.widget.DrawerLayout} does not send
44 * correct {@link AccessibilityEvent}s after scrolling.
45 * </p>
46 */
47public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy {
48  /**
49   * Stores the data if we reached end at the last {@link #scroll}. If the data
50   * match when a new scroll is requested, we can return immediately.
51   */
52  private static class EndData {
53    private Finder containerFinderAtEnd;
54    private PhysicalDirection directionAtEnd;
55
56    public boolean match(Finder containerFinder, PhysicalDirection direction) {
57      return containerFinderAtEnd == containerFinder && directionAtEnd == direction;
58    }
59
60    public void set(Finder containerFinder, PhysicalDirection direction) {
61      containerFinderAtEnd = containerFinder;
62      directionAtEnd = direction;
63    }
64
65    public void reset() {
66      set(null, null);
67    }
68  }
69
70  /**
71   * This filter allows us to grab the last accessibility event generated
72   * for a scroll up to {@code scrollEventTimeoutMillis}.
73   */
74  private static class LastScrollEventFilter implements AccessibilityEventFilter {
75    private AccessibilityEvent lastEvent;
76
77    @Override
78    public boolean accept(AccessibilityEvent event) {
79      if ((event.getEventType() & AccessibilityEvent.TYPE_VIEW_SCROLLED) != 0) {
80        // Recycle the current last event.
81        if (lastEvent != null) {
82          lastEvent.recycle();
83        }
84        lastEvent = AccessibilityEvent.obtain(event);
85      }
86      // Return false to collect events until scrollEventTimeoutMillis has elapsed.
87      return false;
88    }
89
90    public AccessibilityEvent getLastEvent() {
91      return lastEvent;
92    }
93  }
94
95  private final UiAutomation uiAutomation;
96  private final long scrollEventTimeoutMillis;
97  private final DirectionConverter directionConverter;
98  private final EndData endData = new EndData();
99
100  public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
101      long scrollEventTimeoutMillis, DirectionConverter converter) {
102    this.uiAutomation = uiAutomation;
103    this.scrollEventTimeoutMillis = scrollEventTimeoutMillis;
104    this.directionConverter = converter;
105  }
106
107  @Override
108  public boolean scroll(DroidDriver driver, Finder containerFinder,
109      final PhysicalDirection direction) {
110    // Check if we've reached end after last scroll.
111    if (endData.match(containerFinder, direction)) {
112      return false;
113    }
114
115    AccessibilityEvent event = doScrollAndReturnEvent(driver.on(containerFinder), direction);
116    if (detectEnd(event, direction.axis())) {
117      endData.set(containerFinder, direction);
118      Logs.log(Log.DEBUG, "reached scroll end with event: " + event);
119    }
120
121    // Clean up the event after use.
122    if (event != null) {
123      event.recycle();
124    }
125
126    // Even if event == null, that does not mean scroll has no effect!
127    // Some views may not emit correct events when the content changed.
128    return true;
129  }
130
131  // Copied from UiAutomator.
132  // AdapterViews have indices we can use to check for the beginning.
133  protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
134    if (event == null) {
135      return true;
136    }
137    boolean foundEnd = false;
138    if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
139      foundEnd = event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex();
140    } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
141      if (axis == Axis.VERTICAL) {
142        foundEnd = event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY();
143      } else if (axis == Axis.HORIZONTAL) {
144        foundEnd = event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX();
145      }
146    }
147    return foundEnd;
148  }
149
150  @Override
151  public final DirectionConverter getDirectionConverter() {
152    return directionConverter;
153  }
154
155  @Override
156  public String toString() {
157    return String.format("AccessibilityEventScrollStepStrategy{scrollEventTimeoutMillis=%d}",
158        scrollEventTimeoutMillis);
159  }
160
161  @Override
162  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
163      PhysicalDirection direction) {
164    endData.reset();
165  }
166
167  @Override
168  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
169      PhysicalDirection direction) {}
170
171  protected AccessibilityEvent doScrollAndReturnEvent(final UiElement container,
172      final PhysicalDirection direction) {
173    LastScrollEventFilter filter = new LastScrollEventFilter();
174    try {
175      uiAutomation.executeAndWaitForEvent(new Runnable() {
176        @Override
177        public void run() {
178          doScroll(container, direction);
179        }
180      }, filter, scrollEventTimeoutMillis);
181    } catch (IllegalStateException e) {
182      throw new UnrecoverableException(e);
183    } catch (TimeoutException e) {
184      // We expect this because LastScrollEventFilter.accept always returns false.
185    }
186    return filter.getLastEvent();
187  }
188
189  @Override
190  public void doScroll(final UiElement container, final PhysicalDirection direction) {
191    SwipeAction.toScroll(direction).perform(container.getInjector(), container);
192  }
193}
194