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