1 /*
<lambda>null2  * Copyright 2019 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 
17 package androidx.compose.ui.benchmark.input.pointer
18 
19 import android.view.InputDevice.SOURCE_TOUCHSCREEN
20 import android.view.MotionEvent
21 import android.view.MotionEvent.PointerCoords
22 import android.view.MotionEvent.PointerProperties
23 import android.view.View
24 
25 internal const val DefaultPointerInputTimeDelta = 100
26 internal const val DefaultPointerInputMoveAmountPx = 10f
27 
28 internal data class BenchmarkSimplifiedPointerInputPointer(val id: Int, val x: Float, val y: Float)
29 
30 /**
31  * Creates a simple [MotionEvent].
32  *
33  * @param dispatchTarget The [View] that the [MotionEvent] is going to be dispatched to. This
34  *   guarantees that the MotionEvent is created correctly for both Compose (which relies on raw
35  *   coordinates being correct) and Android (which requires that local coordinates are correct).
36  */
37 internal fun MotionEvent(
38     eventTime: Int,
39     action: Int,
40     numPointers: Int,
41     actionIndex: Int,
42     pointerProperties: Array<MotionEvent.PointerProperties>,
43     pointerCoords: Array<MotionEvent.PointerCoords>,
44     dispatchTarget: View
45 ): MotionEvent {
46 
47     // It's important we get the absolute coordinates first for the construction of the MotionEvent,
48     // and after it is created, adjust it back to the local coordinates. This way there is a history
49     // of the absolute coordinates for developers who rely on that (ViewGroup does this as well).
50     val locationOnScreen = IntArray(2) { 0 }
51     dispatchTarget.getLocationOnScreen(locationOnScreen)
52 
53     pointerCoords.forEach {
54         it.x += locationOnScreen[0]
55         it.y += locationOnScreen[1]
56     }
57 
58     val motionEvent =
59         MotionEvent.obtain(
60                 0,
61                 eventTime.toLong(),
62                 action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
63                 numPointers,
64                 pointerProperties,
65                 pointerCoords,
66                 0,
67                 0,
68                 0f,
69                 0f,
70                 0,
71                 0,
72                 SOURCE_TOUCHSCREEN, // Required for offsetLocation() to work correctly
73                 0
74             )
75             .apply {
76                 offsetLocation(-locationOnScreen[0].toFloat(), -locationOnScreen[1].toFloat())
77             }
78 
79     pointerCoords.forEach {
80         it.x -= locationOnScreen[0]
81         it.y -= locationOnScreen[1]
82     }
83 
84     return motionEvent
85 }
86 
87 @Suppress("RemoveRedundantQualifierName")
<lambda>null88 internal fun PointerProperties(id: Int) = MotionEvent.PointerProperties().apply { this.id = id }
89 
90 @Suppress("RemoveRedundantQualifierName")
PointerCoordsnull91 internal fun PointerCoords(x: Float, y: Float) =
92     MotionEvent.PointerCoords().apply {
93         this.x = x
94         this.y = y
95     }
96 
97 /**
98  * Creates an array of down [MotionEvent]s meaning the first event is a [MotionEvent.ACTION_DOWN]
99  * and all following events (if there are any) are [MotionEvent.ACTION_POINTER_DOWN]s. These will
100  * usually be paired with a [MotionEvent] up events.
101  *
102  * @param initialX Starting x coordinate for the first [MotionEvent]
103  * @param initialTime Starting time for the first [MotionEvent]
104  * @param y - Y used for all [MotionEvent]s (only x is updated for each moves).
105  * @param rootView - [View] that the [MotionEvent] is dispatched to.
106  * @param numberOfEvents Number of [MotionEvent]s to create.
107  * @param timeDelta - Time between each [MotionEvent] in milliseconds.
108  * @param moveDelta - Amount to move in pixels for each [MotionEvent]
109  */
createDownsnull110 internal fun createDowns(
111     initialX: Float,
112     initialTime: Int,
113     y: Float, // Same Y used for all moves
114     rootView: View,
115     numberOfEvents: Int = 1,
116     timeDelta: Int = DefaultPointerInputTimeDelta,
117     moveDelta: Float = DefaultPointerInputMoveAmountPx
118 ): Array<MotionEvent> {
119     if (numberOfEvents < 1) {
120         return emptyArray()
121     }
122 
123     var time = initialTime
124     var x = initialX
125 
126     val pointerProperties = mutableListOf<PointerProperties>()
127     val pointerCoords = mutableListOf<PointerCoords>()
128 
129     val downMotionEvents =
130         Array(numberOfEvents) { index ->
131             // Add pointers as we create down events
132             pointerProperties.add(index, PointerProperties(index))
133             pointerCoords.add(index, PointerCoords(x, y))
134 
135             val down =
136                 MotionEvent(
137                     time,
138                     if (index == 0) {
139                         MotionEvent.ACTION_DOWN
140                     } else {
141                         MotionEvent.ACTION_POINTER_DOWN
142                     },
143                     index + 1,
144                     index, // Used in conjunction with ACTION_POINTER_DOWN/UP
145                     pointerProperties.toTypedArray(),
146                     pointerCoords.toTypedArray(),
147                     rootView
148                 )
149 
150             time += timeDelta
151             x += moveDelta
152 
153             down // return down event
154         }
155     return downMotionEvents
156 }
157 
158 /**
159  * Creates an array of up [MotionEvent]s meaning alls up events are [MotionEvent.ACTION_POINTER_UP]s
160  * minus the last one (which is a [MotionEvent.ACTION_UP]). These will usually be paired with a
161  * [MotionEvent] down events.
162  *
163  * @param initialTime Starting time for the first [MotionEvent]
164  * @param initialPointers All pointers to create set of up events
165  * @param rootView - [View] that the [MotionEvent] is dispatched to.
166  * @param timeDelta - Time between each [MotionEvent] in milliseconds.
167  */
createUpsnull168 internal fun createUps(
169     initialTime: Int,
170     initialPointers: Array<BenchmarkSimplifiedPointerInputPointer>,
171     rootView: View,
172     timeDelta: Int = DefaultPointerInputTimeDelta
173 ): Array<MotionEvent> {
174     if (initialPointers.isEmpty()) {
175         return emptyArray()
176     }
177 
178     var time = initialTime
179 
180     val pointerProperties = mutableListOf<PointerProperties>()
181     val pointerCoords = mutableListOf<PointerCoords>()
182 
183     // Convert simplified pointers to actual PointerProperties and PointerCoords.
184     for ((index, initialPointer) in initialPointers.withIndex()) {
185         pointerProperties.add(index, PointerProperties(initialPointer.id))
186         pointerCoords.add(index, PointerCoords(initialPointer.x, initialPointer.y))
187     }
188 
189     val upMotionEvents =
190         Array(initialPointers.size) { index ->
191             // Only the last element should be an ACTION_UP
192             val action =
193                 if (index == initialPointers.size - 1) {
194                     MotionEvent.ACTION_UP
195                 } else {
196                     MotionEvent.ACTION_POINTER_UP
197                 }
198 
199             val numberOfPointers = initialPointers.size - index
200 
201             val up =
202                 MotionEvent(
203                     time,
204                     action,
205                     numberOfPointers,
206                     numberOfPointers - 1, // Used with ACTION_POINTER_DOWN/UP
207                     pointerProperties.toTypedArray(),
208                     pointerCoords.toTypedArray(),
209                     rootView
210                 )
211 
212             // Update time for next ACTION_UP/ACTION_POINTER_UP
213             time += timeDelta
214 
215             // The next ACTION_UP/ACTION_POINTER_UP will have one less pointer, so we remove the
216             // last element from both lists.
217             if (pointerProperties.isNotEmpty()) {
218                 pointerProperties.removeAt(pointerProperties.size - 1)
219             }
220 
221             if (pointerCoords.isNotEmpty()) {
222                 pointerCoords.removeAt(pointerCoords.size - 1)
223             }
224             up // return up event
225         }
226     return upMotionEvents
227 }
228 
229 /**
230  * Creates an array of subsequent [MotionEvent.ACTION_MOVE]s to pair with a
231  * [MotionEvent.ACTION_DOWN] and a [MotionEvent.ACTION_UP] to recreate a user input sequence. Note:
232  * We offset pointers/events by time and x only (y stays the same).
233  *
234  * @param initialTime Starting time for the first [MotionEvent.ACTION_MOVE]
235  * @param initialPointers Starting coordinates for all [MotionEvent.ACTION_MOVE] pointers
236  * @param rootView - [View] that the [MotionEvent] is dispatched to.
237  * @param numberOfMoveEvents Number of [MotionEvent.ACTION_MOVE]s to create.
238  * @param enableFlingStyleHistory - Adds a history of [MotionEvent.ACTION_MOVE]s to each
239  *   [MotionEvent.ACTION_MOVE] to mirror a fling event (where you will get more
240  *   [MotionEvent.ACTION_MOVE]s than the refresh rate of the phone).
241  * @param timeDelta - Time between each [MotionEvent.ACTION_MOVE] in milliseconds.
242  * @param moveDelta - Amount to move in pixels for each [MotionEvent.ACTION_MOVE]
243  */
createMoveMotionEventsnull244 internal fun createMoveMotionEvents(
245     initialTime: Int,
246     initialPointers: Array<BenchmarkSimplifiedPointerInputPointer>,
247     rootView: View,
248     numberOfMoveEvents: Int,
249     enableFlingStyleHistory: Boolean = false,
250     timeDelta: Int = DefaultPointerInputTimeDelta,
251     moveDelta: Float = DefaultPointerInputMoveAmountPx
252 ): Array<MotionEvent> {
253 
254     var time = initialTime
255 
256     // Creates a list of pointer properties and coordinates from initialPointers to represent
257     // all pointers/fingers in the [MotionEvent] we create.
258     val pointerProperties = mutableListOf<PointerProperties>()
259     val pointerCoords = mutableListOf<PointerCoords>()
260 
261     for ((index, initialPointer) in initialPointers.withIndex()) {
262         pointerProperties.add(index, PointerProperties(initialPointer.id))
263         pointerCoords.add(index, PointerCoords(initialPointer.x, initialPointer.y))
264     }
265 
266     val moveMotionEvents =
267         Array(numberOfMoveEvents) { index ->
268             val move =
269                 if (enableFlingStyleHistory) {
270                     val historicalEventCount = numberOfHistoricalEventsBasedOnArrayLocation(index)
271 
272                     // Set the time to the previous event time (either a down or move event) and
273                     // offset it, so it doesn't conflict with that previous event.
274                     var historicalTime: Int = time - timeDelta + 10
275 
276                     var accountForMoveOffset = -1
277 
278                     // Creates starting x values for all historical pointers (takes ending x of
279                     // all pointers and subtracts a delta and offset).
280                     val historicalPointerCoords = mutableListOf<PointerCoords>()
281 
282                     for ((historicalIndex, historicalPointer) in pointerCoords.withIndex()) {
283                         if (moveDelta > 0) {
284                             // accountForMoveOffset stays -1 (to account for +1 offset [below])
285                             historicalPointerCoords.add(
286                                 historicalIndex,
287                                 PointerCoords(
288                                     historicalPointer.x - moveDelta + 1,
289                                     historicalPointer.y
290                                 )
291                             )
292                         } else {
293                             // accountForMoveOffset changes to 1 (to account for -1 offset [below])
294                             accountForMoveOffset = 1
295                             historicalPointerCoords.add(
296                                 historicalIndex,
297                                 PointerCoords(
298                                     historicalPointer.x - moveDelta - 1,
299                                     historicalPointer.y
300                                 )
301                             )
302                         }
303                     }
304 
305                     val historicalTimeDelta: Int = (timeDelta - 10) / historicalEventCount
306                     val historicalXDelta: Float =
307                         (moveDelta + accountForMoveOffset) / historicalEventCount
308 
309                     // Next section of code creates "historical" events by
310                     // 1. Creating [MotionEvent] with oldest historical event.
311                     // 2. Adding each subsequent historical event one at a time via `addBatch()`.
312                     // 3. Finishes by adding the main/end pointers via `addBatch()`.
313 
314                     // Executes step 1 -> Creates [MotionEvent] with oldest historical event.
315                     val moveWithHistory =
316                         MotionEvent(
317                             historicalTime,
318                             MotionEvent.ACTION_MOVE,
319                             pointerProperties.size,
320                             0,
321                             pointerProperties.toTypedArray(), // ids always the same
322                             historicalPointerCoords.toTypedArray(),
323                             rootView
324                         )
325 
326                     // Executes step 2 -> Adds each subsequent historical event one at a time via
327                     // `addBatch()`.
328                     // Starts with the second historical event (1), since we've already added the
329                     // first when we created the [MotionEvent] above.
330                     for (historyIndex in 1 until historicalEventCount) {
331                         // Update historical time
332                         historicalTime += historicalTimeDelta
333 
334                         // Update historical x
335                         for (historicalPointerCoord in historicalPointerCoords) {
336                             historicalPointerCoord.x += historicalXDelta
337                         }
338 
339                         // Add to [MotionEvent] history
340                         moveWithHistory.addBatch(
341                             historicalTime.toLong(),
342                             historicalPointerCoords.toTypedArray(),
343                             0
344                         )
345                     }
346 
347                     // Executes step 3 -> Finishes by adding the main/end pointers via `addBatch()`,
348                     // so it will show up as the current event for the [MotionEvent].
349                     moveWithHistory.addBatch(time.toLong(), pointerCoords.toTypedArray(), 0)
350                     // return move event with history added
351                     moveWithHistory
352                 } else {
353                     MotionEvent(
354                         time,
355                         MotionEvent.ACTION_MOVE,
356                         pointerProperties.size,
357                         0,
358                         pointerProperties.toTypedArray(),
359                         pointerCoords.toTypedArray(),
360                         rootView
361                     )
362                 }
363 
364             time += timeDelta
365             // Update all pointer's x by move delta for next iteration
366             for (pointerCoord in pointerCoords) {
367                 pointerCoord.x += moveDelta
368             }
369             move
370         }
371     return moveMotionEvents
372 }
373 
374 /*
375  * Based on traces of fling events, the first events in a series of "MOVES" have more
376  * historical [MotionEvent]s than the subsequent events.
377  *
378  * Remember, historical events within a [MotionEvent] represent extra [MotionEvent]s
379  * that occurred faster than the refresh rate of the phone. A fling will have many more events
380  * in the beginning (and between the refresh rate since they are happening so quick) than in
381  * the end.
382  */
numberOfHistoricalEventsBasedOnArrayLocationnull383 internal fun numberOfHistoricalEventsBasedOnArrayLocation(index: Int): Int {
384     return when (index) {
385         0 -> 12
386         1 -> 9
387         2,
388         3 -> 4
389         else -> 2
390     }
391 }
392