-
Notifications
You must be signed in to change notification settings - Fork 106
/
input_android.lua
305 lines (281 loc) · 14.3 KB
/
input_android.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
local ffi = require("ffi")
local bit = require("bit")
local C = ffi.C
local android = require("android")
require("ffi/posix_h")
require("ffi/linux_input_h")
local input = {
-- to trigger refreshes for certain Android framework events:
device = nil,
-- NOPs
open = function() end,
close = function() end,
closeAll = function() end,
fakeTapInput = function() end,
-- Tell front that we're a custom imp with no concept of paths/fd
is_ffi = true,
}
local inputQueue = {}
local function genInputTimeval(ts)
local timev = { sec = 0, usec = 0 }
if ts then
-- If we've got one, trust the native event's timestamp, they're guaranteed to be in the CLOCK_MONOTONIC timebase.
-- ns to µs
-- NOTE: Unlike the Java APIs, this is in nanoseconds!
-- c.f., https://developer.android.com/ndk/reference/group/input#amotionevent_geteventtime
local us = ts / 1000
-- TimeVal, how I miss thee...
timev.sec = math.floor(tonumber(us / 1000000))
timev.usec = tonumber(us % 1000000)
else
-- Otherwise, synthetize one in the same time scale.
-- TimeVal probably ought to be in base...
local timespec = ffi.new("struct timespec")
C.clock_gettime(C.CLOCK_MONOTONIC, timespec)
timev.sec = tonumber(timespec.tv_sec)
-- ns to µs
timev.usec = math.floor(tonumber(timespec.tv_nsec / 1000))
end
return timev
end
local function genEmuEvent(evtype, code, value, timev, ts)
local ev = {
type = tonumber(evtype),
code = tonumber(code),
value = tonumber(value),
time = timev or genInputTimeval(ts),
}
table.insert(inputQueue, ev)
end
-- Keep track of all the active pointers in the current gesture.
-- *hash*, key is a pointer *id* (i.e., its slot number),
-- value is a boolean, denoting whether the pointer is currently down (e.g., in contact) (true), up (false) or inactive (nil).
local pointers = {}
local function setPointerDown(slot, down)
if pointers[slot] ~= down then
pointers[slot] = down
end
end
local function genTouchDownEvent(event, slot, index)
local x = android.lib.AMotionEvent_getX(event, index)
local y = android.lib.AMotionEvent_getY(event, index)
local timev = genInputTimeval(android.lib.AMotionEvent_getEventTime(event))
genEmuEvent(C.EV_ABS, C.ABS_MT_SLOT, slot, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_TRACKING_ID, slot, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_X, x, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_Y, y, timev)
genEmuEvent(C.EV_SYN, C.SYN_REPORT, 0, timev)
end
local function genTouchUpEvent(event, slot, index)
local x = android.lib.AMotionEvent_getX(event, index)
local y = android.lib.AMotionEvent_getY(event, index)
local timev = genInputTimeval(android.lib.AMotionEvent_getEventTime(event))
genEmuEvent(C.EV_ABS, C.ABS_MT_SLOT, slot, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_TRACKING_ID, -1, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_X, x, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_Y, y, timev)
genEmuEvent(C.EV_SYN, C.SYN_REPORT, 0, timev)
end
local function genTouchMoveEvent(event, timev, slot, index)
-- NOTE: May return a float for events w/ subpixel precision.
local x = android.lib.AMotionEvent_getX(event, index)
local y = android.lib.AMotionEvent_getY(event, index)
genEmuEvent(C.EV_ABS, C.ABS_MT_SLOT, slot, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_X, x, timev)
genEmuEvent(C.EV_ABS, C.ABS_MT_POSITION_Y, y, timev)
end
local function genEndTouchEvent(event, timev)
genEmuEvent(C.EV_SYN, C.SYN_REPORT, 0, timev)
end
local function genAndroidCancelEvent(event)
local timev = genInputTimeval(android.lib.AMotionEvent_getEventTime(event))
-- We piggyback on this EV_MSC code, because our EV_MSC handler already handles it via Device.input:resetState(),
-- which is *exactly* what we want to happen ;).
genEmuEvent(C.EV_MSC, C.APP_CMD_LOST_FOCUS, 42, timev)
end
-- NOTE: As far as AMotionEvent_getPointerId is concerned, keep in mind that while the id (e.g., slot) that it returns
-- is constant across a pointer's lifetime (i.e., a single gesture), its index *can* and *will* vary across events!
-- (As it's literally an index to the native MotionEvent array where the data is stored).
-- NOTE: The documentation is not always super clear on the fact that the data is always at index 0 for
-- AMOTION_EVENT_ACTION_DOWN & AMOTION_EVENT_ACTION_UP, so, use the source, Luke! c.f., TouchInputMapper::dispatchMotion @
-- https://android.googlesource.com/platform//frameworks/native/+/master/services/inputflinger/reader/mapper/TouchInputMapper.cpp
local function motionEventHandler(motion_event)
if android.isTouchscreenIgnored() then
return
end
local action = android.lib.AMotionEvent_getAction(motion_event)
local flags = bit.band(action, C.AMOTION_EVENT_ACTION_MASK)
if flags == C.AMOTION_EVENT_ACTION_DOWN then
-- Happens on the *first* contact of a gesture (data is always at index 0),
local slot = android.lib.AMotionEvent_getPointerId(motion_event, 0)
setPointerDown(slot, true)
genTouchDownEvent(motion_event, slot, 0)
elseif flags == C.AMOTION_EVENT_ACTION_POINTER_DOWN then
local pointer_index = bit.rshift(
bit.band(action, C.AMOTION_EVENT_ACTION_POINTER_INDEX_MASK),
C.AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT)
local slot = android.lib.AMotionEvent_getPointerId(motion_event, pointer_index)
setPointerDown(slot, true)
genTouchDownEvent(motion_event, slot, pointer_index)
elseif flags == C.AMOTION_EVENT_ACTION_UP then
-- Happens once the *last* contact of a gesture has been lifted (data is always at index 0)
local slot = android.lib.AMotionEvent_getPointerId(motion_event, 0)
setPointerDown(slot, false)
genTouchUpEvent(motion_event, slot, 0)
elseif flags == C.AMOTION_EVENT_ACTION_POINTER_UP then
local pointer_index = bit.rshift(
bit.band(action, C.AMOTION_EVENT_ACTION_POINTER_INDEX_MASK),
C.AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT)
local slot = android.lib.AMotionEvent_getPointerId(motion_event, pointer_index)
setPointerDown(slot, false)
genTouchUpEvent(motion_event, slot, pointer_index)
elseif flags == C.AMOTION_EVENT_ACTION_MOVE then
-- There may be multiple pointers involved, only request the ts once
local timev = genInputTimeval(android.lib.AMotionEvent_getEventTime(motion_event))
-- This effectively gives us the size of the current MotionEvent array...
local pointer_count = tonumber(android.lib.AMotionEvent_getPointerCount(motion_event))
for i = 0, pointer_count - 1 do
-- So, loop through the array, and if that pointer is still down, move it
local slot = android.lib.AMotionEvent_getPointerId(motion_event, i)
if pointers[slot] then
genTouchMoveEvent(motion_event, timev, slot, i)
end
end
-- Bundle everything in a single input frame
genEndTouchEvent(motion_event, timev)
elseif flags == C.AMOTION_EVENT_ACTION_CANCEL then
-- Invalidate the pointers, and push a custom event to notify front to do the same.
-- NOTE: We preserve the actual table object to avoid garbage churn,
-- as we're liable to see a similar amount of slots show up again later.
for slot, _ in pairs(pointers) do
pointers[slot] = nil
end
genAndroidCancelEvent(motion_event)
end
end
local function keyEventHandler(key_event)
local code = android.lib.AKeyEvent_getKeyCode(key_event)
local action = android.lib.AKeyEvent_getAction(key_event)
if code == C.AKEYCODE_VOLUME_UP
or code == C.AKEYCODE_VOLUME_DOWN then
if android.getVolumeKeysIgnored() then
return 0 -- event not consumed
end
elseif code == C.AKEYCODE_MEDIA_PLAY_PAUSE
or code == C.AKEYCODE_MEDIA_PLAY
or code == C.AKEYCODE_MEDIA_PAUSE
or code == C.AKEYCODE_MEDIA_STOP
or code == C.AKEYCODE_MEDIA_NEXT
or code == C.AKEYCODE_MEDIA_PREVIOUS
or code == C.AKEYCODE_MEDIA_REWIND
or code == C.AKEYCODE_MEDIA_FAST_FORWARD
or code == C.AKEYCODE_HEADSETHOOK then
return 0 -- event not consumed
elseif code == C.AKEYCODE_MUTE
or code == C.AKEYCODE_VOLUME_MUTE then
if android.getVolumeKeysIgnored() then
return 0
else
return 1
end
end
if code == C.AKEYCODE_BACK and android.isBackButtonIgnored() then
return 1
end
if action == C.AKEY_EVENT_ACTION_DOWN then
genEmuEvent(C.EV_KEY, code, 1, nil, android.lib.AKeyEvent_getEventTime(key_event))
elseif action == C.AKEY_EVENT_ACTION_UP then
genEmuEvent(C.EV_KEY, code, 0, nil, android.lib.AKeyEvent_getEventTime(key_event))
end
return 1 -- event consumed
end
local function commandHandler(code, value)
genEmuEvent(C.EV_MSC, code, value)
end
function input.waitForEvent(sec, usec)
-- TimeVal's :tomsecs if we were passed one to begin with, otherwise, -1 => block
local timeout = sec and math.floor(sec * 1000000 + usec + 0.5) / 1000 or -1
-- Reset the queue
inputQueue = {}
-- Will point to the the raw fd number
local fd = ffi.new("int[1]")
-- Will point to the poll events
local events = ffi.new("int[1]")
-- Will point to the data passed at addFd/attachLooper time, c.f., the android_poll_source struct definition.
-- NOTE: Its id field is mostly redundant, as ALooper already returns the ident.
-- And its process function can be used as a weird delayed callback mechanism, but ALooper already has native callback handling :?.
-- TL;DR: We don't actually use it here.
local source = ffi.new("struct android_poll_source*[1]")
-- NOTE: Much like the C backend, we only process a *single* fd per waitForEvent iteration.
-- Although, in the Android case, things are a bit more complex, because this is a wrapper around epoll:
-- the backend actually accumulates every poll event in a single (inner) iteration, and enqueues those in a list.
-- That list is what the public function processes, and it processes it item by item, returning one item per call.
-- c.f., https://android.googlesource.com/platform/system/core/+/refs/heads/master/libutils/Looper.cpp
-- NOTE: We don't use callbacks, so pollOnce is good enough for us, no need to resort to pollAll :).
local poll_state = android.lib.ALooper_pollOnce(timeout, fd, events, ffi.cast("void**", source))
if poll_state >= 0 then
-- NOTE: Since we actually want to process this in Lua-land (i.e., here), and not in C-land,
-- we do *NOT* make use of the weird delayed callback mechanism afforded by the android_poll_source struct
-- we pass as the data pointer to ALooper in the glue code when registering a polling source.
-- Instead, we do everything here, which is why this may look eerily like the C functions
-- process_cmd & process_input in the glue code.
-- Sidebar: if you *actually* need to process stuff in C-land ASAP, use ALooper's native callback system.
if poll_state == C.LOOPER_ID_MAIN then
-- e.g., source[0].process(android.app, source[0]) where process would point to process_cmd
local cmd = android.glue.android_app_read_cmd(android.app)
while cmd ~= -1 do
android.glue.android_app_pre_exec_cmd(android.app, cmd)
commandHandler(cmd, 1)
android.glue.android_app_post_exec_cmd(android.app, cmd)
-- Should return -1 (EAGAIN) when we've drained the pipe
cmd = android.glue.android_app_read_cmd(android.app)
end
elseif poll_state == C.LOOPER_ID_INPUT then
-- e.g., source[0].process(android.app, source[0]) where process would point to process_input
local event = ffi.new("AInputEvent*[1]")
while android.lib.AInputQueue_getEvent(android.app.inputQueue, event) >= 0 do
if android.lib.AInputQueue_preDispatchEvent(android.app.inputQueue, event[0]) == 0 then
local event_type = android.lib.AInputEvent_getType(event[0])
local handled = 1
if event_type == C.AINPUT_EVENT_TYPE_MOTION then
motionEventHandler(event[0])
elseif event_type == C.AINPUT_EVENT_TYPE_KEY then
handled = keyEventHandler(event[0])
end
android.lib.AInputQueue_finishEvent(android.app.inputQueue, event[0], handled)
end
end
elseif poll_state == C.LOOPER_ID_USER then
local message = ffi.new("unsigned char [4]")
-- Similarly, read will return -1 (EAGAIN) when we've drained the pipe
while C.read(fd[0], message, 4) == 4 do
if message[0] == C.AEVENT_POWER_CONNECTED then
commandHandler(C.AEVENT_POWER_CONNECTED, 0)
elseif message[0] == C.AEVENT_POWER_DISCONNECTED then
commandHandler(C.AEVENT_POWER_DISCONNECTED, 0)
elseif message[0] == C.AEVENT_DOWNLOAD_COMPLETE then
commandHandler(C.AEVENT_DOWNLOAD_COMPLETE, 0)
end
end
end
if android.app.destroyRequested ~= 0 then
android.LOGI("Engine thread destroy requested!")
-- Do nothing, if this is set, we've already pushed an APP_CMD_DESTROY event that'll get handled in front.
end
elseif poll_state == C.ALOOPER_POLL_TIMEOUT then
return false, C.ETIME
elseif poll_state == C.ALOOPER_POLL_ERROR then
android.LOGE("Encountered a polling error!")
return
end
-- NOTE: We never set callbacks, and we never call wake, so no need to check for ALOOPER_POLL_CALLBACK & ALOOPER_POLL_WAKE
if #inputQueue > 0 then
-- We generated some actionable events
return true, inputQueue
else
-- poll returned early, but without an event we actually use.
-- Back to Input:waitEvent to recompute the timeout
return false, C.EINTR
end
end
return input