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