• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 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 package android.platform.uiautomator_helpers
17 
18 import android.animation.TimeInterpolator
19 import android.graphics.Point
20 import android.graphics.PointF
21 import android.os.SystemClock
22 import android.os.SystemClock.sleep
23 import android.util.Log
24 import android.view.InputDevice
25 import android.view.MotionEvent
26 import android.view.MotionEvent.TOOL_TYPE_FINGER
27 import android.view.animation.DecelerateInterpolator
28 import android.view.animation.LinearInterpolator
29 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
30 import com.google.common.truth.Truth.assertThat
31 import java.time.Duration
32 import java.time.temporal.ChronoUnit.MILLIS
33 import java.util.concurrent.atomic.AtomicInteger
34 
35 private val DEFAULT_DURATION: Duration = Duration.of(500, MILLIS)
36 private val PAUSE_DURATION: Duration = Duration.of(250, MILLIS)
37 private val GESTURE_STEP = Duration.of(16, MILLIS)
38 
39 /**
40  * Allows fine control of swipes on the screen.
41  *
42  * Guarantees that all touches are dispatched, as opposed to [UiDevice] APIs, that might lose
43  * touches in case of high load.
44  *
45  * It is possible to perform operation before the swipe finishes. Timestamp of touch events are set
46  * according to initial time and duration.
47  *
48  * Example usage:
49  * ```
50  * val swipe = BetterSwipe.from(startPoint).to(intermediatePoint)
51  *
52  * assertThat(someUiState).isTrue();
53  *
54  * swipe.to(anotherPoint).release()
55  * ```
56  */
57 object BetterSwipe {
58 
59     private val lastPointerId = AtomicInteger(0)
60 
61     /** Starts a swipe from [start] at the current time. */
62     @JvmStatic fun from(start: PointF) = Swipe(start)
63 
64     /** Starts a swipe from [start] at the current time. */
65     @JvmStatic fun from(start: Point) = Swipe(PointF(start.x.toFloat(), start.y.toFloat()))
66 
67     /** Starts a swipe for each [starts] point at the current time. */
68     @JvmStatic fun from(vararg starts: PointF) = Swipes(*starts)
69 
70     class Swipe internal constructor(start: PointF) {
71 
72         private val downTime = SystemClock.uptimeMillis()
73         private val pointerId = lastPointerId.incrementAndGet()
74         private var lastPoint: PointF = start
75         private var lastTime: Long = downTime
76         private var released = false
77 
78         init {
79             log("Touch $pointerId started at $start")
80             sendPointer(currentTime = downTime, action = MotionEvent.ACTION_DOWN, point = start)
81         }
82 
83         /**
84          * Swipes from the current point to [end] in [duration] using [interpolator] for the gesture
85          * speed. Pass [FLING_GESTURE_INTERPOLATOR] for a fling-like gesture that may leave the
86          * surface moving by inertia. Don't use it to drag objects to a precisely specified
87          * position. [PRECISE_GESTURE_INTERPOLATOR] will result in a precise drag-like gesture not
88          * triggering inertia.
89          */
90         @JvmOverloads
91         fun to(
92             end: PointF,
93             duration: Duration = DEFAULT_DURATION,
94             interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
95         ): Swipe {
96             throwIfReleased()
97             log(
98                 "Swiping from $lastPoint to $end in $duration " +
99                     "using ${interpolator.javaClass.simpleName}"
100             )
101             lastTime = movePointer(duration = duration, from = lastPoint, to = end, interpolator)
102             lastPoint = end
103             return this
104         }
105 
106         /**
107          * Swipes from the current point to [end] in [duration] using [interpolator] for the gesture
108          * speed. Pass [FLING_GESTURE_INTERPOLATOR] for a fling-like gesture that may leave the
109          * surface moving by inertia. Don't use it to drag objects to a precisely specified
110          * position. [PRECISE_GESTURE_INTERPOLATOR] will result in a precise drag-like gesture not
111          * triggering inertia.
112          */
113         @JvmOverloads
114         fun to(
115             end: Point,
116             duration: Duration = DEFAULT_DURATION,
117             interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
118         ): Swipe {
119             return to(PointF(end.x.toFloat(), end.y.toFloat()), duration, interpolator)
120         }
121 
122         /** Sends the last point, simulating a finger pause. */
123         fun pause(): Swipe {
124             return to(PointF(lastPoint.x, lastPoint.y), PAUSE_DURATION)
125         }
126 
127         /** Moves the pointer up, finishing the swipe. Further calls will result in an exception. */
128         @JvmOverloads
129         fun release(sync: Boolean = true) {
130             throwIfReleased()
131             log("Touch $pointerId released at $lastPoint")
132             sendPointer(
133                 currentTime = lastTime,
134                 action = MotionEvent.ACTION_UP,
135                 point = lastPoint,
136                 sync = sync
137             )
138             lastPointerId.decrementAndGet()
139             released = true
140         }
141 
142         /** Moves the pointer by [delta], sending the event at [currentTime]. */
143         internal fun moveBy(delta: PointF, currentTime: Long, sync: Boolean) {
144             val targetPoint = PointF(lastPoint.x + delta.x, lastPoint.y + delta.y)
145             sendPointer(currentTime, MotionEvent.ACTION_MOVE, targetPoint, sync)
146             lastTime = currentTime
147             lastPoint = targetPoint
148         }
149 
150         private fun throwIfReleased() {
151             check(!released) { "Trying to perform a swipe operation after pointer released" }
152         }
153 
154         private fun sendPointer(
155             currentTime: Long,
156             action: Int,
157             point: PointF,
158             sync: Boolean = true
159         ) {
160             val event = getMotionEvent(downTime, currentTime, action, point, pointerId)
161             check(
162                 getInstrumentation()
163                     .uiAutomation
164                     .injectInputEvent(event, sync, /* waitForAnimations= */ false)
165             ) {
166                 "Touch injection failed"
167             }
168             event.recycle()
169         }
170 
171         /** Returns the time when movement finished. */
172         private fun movePointer(
173             duration: Duration,
174             from: PointF,
175             to: PointF,
176             interpolator: TimeInterpolator
177         ): Long {
178             val stepTimeMs = GESTURE_STEP.toMillis()
179             val durationMs = duration.toMillis()
180             val steps = durationMs / stepTimeMs
181             val startTime = lastTime
182             var currentTime = lastTime
183             for (i in 0 until steps) {
184                 sleep(stepTimeMs)
185                 currentTime += stepTimeMs
186                 val progress = interpolator.getInterpolation(i / (steps - 1f))
187                 val point = from.lerp(progress, to)
188                 sendPointer(currentTime, MotionEvent.ACTION_MOVE, point)
189             }
190             assertThat(currentTime).isEqualTo(startTime + stepTimeMs * steps)
191             return currentTime
192         }
193     }
194 
195     /** Collection of swipes. This can be used to simulate multitouch. */
196     class Swipes internal constructor(vararg starts: PointF) {
197 
198         private var lastTime: Long = SystemClock.uptimeMillis()
199         private val swipes: List<Swipe> = starts.map { Swipe(it) }
200 
201         /** Moves all the swipes by [delta], in [duration] time with constant speed. */
202         fun moveBy(delta: PointF, duration: Duration = DEFAULT_DURATION): Swipes {
203             log("Moving ${swipes.size} touches by $delta")
204 
205             val stepTimeMs = GESTURE_STEP.toMillis()
206             val durationMs = duration.toMillis()
207             val steps = durationMs / stepTimeMs
208             val startTime = lastTime
209             var currentTime = lastTime
210             val stepDelta = PointF(delta.x / steps, delta.y / steps)
211             (1..steps).forEach { _ ->
212                 sleep(stepTimeMs)
213                 currentTime += stepTimeMs
214                 swipes.forEach { swipe ->
215                     // Sending the move events as not "sync". Otherwise the method waits for them
216                     // to be displatched. As here we're sending many that are supposed to happen at
217                     // the same time, we don't want the method to
218                     // wait after each single injection.
219                     swipe.moveBy(stepDelta, currentTime, sync = false)
220                 }
221             }
222             assertThat(currentTime).isEqualTo(startTime + stepTimeMs * steps)
223             lastTime = currentTime
224             return this
225         }
226 
227         /** Moves pointers up, finishing the swipe. Further calls will result in an exception. */
228         fun release() {
229             swipes.forEach { it.release(sync = false) }
230         }
231     }
232 
233     private fun log(s: String) = Log.d("BetterSwipe", s)
234 }
235 
getMotionEventnull236 private fun getMotionEvent(
237     downTime: Long,
238     eventTime: Long,
239     action: Int,
240     p: PointF,
241     pointerId: Int,
242 ): MotionEvent {
243     val properties =
244         MotionEvent.PointerProperties().apply {
245             id = pointerId
246             toolType = TOOL_TYPE_FINGER
247         }
248     val coordinates =
249         MotionEvent.PointerCoords().apply {
250             pressure = 1f
251             size = 1f
252             x = p.x
253             y = p.y
254         }
255     return MotionEvent.obtain(
256         /* downTime= */ downTime,
257         /* eventTime= */ eventTime,
258         /* action= */ action,
259         /* pointerCount= */ 1,
260         /* pointerProperties= */ arrayOf(properties),
261         /* pointerCoords= */ arrayOf(coordinates),
262         /* metaState= */ 0,
263         /* buttonState= */ 0,
264         /* xPrecision= */ 1.0f,
265         /* yPrecision= */ 1.0f,
266         /* deviceId= */ 0,
267         /* edgeFlags= */ 0,
268         /* source= */ InputDevice.SOURCE_TOUCHSCREEN,
269         /* flags= */ 0
270     )
271 }
272 
lerpnull273 private fun PointF.lerp(amount: Float, b: PointF) =
274     PointF(lerp(x, b.x, amount), lerp(y, b.y, amount))
275 
276 private fun lerp(start: Float, stop: Float, amount: Float): Float = start + (stop - start) * amount
277 
278 /**
279  * Interpolator for a fling-like gesture that may leave the surface moving by inertia. Don't use it
280  * to drag objects to a precisely specified position.
281  */
282 val FLING_GESTURE_INTERPOLATOR = LinearInterpolator()
283 
284 /** Interpolator for a precise drag-like gesture not triggering inertia. */
285 val PRECISE_GESTURE_INTERPOLATOR = DecelerateInterpolator()
286