Home | History | Annotate | Download | only in espresso
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      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 
     17 package android.widget.espresso;
     18 
     19 import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
     20 import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
     21 import static com.android.internal.util.Preconditions.checkNotNull;
     22 import static org.hamcrest.Matchers.allOf;
     23 import android.annotation.Nullable;
     24 import android.os.SystemClock;
     25 import android.support.test.espresso.UiController;
     26 import android.support.test.espresso.PerformException;
     27 import android.support.test.espresso.ViewAction;
     28 import android.support.test.espresso.action.CoordinatesProvider;
     29 import android.support.test.espresso.action.MotionEvents;
     30 import android.support.test.espresso.action.PrecisionDescriber;
     31 import android.support.test.espresso.action.Swiper;
     32 import android.support.test.espresso.util.HumanReadables;
     33 import android.util.Log;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewConfiguration;
     37 
     38 import org.hamcrest.Matcher;
     39 
     40 
     41 /**
     42  * Drags on a View using touch events.<br>
     43  * <br>
     44  * View constraints:
     45  * <ul>
     46  * <li>must be displayed on screen
     47  * <ul>
     48  */
     49 public final class DragAction implements ViewAction {
     50     public interface Dragger extends Swiper {
     51         UiController wrapUiController(UiController uiController);
     52     }
     53 
     54     /**
     55      * Executes different drag types to given positions.
     56      */
     57     public enum Drag implements Dragger {
     58 
     59         /**
     60          * Starts a drag with a mouse down.
     61          */
     62         MOUSE_DOWN {
     63             private DownMotionPerformer downMotion = new DownMotionPerformer() {
     64                 @Override
     65                 public MotionEvent perform(
     66                         UiController uiController, float[] coordinates, float[] precision) {
     67                     MotionEvent downEvent = MotionEvents.sendDown(
     68                             uiController, coordinates, precision)
     69                             .down;
     70                     return downEvent;
     71                 }
     72             };
     73 
     74             @Override
     75             public Status sendSwipe(
     76                     UiController uiController,
     77                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
     78                 return sendLinearDrag(
     79                         uiController, downMotion, startCoordinates, endCoordinates, precision);
     80             }
     81 
     82             @Override
     83             public String toString() {
     84                 return "mouse down and drag";
     85             }
     86 
     87             @Override
     88             public UiController wrapUiController(UiController uiController) {
     89                 return new MouseUiController(uiController);
     90             }
     91         },
     92 
     93         /**
     94          * Starts a drag with a mouse double click.
     95          */
     96         MOUSE_DOUBLE_CLICK {
     97             private DownMotionPerformer downMotion = new DownMotionPerformer() {
     98                 @Override
     99                 @Nullable
    100                 public MotionEvent perform(
    101                         UiController uiController,  float[] coordinates, float[] precision) {
    102                     return performDoubleTap(uiController, coordinates, precision);
    103                 }
    104             };
    105 
    106             @Override
    107             public Status sendSwipe(
    108                     UiController uiController,
    109                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    110                 return sendLinearDrag(
    111                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    112             }
    113 
    114             @Override
    115             public String toString() {
    116                 return "mouse double click and drag to select";
    117             }
    118 
    119             @Override
    120             public UiController wrapUiController(UiController uiController) {
    121                 return new MouseUiController(uiController);
    122             }
    123         },
    124 
    125         /**
    126          * Starts a drag with a mouse long click.
    127          */
    128         MOUSE_LONG_CLICK {
    129             private DownMotionPerformer downMotion = new DownMotionPerformer() {
    130                 @Override
    131                 public MotionEvent perform(
    132                         UiController uiController, float[] coordinates, float[] precision) {
    133                     MotionEvent downEvent = MotionEvents.sendDown(
    134                             uiController, coordinates, precision)
    135                             .down;
    136                     return performLongPress(uiController, coordinates, precision);
    137                 }
    138             };
    139 
    140             @Override
    141             public Status sendSwipe(
    142                     UiController uiController,
    143                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    144                 return sendLinearDrag(
    145                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    146             }
    147 
    148             @Override
    149             public String toString() {
    150                 return "mouse long click and drag to select";
    151             }
    152 
    153             @Override
    154             public UiController wrapUiController(UiController uiController) {
    155                 return new MouseUiController(uiController);
    156             }
    157         },
    158 
    159         /**
    160          * Starts a drag with a mouse triple click.
    161          */
    162         MOUSE_TRIPLE_CLICK {
    163             private DownMotionPerformer downMotion = new DownMotionPerformer() {
    164                 @Override
    165                 @Nullable
    166                 public MotionEvent perform(
    167                         UiController uiController, float[] coordinates, float[] precision) {
    168                     MotionEvent downEvent = MotionEvents.sendDown(
    169                             uiController, coordinates, precision)
    170                             .down;
    171                     for (int i = 0; i < 2; ++i) {
    172                         try {
    173                             if (!MotionEvents.sendUp(uiController, downEvent)) {
    174                                 String logMessage = "Injection of up event as part of the triple "
    175                                         + "click failed. Sending cancel event.";
    176                                 Log.d(TAG, logMessage);
    177                                 MotionEvents.sendCancel(uiController, downEvent);
    178                                 return null;
    179                             }
    180 
    181                             long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime();
    182                             uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout);
    183                         } finally {
    184                             downEvent.recycle();
    185                         }
    186                         downEvent = MotionEvents.sendDown(
    187                                 uiController, coordinates, precision).down;
    188                     }
    189                     return downEvent;
    190                 }
    191             };
    192 
    193             @Override
    194             public Status sendSwipe(
    195                     UiController uiController,
    196                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    197                 return sendLinearDrag(
    198                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    199             }
    200 
    201             @Override
    202             public String toString() {
    203                 return "mouse triple click and drag to select";
    204             }
    205 
    206             @Override
    207             public UiController wrapUiController(UiController uiController) {
    208                 return new MouseUiController(uiController);
    209             }
    210         },
    211 
    212         /**
    213          * Starts a drag with a tap.
    214          */
    215         TAP {
    216             private DownMotionPerformer downMotion = new DownMotionPerformer() {
    217                 @Override
    218                 public MotionEvent perform(
    219                         UiController uiController, float[] coordinates, float[] precision) {
    220                     MotionEvent downEvent = MotionEvents.sendDown(
    221                             uiController, coordinates, precision)
    222                             .down;
    223                     return downEvent;
    224                 }
    225             };
    226 
    227             @Override
    228             public Status sendSwipe(
    229                     UiController uiController,
    230                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    231                 return sendLinearDrag(
    232                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    233             }
    234 
    235             @Override
    236             public String toString() {
    237                 return "tap and drag";
    238             }
    239         },
    240 
    241         /**
    242          * Starts a drag with a long-press.
    243          */
    244         LONG_PRESS {
    245             private DownMotionPerformer downMotion = new DownMotionPerformer() {
    246                 @Override
    247                 public MotionEvent perform(
    248                         UiController uiController, float[] coordinates, float[] precision) {
    249                     return performLongPress(uiController, coordinates, precision);
    250                 }
    251             };
    252 
    253             @Override
    254             public Status sendSwipe(
    255                     UiController uiController,
    256                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    257                 return sendLinearDrag(
    258                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    259             }
    260 
    261             @Override
    262             public String toString() {
    263                 return "long press and drag";
    264             }
    265         },
    266 
    267         /**
    268          * Starts a drag with a double-tap.
    269          */
    270         DOUBLE_TAP {
    271             private DownMotionPerformer downMotion = new DownMotionPerformer() {
    272                 @Override
    273                 @Nullable
    274                 public MotionEvent perform(
    275                         UiController uiController,  float[] coordinates, float[] precision) {
    276                     return performDoubleTap(uiController, coordinates, precision);
    277                 }
    278             };
    279 
    280             @Override
    281             public Status sendSwipe(
    282                     UiController uiController,
    283                     float[] startCoordinates, float[] endCoordinates, float[] precision) {
    284                 return sendLinearDrag(
    285                         uiController, downMotion, startCoordinates, endCoordinates, precision);
    286             }
    287 
    288             @Override
    289             public String toString() {
    290                 return "double-tap and drag";
    291             }
    292         };
    293 
    294         private static final String TAG = Drag.class.getSimpleName();
    295 
    296         /** The number of move events to send for each drag. */
    297         private static final int DRAG_STEP_COUNT = 10;
    298 
    299         /** Length of time a drag should last for, in milliseconds. */
    300         private static final int DRAG_DURATION = 1500;
    301 
    302         /** Duration between the last move event and the up event, in milliseconds. */
    303         private static final int WAIT_BEFORE_SENDING_UP = 400;
    304 
    305         private static Status sendLinearDrag(
    306                 UiController uiController, DownMotionPerformer downMotion,
    307                 float[] startCoordinates, float[] endCoordinates, float[] precision) {
    308             float[][] steps = interpolate(startCoordinates, endCoordinates);
    309             final int delayBetweenMovements = DRAG_DURATION / steps.length;
    310 
    311             MotionEvent downEvent = downMotion.perform(uiController, startCoordinates, precision);
    312             if (downEvent == null) {
    313                 return Status.FAILURE;
    314             }
    315 
    316             try {
    317                 for (int i = 0; i < steps.length; i++) {
    318                     if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) {
    319                         String logMessage = "Injection of move event as part of the drag failed. " +
    320                                 "Sending cancel event.";
    321                         Log.e(TAG, logMessage);
    322                         MotionEvents.sendCancel(uiController, downEvent);
    323                         return Status.FAILURE;
    324                     }
    325 
    326                     long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i;
    327                     long timeUntilDesired = desiredTime - SystemClock.uptimeMillis();
    328                     if (timeUntilDesired > 10) {
    329                         // If the wait time until the next event isn't long enough, skip the wait
    330                         // and execute the next event.
    331                         uiController.loopMainThreadForAtLeast(timeUntilDesired);
    332                     }
    333                 }
    334 
    335                 // Wait before sending up because some drag handling logic may discard move events
    336                 // that has been sent immediately before the up event. e.g. HandleView.
    337                 uiController.loopMainThreadForAtLeast(WAIT_BEFORE_SENDING_UP);
    338 
    339                 if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) {
    340                     String logMessage = "Injection of up event as part of the drag failed. " +
    341                             "Sending cancel event.";
    342                     Log.e(TAG, logMessage);
    343                     MotionEvents.sendCancel(uiController, downEvent);
    344                     return Status.FAILURE;
    345                 }
    346             } finally {
    347                 downEvent.recycle();
    348             }
    349             return Status.SUCCESS;
    350         }
    351 
    352         private static float[][] interpolate(float[] start, float[] end) {
    353             float[][] res = new float[DRAG_STEP_COUNT][2];
    354 
    355             for (int i = 0; i < DRAG_STEP_COUNT; i++) {
    356                 res[i][0] = start[0] + (end[0] - start[0]) * i / (DRAG_STEP_COUNT - 1f);
    357                 res[i][1] = start[1] + (end[1] - start[1]) * i / (DRAG_STEP_COUNT - 1f);
    358             }
    359 
    360             return res;
    361         }
    362 
    363         private static MotionEvent performLongPress(
    364                 UiController uiController, float[] coordinates, float[] precision) {
    365             MotionEvent downEvent = MotionEvents.sendDown(
    366                     uiController, coordinates, precision)
    367                     .down;
    368             // Duration before a press turns into a long press.
    369             // Factor 1.5 is needed, otherwise a long press is not safely detected.
    370             // See android.test.TouchUtils longClickView
    371             long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
    372             uiController.loopMainThreadForAtLeast(longPressTimeout);
    373             return downEvent;
    374         }
    375 
    376         @Nullable
    377         private static MotionEvent performDoubleTap(
    378                 UiController uiController,  float[] coordinates, float[] precision) {
    379             MotionEvent downEvent = MotionEvents.sendDown(
    380                     uiController, coordinates, precision)
    381                     .down;
    382             try {
    383                 if (!MotionEvents.sendUp(uiController, downEvent)) {
    384                     String logMessage = "Injection of up event as part of the double tap " +
    385                             "failed. Sending cancel event.";
    386                     Log.d(TAG, logMessage);
    387                     MotionEvents.sendCancel(uiController, downEvent);
    388                     return null;
    389                 }
    390 
    391                 long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime();
    392                 uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout);
    393 
    394                 return MotionEvents.sendDown(uiController, coordinates, precision).down;
    395             } finally {
    396                 downEvent.recycle();
    397             }
    398         }
    399 
    400         @Override
    401         public UiController wrapUiController(UiController uiController) {
    402             return uiController;
    403         }
    404     }
    405 
    406     /**
    407      * Interface to implement different "down motion" types.
    408      */
    409     private interface DownMotionPerformer {
    410         /**
    411          * Performs and returns a down motion.
    412          *
    413          * @param uiController a UiController to use to send MotionEvents to the screen.
    414          * @param coordinates a float[] with x and y values of center of the tap.
    415          * @param precision  a float[] with x and y values of precision of the tap.
    416          * @return the down motion event or null if the down motion event failed.
    417          */
    418         @Nullable
    419         MotionEvent perform(UiController uiController, float[] coordinates, float[] precision);
    420     }
    421 
    422     private final Dragger mDragger;
    423     private final CoordinatesProvider mStartCoordinatesProvider;
    424     private final CoordinatesProvider mEndCoordinatesProvider;
    425     private final PrecisionDescriber mPrecisionDescriber;
    426     private final Class<? extends View> mViewClass;
    427 
    428     public DragAction(
    429             Dragger dragger,
    430             CoordinatesProvider startCoordinatesProvider,
    431             CoordinatesProvider endCoordinatesProvider,
    432             PrecisionDescriber precisionDescriber,
    433             Class<? extends View> viewClass) {
    434         mDragger = checkNotNull(dragger);
    435         mStartCoordinatesProvider = checkNotNull(startCoordinatesProvider);
    436         mEndCoordinatesProvider = checkNotNull(endCoordinatesProvider);
    437         mPrecisionDescriber = checkNotNull(precisionDescriber);
    438         mViewClass = viewClass;
    439     }
    440 
    441     @Override
    442     @SuppressWarnings("unchecked")
    443     public Matcher<View> getConstraints() {
    444         return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass));
    445     }
    446 
    447     @Override
    448     public void perform(UiController uiController, View view) {
    449         checkNotNull(uiController);
    450         checkNotNull(view);
    451 
    452         uiController = mDragger.wrapUiController(uiController);
    453 
    454         float[] startCoordinates = mStartCoordinatesProvider.calculateCoordinates(view);
    455         float[] endCoordinates = mEndCoordinatesProvider.calculateCoordinates(view);
    456         float[] precision = mPrecisionDescriber.describePrecision();
    457 
    458         Swiper.Status status;
    459 
    460         try {
    461             status = mDragger.sendSwipe(
    462                     uiController, startCoordinates, endCoordinates, precision);
    463         } catch (RuntimeException re) {
    464             throw new PerformException.Builder()
    465                     .withActionDescription(this.getDescription())
    466                     .withViewDescription(HumanReadables.describe(view))
    467                     .withCause(re)
    468                     .build();
    469         }
    470 
    471         int duration = ViewConfiguration.getPressedStateDuration();
    472         // ensures that all work enqueued to process the swipe has been run.
    473         if (duration > 0) {
    474             uiController.loopMainThreadForAtLeast(duration);
    475         }
    476 
    477         if (status == Swiper.Status.FAILURE) {
    478             throw new PerformException.Builder()
    479                     .withActionDescription(getDescription())
    480                     .withViewDescription(HumanReadables.describe(view))
    481                     .withCause(new RuntimeException(getDescription() + " failed"))
    482                     .build();
    483         }
    484     }
    485 
    486     @Override
    487     public String getDescription() {
    488         return mDragger.toString();
    489     }
    490 }
    491