1 /* 2 * Copyright 2019 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 androidx.testutils; 18 19 import static android.os.SystemClock.uptimeMillis; 20 21 import android.annotation.SuppressLint; 22 import android.app.Instrumentation; 23 import android.os.SystemClock; 24 import android.view.MotionEvent; 25 import android.view.View; 26 27 import androidx.annotation.NonNull; 28 import androidx.test.espresso.action.CoordinatesProvider; 29 30 /** 31 * Injects motion events for custom gestures using Instrumentation. Use this to "draw" the gestures, 32 * in a similar way as a Path is defined. Create an instance of this class, start a drag, move it 33 * around and then finish it. For example, this injects a "Swipe right then left" gesture from 34 * TalkBack: 35 * 36 * <pre> 37 * View view = findViewById(R.id.view_to_swipe_on); 38 * 39 * SwipeInjector swiper = new SwipeInjector(InstrumentationRegistry.getInstrumentation()); 40 * swiper.startDrag(GeneralLocation.LEFT, view); // Start at the left side of the view 41 * swiper.dragTo(GeneralLocation.RIGHT, view, 200); // Swipe to the right in 200 ms 42 * swiper.dragTo(GeneralLocation.LEFT, view, 200); // Swipe to the left in 200 ms 43 * swiper.finishDrag(); // Finish the gesture 44 * </pre> 45 * 46 * @see androidx.test.espresso.action.GeneralLocation GeneralLocation 47 * @see TranslatedCoordinatesProvider 48 */ 49 public class SwipeInjector { 50 51 private static final int X = 0; 52 private static final int Y = 1; 53 54 private Instrumentation mInstrumentation; 55 56 private long mStartTime = 0; 57 private long mCurrentTime = 0; 58 private float[] mFloats = new float[2]; 59 60 /** 61 * Creates an injector. 62 */ SwipeInjector(@onNull Instrumentation instrumentation)63 public SwipeInjector(@NonNull Instrumentation instrumentation) { 64 mInstrumentation = instrumentation; 65 } 66 67 /** 68 * Start a drag on the coordinate provided for the given view. For example, 69 * {@code startDrag(GeneralLocation.CENTER, mView)}. 70 * 71 * @param provider provider of the coordinate of the pointer down event, like 72 * {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER} 73 * @param view the view passed to the 74 * {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider} 75 */ 76 @SuppressLint("LambdaLast") startDrag(@onNull CoordinatesProvider provider, @NonNull View view)77 public void startDrag(@NonNull CoordinatesProvider provider, @NonNull View view) { 78 float[] floats = provider.calculateCoordinates(view); 79 startDrag(floats[X], floats[Y]); 80 } 81 82 /** 83 * Start a drag on the given coordinate. For example, {@code startDrag(100, 200)}. Note that the 84 * coordinates are absolute coordinates for the screen. 85 * 86 * @param x the x coordinate of the pointer down event 87 * @param y the y coordinate of the pointer down event 88 */ startDrag(float x, float y)89 public void startDrag(float x, float y) { 90 mStartTime = uptimeMillis(); 91 mFloats[X] = x; 92 mFloats[Y] = y; 93 injectMotionEvent(obtainDownEvent(mStartTime, mFloats)); 94 } 95 96 /** 97 * Extend the drag with a single motion event to the coordinates provided for the given view. 98 * For example, {@code dragTo(GeneralLocation.LEFT, mView)}. Call one of the {@code startDrag} 99 * methods before calling any of the {@code dragTo} methods. 100 * 101 * @param provider provider of the coordinate of the pointer move event, like 102 * {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER} 103 * @param view the view passed to the 104 * {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider} 105 */ 106 @SuppressLint("LambdaLast") dragTo(@onNull CoordinatesProvider provider, @NonNull View view)107 public void dragTo(@NonNull CoordinatesProvider provider, @NonNull View view) { 108 float[] floats = provider.calculateCoordinates(view); 109 dragTo(floats[X], floats[Y]); 110 } 111 112 /** 113 * Extend the drag with a single motion event to the given coordinate. For example, 114 * {@code dragTo(0, 10)}. Call one of the {@code startDrag} methods before calling any of the 115 * {@code dragTo} methods. Note that the coordinates are absolute coordinates for the screen. 116 * 117 * @param x the x coordinate of the pointer move event 118 * @param y the y coordinate of the pointer move event 119 */ dragTo(float x, float y)120 public void dragTo(float x, float y) { 121 mFloats[X] = x; 122 mFloats[Y] = y; 123 injectMotionEvent(obtainMoveEvent(mStartTime, uptimeMillis(), mFloats)); 124 } 125 126 /** 127 * Extend the drag with a range of motion events to the coordinate provided for the given view. 128 * An event will be injected every 10ms, interpolating linearly to the destination. For example, 129 * {@code dragTo(GeneralLocation.LEFT, mView, 300)}. Call one of the {@code startDrag} methods 130 * before calling any of the {@code dragTo} methods. 131 * 132 * @param provider provider of the final coordinate of this part of the drag, like 133 * {@link androidx.test.espresso.action.GeneralLocation#CENTER GeneralLocation.CENTER} 134 * @param view the view passed to the 135 * {@link CoordinatesProvider#calculateCoordinates(View) coordinates provider} 136 * @param duration the time in milliseconds this part of the drag should take. Actual time will 137 * be different if not a multiple of 10. 138 */ 139 @SuppressLint("LambdaLast") dragTo(@onNull CoordinatesProvider provider, @NonNull View view, long duration)140 public void dragTo(@NonNull CoordinatesProvider provider, @NonNull View view, long duration) { 141 float[] floats = provider.calculateCoordinates(view); 142 dragTo(floats[X], floats[Y], duration); 143 } 144 145 /** 146 * Extend the drag with a range of motion events to the given coordinate. An event will be 147 * injected every 10ms, interpolating linearly to the destination. For example, 148 * {@code dragTo(10, 0, 350)}. Call one of the {@code startDrag} methods before calling any of 149 * the {@code dragTo} methods. Note that the coordinates are absolute coordinates for the 150 * screen. 151 * 152 * @param x the final x coordinate of this part of the drag 153 * @param y the final y coordinate of this part of the drag 154 * @param duration the time in milliseconds this part of the drag should take. Actual time will 155 * be different if not a multiple of 10. 156 */ dragTo(float x, float y, long duration)157 public void dragTo(float x, float y, long duration) { 158 float x0 = mFloats[X]; 159 float y0 = mFloats[Y]; 160 float dx = x - x0; 161 float dy = y - y0; 162 int steps = Math.max(1, Math.round(duration / 10f)); 163 for (int i = 1; i <= steps; i++) { 164 float progress = (float) i / steps; 165 mFloats[X] = x0 + dx * progress; 166 mFloats[Y] = y0 + dy * progress; 167 injectMotionEvent(obtainMoveEvent(mStartTime, mCurrentTime + 10L, mFloats)); 168 } 169 } 170 171 /** 172 * Extend the drag with a single motion event covering the given delta. For example, 173 * {@code dragBy(10, -20)}. Call one of the {@code startDrag} methods before calling any of 174 * the {@code dragBy} methods. 175 * 176 * @param dx the distance in x direction 177 * @param dy the distance in y direction 178 */ dragBy(float dx, float dy)179 public void dragBy(float dx, float dy) { 180 dragTo(mFloats[X] + dx, mFloats[Y] + dy); 181 } 182 183 /** 184 * Extend the drag with a range of motion events covering the given delta. An event will be 185 * injected every 10ms, interpolating linearly to the destination. For example, 186 * {@code dragBy(-50, 0, 100)}. Call one of the {@code startDrag} methods before calling any of 187 * the {@code dragBy} methods. 188 * 189 * @param dx the distance in x direction 190 * @param dy the distance in y direction 191 * @param duration the time in milliseconds this part of the drag should take. Actual time will 192 * be different if not a multiple of 10. 193 */ dragBy(float dx, float dy, long duration)194 public void dragBy(float dx, float dy, long duration) { 195 dragTo(mFloats[X] + dx, mFloats[Y] + dy, duration); 196 } 197 198 /** 199 * Finish the drag with a pointer up event. A new drag can be started after this with one of the 200 * {@code startDrag} methods. 201 */ finishDrag()202 public void finishDrag() { 203 injectMotionEvent(obtainUpEvent(mStartTime, uptimeMillis(), mFloats)); 204 } 205 obtainDownEvent(long time, float[] coord)206 private static MotionEvent obtainDownEvent(long time, float[] coord) { 207 return MotionEvent.obtain(time, time, 208 MotionEvent.ACTION_DOWN, coord[X], coord[Y], 0); 209 } 210 obtainMoveEvent(long startTime, long time, float[] coord)211 private static MotionEvent obtainMoveEvent(long startTime, long time, float[] coord) { 212 return MotionEvent.obtain(startTime, time, 213 MotionEvent.ACTION_MOVE, coord[X], coord[Y], 0); 214 } 215 obtainUpEvent(long startTime, long time, float[] coord)216 private static MotionEvent obtainUpEvent(long startTime, long time, float[] coord) { 217 return MotionEvent.obtain(startTime, time, 218 MotionEvent.ACTION_UP, coord[X], coord[Y], 0); 219 } 220 injectMotionEvent(MotionEvent event)221 private void injectMotionEvent(MotionEvent event) { 222 try { 223 long eventTime = event.getEventTime(); 224 long now = uptimeMillis(); 225 if (eventTime - now > 0) { 226 SystemClock.sleep(eventTime - now); 227 } 228 mInstrumentation.sendPointerSync(event); 229 mCurrentTime = eventTime; 230 } finally { 231 event.recycle(); 232 } 233 } 234 } 235