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 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