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