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.Point
21 import android.hardware.input.InputManager
22 import android.os.Handler
23 import android.os.Looper
24 import android.server.wm.WindowManagerStateHelper
25 import android.view.Display
26 import android.view.Surface
27 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
28 import com.android.compatibility.common.util.TestUtils.waitOn
29 import java.util.concurrent.TimeUnit
30
rotateFromScreenToTouchDeviceSpacenull31 private fun rotateFromScreenToTouchDeviceSpace(x: Int, y: Int, display: Display): Point {
32 return when (display.rotation) {
33 Surface.ROTATION_0 -> Point(x, y)
34 Surface.ROTATION_90 -> Point(display.mode.physicalWidth - 1 - y, x)
35 Surface.ROTATION_180 -> Point(
36 display.mode.physicalWidth - 1 - x,
37 display.mode.physicalHeight - 1 - y
38 )
39 Surface.ROTATION_270 -> Point(y, display.mode.physicalHeight - 1 - x)
40 else -> throw IllegalStateException("unexpected display rotation ${display.rotation}")
41 }
42 }
43
44 /**
45 * Helper class for configuring and interacting with a [UinputDevice] that uses the evdev
46 * multitouch protocol.
47 */
48 open class UinputTouchDevice(
49 instrumentation: Instrumentation,
50 private val display: Display,
51 private val registerCommand: UinputRegisterCommand,
52 source: Int,
53 ) : AutoCloseable {
54
55 private val DISPLAY_ASSOCIATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5)
56 private val uinputDevice = UinputDevice(instrumentation, source, registerCommand)
57 private val inputManager: InputManager
58
59 init {
60 inputManager = instrumentation.targetContext.getSystemService(InputManager::class.java)!!
61 associateWith(display)
62
63 // Wait for display transitions to idle as associating an input device with a display could
64 // trigger one because of a display configuration change
65 WindowManagerStateHelper().waitForAppTransitionIdleOnDisplay(display.displayId)
66 instrumentation.uiAutomation.syncInputTransactions()
67 }
68
injectEventnull69 private fun injectEvent(events: IntArray) {
70 uinputDevice.injectEvents(events.joinToString(
71 prefix = "[",
72 postfix = "]",
73 separator = ",",
74 ))
75 }
76
sendBtnTouchnull77 fun sendBtnTouch(isDown: Boolean) {
78 injectEvent(intArrayOf(EV_KEY, BTN_TOUCH, if (isDown) 1 else 0))
79 }
80
sendBtnnull81 fun sendBtn(btnCode: Int, isDown: Boolean) {
82 injectEvent(intArrayOf(EV_KEY, btnCode, if (isDown) 1 else 0))
83 }
84
sendDownnull85 fun sendDown(id: Int, location: Point, toolType: Int? = null) {
86 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
87 injectEvent(intArrayOf(EV_ABS, ABS_MT_TRACKING_ID, id))
88 if (toolType != null) injectEvent(intArrayOf(EV_ABS, ABS_MT_TOOL_TYPE, toolType))
89 injectEvent(intArrayOf(EV_ABS, ABS_MT_POSITION_X, location.x))
90 injectEvent(intArrayOf(EV_ABS, ABS_MT_POSITION_Y, location.y))
91 }
92
sendMovenull93 fun sendMove(id: Int, location: Point) {
94 // Use same events of down.
95 sendDown(id, location)
96 }
97
sendUpnull98 fun sendUp(id: Int) {
99 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
100 injectEvent(intArrayOf(EV_ABS, ABS_MT_TRACKING_ID, INVALID_TRACKING_ID))
101 }
102
sendToolTypenull103 fun sendToolType(id: Int, toolType: Int) {
104 injectEvent(intArrayOf(EV_ABS, ABS_MT_SLOT, id))
105 injectEvent(intArrayOf(EV_ABS, ABS_MT_TOOL_TYPE, toolType))
106 }
107
sendPressurenull108 fun sendPressure(pressure: Int) {
109 injectEvent(intArrayOf(EV_ABS, ABS_MT_PRESSURE, pressure))
110 }
111
syncnull112 fun sync() {
113 injectEvent(intArrayOf(EV_SYN, SYN_REPORT, 0))
114 }
115
delaynull116 fun delay(delayMs: Int) {
117 uinputDevice.injectDelay(delayMs)
118 }
119
getDeviceIdnull120 fun getDeviceId(): Int {
121 return uinputDevice.deviceId
122 }
123
associateWithnull124 private fun associateWith(display: Display) {
125 runWithShellPermissionIdentity(
126 { inputManager.addUniqueIdAssociationByPort(
127 registerCommand.port,
128 display.uniqueId!!
129 )
130 },
131 "android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY"
132 )
133 waitForDeviceUpdatesUntil {
134 val inputDevice = inputManager.getInputDevice(uinputDevice.deviceId)
135 display.displayId == inputDevice!!.associatedDisplayId
136 }
137 }
138
waitForDeviceUpdatesUntilnull139 private fun waitForDeviceUpdatesUntil(condition: () -> Boolean) {
140 val lockForInputDeviceUpdates = Object()
141 val inputDeviceListener =
142 object : InputManager.InputDeviceListener {
143 override fun onInputDeviceAdded(deviceId: Int) {
144 synchronized(lockForInputDeviceUpdates) {
145 lockForInputDeviceUpdates.notify()
146 }
147 }
148
149 override fun onInputDeviceRemoved(deviceId: Int) {
150 synchronized(lockForInputDeviceUpdates) {
151 lockForInputDeviceUpdates.notify()
152 }
153 }
154
155 override fun onInputDeviceChanged(deviceId: Int) {
156 synchronized(lockForInputDeviceUpdates) {
157 lockForInputDeviceUpdates.notify()
158 }
159 }
160 }
161
162 inputManager.registerInputDeviceListener(
163 inputDeviceListener,
164 Handler(Looper.getMainLooper())
165 )
166
167 waitOn(
168 lockForInputDeviceUpdates,
169 condition,
170 DISPLAY_ASSOCIATION_TIMEOUT_MILLIS,
171 null
172 )
173
174 inputManager.unregisterInputDeviceListener(inputDeviceListener)
175 }
176
closenull177 override fun close() {
178 runWithShellPermissionIdentity(
179 { inputManager.removeUniqueIdAssociationByPort(registerCommand.port) },
180 "android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY"
181 )
182 uinputDevice.close()
183 }
184
185 private val pointerIds = mutableSetOf<Int>()
186
187 /**
188 * Send a new pointer to the screen, generating an ACTION_DOWN if there aren't any other
189 * pointers currently down, or an ACTION_POINTER_DOWN otherwise.
190 */
touchDownnull191 fun touchDown(x: Int, y: Int): Pointer {
192 val pointerId = firstUnusedPointerId()
193 pointerIds.add(pointerId)
194 return Pointer(pointerId, 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 coordinate space.
215 */
216 inner class Pointer(
217 private val id: Int,
218 x: Int,
219 y: Int,
220 ) : AutoCloseable {
221 private var active = true
222 init {
223 // Send ACTION_DOWN or ACTION_POINTER_DOWN
224 sendBtnTouch(true)
225 sendDown(id, rotateFromScreenToTouchDeviceSpace(x, y, display), MT_TOOL_FINGER)
226 sync()
227 }
228
229 /**
230 * Send ACTION_MOVE
231 * The coordinates provided here should be relative to the screen edge, rather than the
232 * window corner. That is, the location should be in the same coordinate space as that
233 * returned by View::getLocationOnScreen API rather than View::getLocationInWindow.
234 */
moveTonull235 fun moveTo(x: Int, y: Int) {
236 if (!active) {
237 throw IllegalStateException("Pointer $id is not active, can't move to ($x, $y)")
238 }
239 sendMove(id, rotateFromScreenToTouchDeviceSpace(x, y, display))
240 sync()
241 }
242
liftnull243 fun lift() {
244 if (!active) {
245 throw IllegalStateException("Pointer $id is not active, already lifted?")
246 }
247 if (pointerCount == 1) {
248 sendBtnTouch(false)
249 }
250 sendUp(id)
251 sync()
252 active = false
253 removePointer(id)
254 }
255
256 /**
257 * Send a cancel if this pointer hasn't yet been lifted
258 */
closenull259 override fun close() {
260 if (!active) {
261 return
262 }
263 sendToolType(id, MT_TOOL_PALM)
264 sync()
265 lift()
266 }
267 }
268
269 companion object {
270 const val EV_SYN = 0
271 const val EV_KEY = 1
272 const val EV_ABS = 3
273 const val ABS_MT_SLOT = 0x2f
274 const val ABS_MT_POSITION_X = 0x35
275 const val ABS_MT_POSITION_Y = 0x36
276 const val ABS_MT_TOOL_TYPE = 0x37
277 const val ABS_MT_TRACKING_ID = 0x39
278 const val ABS_MT_PRESSURE = 0x3a
279 const val BTN_TOUCH = 0x14a
280 const val BTN_TOOL_PEN = 0x140
281 const val BTN_TOOL_FINGER = 0x145
282 const val BTN_TOOL_DOUBLETAP = 0x14d
283 const val BTN_TOOL_TRIPLETAP = 0x14e
284 const val BTN_TOOL_QUADTAP = 0x14f
285 const val BTN_TOOL_QUINTTAP = 0x148
286 const val SYN_REPORT = 0
287 const val MT_TOOL_FINGER = 0
288 const val MT_TOOL_PEN = 1
289 const val MT_TOOL_PALM = 2
290 const val INVALID_TRACKING_ID = -1
291
toolBtnForFingerCountnull292 fun toolBtnForFingerCount(numFingers: Int): Int {
293 return when (numFingers) {
294 1 -> BTN_TOOL_FINGER
295 2 -> BTN_TOOL_DOUBLETAP
296 3 -> BTN_TOOL_TRIPLETAP
297 4 -> BTN_TOOL_QUADTAP
298 5 -> BTN_TOOL_QUINTTAP
299 else -> throw IllegalArgumentException("Number of fingers must be between 1 and 5")
300 }
301 }
302 }
303 }
304