• 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.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