aboutsummaryrefslogtreecommitdiff
path: root/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
blob: 5e3d766c42ebcbe12d94b32308f852f54c44e8c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*
 * 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 com.google.android.droiddriver.scroll;

import android.app.UiAutomation;
import android.app.UiAutomation.AccessibilityEventFilter;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

import com.google.android.droiddriver.DroidDriver;
import com.google.android.droiddriver.UiElement;
import com.google.android.droiddriver.actions.SwipeAction;
import com.google.android.droiddriver.exceptions.UnrecoverableException;
import com.google.android.droiddriver.finders.Finder;
import com.google.android.droiddriver.scroll.Direction.Axis;
import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
import com.google.android.droiddriver.util.Logs;

import java.util.concurrent.TimeoutException;

/**
 * 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 {@link android.support.v4.widget.DrawerLayout} does not send
 * correct {@link AccessibilityEvent}s after scrolling.
 * </p>
 */
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;
    }
    boolean foundEnd = false;
    if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
      foundEnd = event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex();
    } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
      if (axis == Axis.VERTICAL) {
        foundEnd = event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY();
      } else if (axis == Axis.HORIZONTAL) {
        foundEnd = event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX();
      }
    }
    return foundEnd;
  }

  @Override
  public final DirectionConverter getDirectionConverter() {
    return directionConverter;
  }

  @Override
  public String toString() {
    return String.format("AccessibilityEventScrollStepStrategy{scrollEventTimeoutMillis=%d}",
        scrollEventTimeoutMillis);
  }

  @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) {
    SwipeAction.toScroll(direction).perform(container.getInjector(), container);
  }
}