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