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