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 */ 16 package com.google.android.droiddriver.scroll; 17 18 import android.app.UiAutomation; 19 import android.app.UiAutomation.AccessibilityEventFilter; 20 import android.util.Log; 21 import android.view.accessibility.AccessibilityEvent; 22 23 import com.google.android.droiddriver.DroidDriver; 24 import com.google.android.droiddriver.UiElement; 25 import com.google.android.droiddriver.actions.SwipeAction; 26 import com.google.android.droiddriver.exceptions.UnrecoverableException; 27 import com.google.android.droiddriver.finders.Finder; 28 import com.google.android.droiddriver.scroll.Direction.Axis; 29 import com.google.android.droiddriver.scroll.Direction.DirectionConverter; 30 import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; 31 import com.google.android.droiddriver.util.Logs; 32 33 import 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 */ 47 public 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 for a 72 * 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 87 // elapsed. 88 return false; 89 } 90 91 public AccessibilityEvent getLastEvent() { 92 return lastEvent; 93 } 94 } 95 96 private final UiAutomation uiAutomation; 97 private final long scrollEventTimeoutMillis; 98 private final DirectionConverter directionConverter; 99 private final EndData endData = new EndData(); 100 101 public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation, 102 long scrollEventTimeoutMillis, DirectionConverter converter) { 103 this.uiAutomation = uiAutomation; 104 this.scrollEventTimeoutMillis = scrollEventTimeoutMillis; 105 this.directionConverter = converter; 106 } 107 108 @Override 109 public boolean scroll(DroidDriver driver, Finder containerFinder, 110 final PhysicalDirection direction) { 111 // Check if we've reached end after last scroll. 112 if (endData.match(containerFinder, direction)) { 113 return false; 114 } 115 116 AccessibilityEvent event = doScrollAndReturnEvent(driver.on(containerFinder), direction); 117 if (detectEnd(event, direction.axis())) { 118 endData.set(containerFinder, direction); 119 Logs.log(Log.DEBUG, "reached scroll end with event: " + event); 120 } 121 122 // Clean up the event after use. 123 if (event != null) { 124 event.recycle(); 125 } 126 127 // Even if event == null, that does not mean scroll has no effect! 128 // Some views may not emit correct events when the content changed. 129 return true; 130 } 131 132 // Copied from UiAutomator. 133 // AdapterViews have indices we can use to check for the beginning. 134 protected boolean detectEnd(AccessibilityEvent event, Axis axis) { 135 if (event == null) { 136 return true; 137 } 138 139 if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) { 140 return event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex(); 141 } 142 if (event.getScrollX() != -1 && event.getScrollY() != -1) { 143 if (axis == Axis.VERTICAL) { 144 return event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY(); 145 } else if (axis == Axis.HORIZONTAL) { 146 return event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX(); 147 } 148 } 149 150 // This case is different from UiAutomator. 151 return event.getFromIndex() == -1 && event.getToIndex() == -1 && event.getItemCount() == -1 152 && event.getScrollX() == -1 && event.getScrollY() == -1; 153 } 154 155 @Override 156 public final DirectionConverter getDirectionConverter() { 157 return directionConverter; 158 } 159 160 @Override 161 public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 162 PhysicalDirection direction) { 163 endData.reset(); 164 } 165 166 @Override 167 public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 168 PhysicalDirection direction) {} 169 170 protected AccessibilityEvent doScrollAndReturnEvent(final UiElement container, 171 final PhysicalDirection direction) { 172 LastScrollEventFilter filter = new LastScrollEventFilter(); 173 try { 174 uiAutomation.executeAndWaitForEvent(new Runnable() { 175 @Override 176 public void run() { 177 doScroll(container, direction); 178 } 179 }, filter, scrollEventTimeoutMillis); 180 } catch (IllegalStateException e) { 181 throw new UnrecoverableException(e); 182 } catch (TimeoutException e) { 183 // We expect this because LastScrollEventFilter.accept always returns 184 // false. 185 } 186 return filter.getLastEvent(); 187 } 188 189 @Override 190 public void doScroll(final UiElement container, final PhysicalDirection direction) { 191 // We do not call container.scroll(direction) because it uses a SwipeAction 192 // with positive tTimeoutMillis. That path calls 193 // UiAutomation.executeAndWaitForEvent which clears the 194 // AccessibilityEvent Queue, preventing us from fetching the last 195 // accessibility event to determine if scrolling has finished. 196 container 197 .perform(new SwipeAction(direction, SwipeAction.getScrollSteps(), false /* drag */, 0L/* timeoutMillis */)); 198 } 199 200 /** 201 * Some widgets may not always fire correct {@link AccessibilityEvent}. 202 * Detecting end by null event is safer (at the cost of a extra scroll) than 203 * examining indices. 204 */ 205 public static class NullAccessibilityEventScrollStepStrategy extends 206 AccessibilityEventScrollStepStrategy { 207 208 public NullAccessibilityEventScrollStepStrategy(UiAutomation uiAutomation, 209 long scrollEventTimeoutMillis, DirectionConverter converter) { 210 super(uiAutomation, scrollEventTimeoutMillis, converter); 211 } 212 213 @Override 214 protected boolean detectEnd(AccessibilityEvent event, Axis axis) { 215 return event == null; 216 } 217 } 218 } 219