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