• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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