1 /*
2 * 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
17 package com.android.cts.input
18
19 import android.app.Instrumentation
20 import android.graphics.Matrix
21 import android.graphics.Point
22 import android.server.wm.CtsWindowInfoUtils
23 import android.view.Display
24 import android.view.Surface
25 import android.view.View
26 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_POSITION_X
27 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_POSITION_Y
28 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_PRESSURE
29 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_SLOT
30 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_TOOL_TYPE
31 import com.android.cts.input.EvdevInputEventCodes.Companion.ABS_MT_TRACKING_ID
32 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOOL_DOUBLETAP
33 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOOL_FINGER
34 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOOL_QUADTAP
35 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOOL_QUINTTAP
36 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOOL_TRIPLETAP
37 import com.android.cts.input.EvdevInputEventCodes.Companion.BTN_TOUCH
38 import com.android.cts.input.EvdevInputEventCodes.Companion.EV_ABS
39 import com.android.cts.input.EvdevInputEventCodes.Companion.EV_KEY
40 import com.android.cts.input.EvdevInputEventCodes.Companion.EV_SYN
41 import com.android.cts.input.EvdevInputEventCodes.Companion.INVALID_MT_TRACKING_ID
42 import com.android.cts.input.EvdevInputEventCodes.Companion.MT_TOOL_PALM
43 import com.android.cts.input.EvdevInputEventCodes.Companion.SYN_REPORT
44 import kotlin.math.round
45
transformFromScreenToTouchDeviceSpacenull46 private fun transformFromScreenToTouchDeviceSpace(x: Int, y: Int, display: Display): Point {
47 val displayInfos = CtsWindowInfoUtils.getWindowAndDisplayState().second
48
49 var displayTransform: Matrix? = null
50 for (displayInfo in displayInfos) {
51 if (displayInfo.displayId == display.displayId) {
52 displayTransform = displayInfo.transform
53 }
54 }
55
56 if (displayTransform == null) {
57 throw IllegalStateException(
58 "failed to find display transform for display ${display.displayId}"
59 )
60 }
61
62 // The display transform is the transform from physical display space to
63 // logical display space. We need to go from logical display space to
64 // physical display space so we take the inverse transform.
65 val inverseTransform = Matrix()
66 displayTransform.invert(inverseTransform)
67
68 val p = floatArrayOf(x.toFloat(), y.toFloat())
69 inverseTransform.mapPoints(p)
70
71 val point = Point(round(p[0]).toInt(), round(p[1]).toInt())
72
73 // We need to apply offset to correctly map the point to discrete coordinate space, this is done
74 // to account for the disparity between continuous and discrete coordinates during rotation
75 // Refer to frameworks/native/services/inputflinger/docs/input_coordinates.md
76 return when (display.rotation) {
77 Surface.ROTATION_0 -> point
78 Surface.ROTATION_90 -> point.apply { offset(-1, 0) }
79 Surface.ROTATION_180 -> point.apply { offset(-1, -1) }
80 Surface.ROTATION_270 -> point.apply { offset(0, -1) }
81 else -> throw IllegalStateException("unexpected display rotation ${display.rotation}")
82 }
83 }
84
85 /**
86 * Helper class for configuring and interacting with a [UinputDevice] that uses the evdev
87 * multitouch protocol.
88 */
89 open class UinputTouchDevice(
90 instrumentation: Instrumentation,
91 private val display: Display,
92 private val registerCommand: UinputRegisterCommand,
93 source: Int,
94 private val defaultToolType: Int,
95 ) : AutoCloseable {
96
97 val uinputDevice = UinputDevice(instrumentation, source, registerCommand, display)
98
injectEventnull99 private fun injectEvent(events: IntArray) {
100 uinputDevice.injectEvents(events.joinToString(
101 prefix = "[",
102 postfix = "]",
103 separator = ",",
104 ))
105 }
106
sendBtnTouchnull107 fun sendBtnTouch(isDown: Boolean) {
108 injectEvent(intArrayOf(EV_KEY, BTN_TOUCH, if (isDown) 1 else 0))
109 }
110
sendBtnnull111 fun sendBtn(btnCode: Int, isDown: Boolean) {
112 injectEvent(intArrayOf(EV_KEY, btnCode, if (isDown) 1 else 0))
113 }
114
115 /**
116 * Send events signifying a new pointer is being tracked.
117 *
118 * Note: The [physicalLocation] parameter is specified in the touch device's
119 * raw coordinate space, and does not factor display rotation or scaling. Use
120 * [touchDown] to start tracking a pointer in screen (a.k.a. logical display)
121 * coordinate space.
122 */
sendDownnull123 fun sendDown(id: Int, physicalLocation: Point) {
124 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
125 injectEvent(intArrayOf(EV_ABS, ABS_MT_TRACKING_ID, id))
126 injectEvent(intArrayOf(EV_ABS, ABS_MT_TOOL_TYPE, defaultToolType))
127 injectEvent(intArrayOf(EV_ABS, ABS_MT_POSITION_X, physicalLocation.x))
128 injectEvent(intArrayOf(EV_ABS, ABS_MT_POSITION_Y, physicalLocation.y))
129 }
130
131 /**
132 * Send events signifying a tracked pointer is being moved.
133 *
134 * Note: The [physicalLocation] parameter is specified in the touch device's
135 * raw coordinate space, and does not factor display rotation or scaling.
136 */
sendMovenull137 fun sendMove(id: Int, physicalLocation: Point) {
138 // Use same events of down.
139 sendDown(id, physicalLocation)
140 }
141
sendUpnull142 fun sendUp(id: Int) {
143 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
144 injectEvent(intArrayOf(EV_ABS, ABS_MT_TRACKING_ID, INVALID_MT_TRACKING_ID))
145 }
146
sendToolTypenull147 fun sendToolType(id: Int, toolType: Int) {
148 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
149 injectEvent(intArrayOf(EV_ABS, ABS_MT_TOOL_TYPE, toolType))
150 }
151
sendPressurenull152 fun sendPressure(pressure: Int) {
153 injectEvent(intArrayOf(EV_ABS, ABS_MT_PRESSURE, pressure))
154 }
155
syncnull156 fun sync() {
157 injectEvent(intArrayOf(EV_SYN, SYN_REPORT, 0))
158 }
159
delaynull160 fun delay(delayMs: Int) {
161 uinputDevice.injectDelay(delayMs)
162 }
163
getDeviceIdnull164 fun getDeviceId(): Int {
165 return uinputDevice.deviceId
166 }
167
closenull168 override fun close() {
169 uinputDevice.close()
170 }
171
tapOnViewCenternull172 fun tapOnViewCenter(view: View) {
173 val xy = IntArray(2)
174 view.getLocationOnScreen(xy)
175 val x = xy[0] + view.width / 2
176 val y = xy[1] + view.height / 2
177 val pointer = touchDown(x, y)
178 pointer.lift()
179 }
180
181 private val pointerIds = mutableSetOf<Int>()
182
183 /**
184 * Send a new pointer to the screen, generating an ACTION_DOWN if there aren't any other
185 * pointers currently down, or an ACTION_POINTER_DOWN otherwise.
186 * @param x The x coordinate in screen (logical display) space.
187 * @param y The y coordinate in screen (logical display) space.
188 * @param pressure The pressure value to be used, default not sending pressure.
189 */
190 @JvmOverloads
touchDownnull191 fun touchDown(x: Int, y: Int, pressure: Int? = null): Pointer {
192 val pointerId = firstUnusedPointerId()
193 pointerIds.add(pointerId)
194 return Pointer(pointerId, pressure, x, y)
195 }
196
firstUnusedPointerIdnull197 private fun firstUnusedPointerId(): Int {
198 var id = 0
199 while (pointerIds.contains(id)) {
200 id++
201 }
202 return id
203 }
204
removePointernull205 private fun removePointer(id: Int) {
206 pointerIds.remove(id)
207 }
208
209 private val pointerCount get() = pointerIds.size
210
211 /**
212 * A single pointer interacting with the screen. This class simplifies the interactions by
213 * removing the need to separately manage the pointer id.
214 * Works in the screen (logical display) coordinate space.
215 */
216 inner class Pointer(
217 private val id: Int,
218 private val pressure: Int?,
219 x: Int,
220 y: Int,
221 ) : AutoCloseable {
222 private var active = true
223
224 init {
225 // Send ACTION_DOWN or ACTION_POINTER_DOWN
226 sendBtnTouch(true)
227 sendDown(id, transformFromScreenToTouchDeviceSpace(x, y, display))
<lambda>null228 pressure?.let { sendPressure(pressure) }
229 sync()
230 }
231
232 /**
233 * Send ACTION_MOVE
234 * The coordinates provided here should be relative to the screen edge, rather than the
235 * window corner. That is, the location should be in the same coordinate space as that
236 * returned by View::getLocationOnScreen API rather than View::getLocationInWindow.
237 */
moveTonull238 fun moveTo(x: Int, y: Int) {
239 if (!active) {
240 throw IllegalStateException("Pointer $id is not active, can't move to ($x, $y)")
241 }
242 sendMove(id, transformFromScreenToTouchDeviceSpace(x, y, display))
243 sync()
244 }
245
liftnull246 fun lift() {
247 if (!active) {
248 throw IllegalStateException("Pointer $id is not active, already lifted?")
249 }
250 if (pointerCount == 1) {
251 sendBtnTouch(false)
252 }
253 sendUp(id)
254 pressure?.let { sendPressure(0) }
255 sync()
256 active = false
257 removePointer(id)
258 }
259
260 /**
261 * Send a cancel if this pointer hasn't yet been lifted
262 */
closenull263 override fun close() {
264 if (!active) {
265 return
266 }
267 sendToolType(id, MT_TOOL_PALM)
268 sync()
269 lift()
270 }
271 }
272
273 companion object {
274 /**
275 * The allowed error when making assertions on touch coordinates.
276 *
277 * Coordinates are transformed from logical display space to physical display space and
278 * then rounded to the nearest integer, introducing error. The epsilon value effectively
279 * sets the maximum allowed scaling factor for a display. This value allows a maximum scale
280 * factor of 2.
281 */
282 const val TOUCH_COORDINATE_EPSILON = 1.001f
283
toolBtnForFingerCountnull284 fun toolBtnForFingerCount(numFingers: Int): Int {
285 return when (numFingers) {
286 1 -> BTN_TOOL_FINGER
287 2 -> BTN_TOOL_DOUBLETAP
288 3 -> BTN_TOOL_TRIPLETAP
289 4 -> BTN_TOOL_QUADTAP
290 5 -> BTN_TOOL_QUINTTAP
291 else -> throw IllegalArgumentException("Number of fingers must be between 1 and 5")
292 }
293 }
294 }
295 }
296