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