-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprotocol.go
More file actions
571 lines (523 loc) · 19.1 KB
/
Copy pathprotocol.go
File metadata and controls
571 lines (523 loc) · 19.1 KB
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
// Copyright (c) the go-widgets/android authors. All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause
// Package android implements the application half of the go-widgets
// Android host protocol, so a go-widgets application runs inside a real
// Android app exactly as it runs on X11, Wayland, Cocoa or Win32.
//
// Android hands out no drawable surface to a process that is not the app: the
// whole graphics API is behind JNI, and JNI needs cgo. So the app is split in
// two. A thin Java host owns the Activity, the SurfaceView and the input
// stream; the go-widgets application is an ordinary CGO-free executable the
// host spawns, which paints into a shared mapping and tells the host which
// rectangle changed. The split is the same one the Linux back-ends already
// live with — a socket protocol plus a shared pixel buffer — with the Java
// host standing where the X server or the Wayland compositor stands.
//
// This file is the SOVEREIGN, transport-agnostic codec: the wire messages, the
// framing, and the input→toolkit.Event mapping, over plain Go values. It
// carries no syscall and no net dependency, so it builds — and is unit-tested
// to 100% — on every GOOS. The transport that dials the host socket, maps the
// buffer and drives a widget tree lives in client.go.
package android
import (
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"github.com/go-widgets/toolkit"
)
// Message types. Host→app messages are below 0x80, app→host at or above it, so
// a misrouted message is a decode error rather than a plausible other message.
const (
// MsgConfig carries the surface geometry and the shared buffer path. The
// host sends it once at start-up and again on every resize or rotation.
MsgConfig uint8 = 0x01
// MsgTouch carries one pointer sample in surface pixels.
MsgTouch uint8 = 0x02
// MsgKey carries one key event: an Android key code plus the unicode rune
// the host's key-character map produced (0 when the key produces none).
MsgKey uint8 = 0x03
// MsgLifecycle carries an Activity transition: the app keeps its widget
// tree across a pause, but stops painting until it resumes.
MsgLifecycle uint8 = 0x04
// MsgClose asks the application to end its Run loop.
MsgClose uint8 = 0x05
// MsgA11yRequest asks the application for its accessibility tree. The host
// sends it only when something is actually reading one, so an app with no
// screen reader attached never builds a tree at all.
MsgA11yRequest uint8 = 0x07
// MsgA11yAction carries the index of the element a screen reader activated.
MsgA11yAction uint8 = 0x08
// MsgText carries text an input method committed, as UTF-8. A soft keyboard
// does not send keystrokes: it commits finished text, sometimes several
// characters at once (a word completion, an emoji, a pasted clipboard).
MsgText uint8 = 0x09
// MsgTextDelete asks to delete a number of characters before the cursor,
// which is how an input method spells backspace.
MsgTextDelete uint8 = 0x0a
// MsgScroll carries a scroll notch from a wheel or a trackpad, in detents.
// A finger on the glass is not a scroll: it is a drag, and arrives as
// MsgTouch. This is the pointing device a Chromebook, a DeX desktop or a
// tablet with a mouse has.
MsgScroll uint8 = 0x0b
// MsgInsets carries the area of the surface the system is drawing over.
// It is its own message rather than a Config field because insets change
// on their own schedule: the soft keyboard opening does not resize the
// surface, and a bar auto-hiding does not either.
MsgInsets uint8 = 0x06
// MsgReady tells the host the shared buffer is mapped at the announced
// size, so the host may map it in turn. Every MsgFrame that follows
// refers to this mapping, until the next MsgReady replaces it.
MsgReady uint8 = 0x81
// MsgFrame tells the host which surface-local rectangle changed.
MsgFrame uint8 = 0x82
// MsgTitle updates the host's window title.
MsgTitle uint8 = 0x83
// MsgBye tells the host the application ended.
MsgBye uint8 = 0x84
// MsgA11yTree answers MsgA11yRequest with the accessibility elements.
MsgA11yTree uint8 = 0x85
// MsgKeyboard asks the host to show or hide the soft keyboard. Only the host
// can: the keyboard is a window, and the application owns no windows.
MsgKeyboard uint8 = 0x86
)
// Touch actions, matching the three MotionEvent actions the host forwards.
const (
TouchDown uint8 = 0
TouchUp uint8 = 1
TouchMove uint8 = 2
)
// Key actions.
const (
KeyDown uint8 = 0
KeyUp uint8 = 1
)
// Lifecycle states.
const (
LifecyclePause uint8 = 0
LifecycleResume uint8 = 1
)
// MaxPayload bounds one decoded message body. The largest message a host
// legitimately sends is a Config carrying a filesystem path, so a frame beyond
// this is a desynchronised stream — refused rather than allocated.
const MaxPayload = 1 << 16
// ErrShortPayload reports a message whose body is too short for its type.
var ErrShortPayload = errors.New("android: truncated message payload")
// Rect is a surface-local rectangle in pixels. It mirrors toolkit.Rect but is
// kept local so the codec stays a leaf with one toolkit dependency (the event
// model).
type Rect struct{ X, Y, W, H int }
// Config is the host's geometry announcement.
type Config struct {
// W and H are the surface size in physical pixels.
W, H int
// Density is the display density in hundredths (Android's
// DisplayMetrics.density × 100, so a 3.0x panel arrives as 300). It is the
// Android spelling of the backing-scale factor the Cocoa back-end reads
// from the screen.
Density int
// BufPath is the file the application maps as its framebuffer. The host
// picks it inside the app's own storage, which both processes share.
BufPath string
}
// Touch is one pointer sample.
type Touch struct {
Action uint8
X, Y int
// ID is the pointer index, so a later multi-touch host can be told apart
// from this one without a protocol break. Single-touch hosts send 0.
ID int
}
// Insets is the margin of the surface the system draws over, in pixels.
//
// An Android window is edge-to-edge from API 35: the surface really is the
// whole screen, and the status bar, the navigation bar, a display cutout and
// the soft keyboard are painted ON TOP of it rather than shrinking it. So a
// widget tree laid out to the full surface is correct in size and wrong in
// practice — its first and last rows are behind the bars. These are the four
// edges to keep clear.
type Insets struct{ Left, Top, Right, Bottom int }
// Empty reports whether nothing is covering the surface.
func (i Insets) Empty() bool { return i == Insets{} }
// Apply returns the part of a w×h surface that nothing is drawn over. It never
// returns a negative extent: insets wider than the surface (a phone folded to
// a sliver, a bad host) collapse the area to zero rather than inverting it.
func (i Insets) Apply(w, h int) Rect {
r := Rect{X: i.Left, Y: i.Top, W: w - i.Left - i.Right, H: h - i.Top - i.Bottom}
if r.W < 0 {
r.W = 0
}
if r.H < 0 {
r.H = 0
}
return r
}
// EncodeInsets builds a MsgInsets body.
func EncodeInsets(i Insets) []byte {
b := appendInt32(make([]byte, 0, 16), i.Left)
b = appendInt32(b, i.Top)
b = appendInt32(b, i.Right)
return appendInt32(b, i.Bottom)
}
// DecodeInsets parses a MsgInsets body.
func DecodeInsets(b []byte) (Insets, error) {
if len(b) < 16 {
return Insets{}, ErrShortPayload
}
return Insets{
Left: int32At(b, 0),
Top: int32At(b, 4),
Right: int32At(b, 8),
Bottom: int32At(b, 12),
}, nil
}
// Key is one key event.
type Key struct {
Action uint8
// Code is the Android KeyEvent key code.
Code int
// Rune is the character the key produced, or 0 for a key that produces
// none (an arrow, a modifier, the back key).
Rune rune
}
// EncodeConfig builds a MsgConfig body.
func EncodeConfig(c Config) []byte {
b := make([]byte, 0, 16+len(c.BufPath))
b = appendInt32(b, c.W)
b = appendInt32(b, c.H)
b = appendInt32(b, c.Density)
b = appendInt32(b, len(c.BufPath))
return append(b, c.BufPath...)
}
// DecodeConfig parses a MsgConfig body.
func DecodeConfig(b []byte) (Config, error) {
if len(b) < 16 {
return Config{}, ErrShortPayload
}
c := Config{W: int32At(b, 0), H: int32At(b, 4), Density: int32At(b, 8)}
n := int32At(b, 12)
if n < 0 || 16+n > len(b) {
return Config{}, ErrShortPayload
}
c.BufPath = string(b[16 : 16+n])
return c, nil
}
// EncodeTouch builds a MsgTouch body.
func EncodeTouch(t Touch) []byte {
b := make([]byte, 0, 13)
b = append(b, t.Action)
b = appendInt32(b, t.X)
b = appendInt32(b, t.Y)
return appendInt32(b, t.ID)
}
// DecodeTouch parses a MsgTouch body.
func DecodeTouch(b []byte) (Touch, error) {
if len(b) < 13 {
return Touch{}, ErrShortPayload
}
return Touch{Action: b[0], X: int32At(b, 1), Y: int32At(b, 5), ID: int32At(b, 9)}, nil
}
// EncodeKey builds a MsgKey body.
func EncodeKey(k Key) []byte {
b := make([]byte, 0, 9)
b = append(b, k.Action)
b = appendInt32(b, k.Code)
return appendInt32(b, int(k.Rune))
}
// DecodeKey parses a MsgKey body.
func DecodeKey(b []byte) (Key, error) {
if len(b) < 9 {
return Key{}, ErrShortPayload
}
return Key{Action: b[0], Code: int32At(b, 1), Rune: rune(int32At(b, 5))}, nil
}
// EncodeReady builds a MsgReady body: the size the application actually mapped.
func EncodeReady(w, h int) []byte {
return appendInt32(appendInt32(make([]byte, 0, 8), w), h)
}
// DecodeReady parses a MsgReady body.
func DecodeReady(b []byte) (w, h int, err error) {
if len(b) < 8 {
return 0, 0, ErrShortPayload
}
return int32At(b, 0), int32At(b, 4), nil
}
// EncodeFrame builds a MsgFrame body naming the damaged rectangle.
func EncodeFrame(r Rect) []byte {
b := appendInt32(make([]byte, 0, 16), r.X)
b = appendInt32(b, r.Y)
b = appendInt32(b, r.W)
return appendInt32(b, r.H)
}
// DecodeFrame parses a MsgFrame body.
func DecodeFrame(b []byte) (Rect, error) {
if len(b) < 16 {
return Rect{}, ErrShortPayload
}
return Rect{X: int32At(b, 0), Y: int32At(b, 4), W: int32At(b, 8), H: int32At(b, 12)}, nil
}
// FrameMessage returns one framed message: a 4-byte big-endian length covering
// the type byte and the body, then the type byte, then the body. Big-endian
// keeps the Java host on DataInputStream.readInt with no byte-swapping.
//
// It exists as bytes rather than as writes because a message that carries an
// ancillary descriptor has to reach the host in ONE sendmsg: split across two
// writes, the host could attribute the descriptor to the wrong message.
func FrameMessage(typ uint8, body []byte) []byte {
b := make([]byte, 5+len(body))
binary.BigEndian.PutUint32(b, uint32(len(body)+1))
b[4] = typ
copy(b[5:], body)
return b
}
// WriteMessage writes one framed message.
func WriteMessage(w io.Writer, typ uint8, body []byte) error {
_, err := w.Write(FrameMessage(typ, body))
return err
}
// ReadMessage reads one framed message. It returns io.EOF when the stream ends
// cleanly between messages, so a caller can tell a closed host from a truncated
// one.
func ReadMessage(r io.Reader) (typ uint8, body []byte, err error) {
var hdr [5]byte
if _, err := io.ReadFull(r, hdr[:4]); err != nil {
return 0, nil, err
}
n := int(binary.BigEndian.Uint32(hdr[:4]))
if n < 1 || n > MaxPayload {
return 0, nil, fmt.Errorf("android: message length %d out of range", n)
}
if _, err := io.ReadFull(r, hdr[4:]); err != nil {
return 0, nil, err
}
body = make([]byte, n-1)
if _, err := io.ReadFull(r, body); err != nil {
return 0, nil, err
}
return hdr[4], body, nil
}
// MapTouch maps one pointer sample to toolkit events.
//
// A contact always yields its touch event — EventTouchStart/Move/End with the
// pointer id in Event.Code, which is what toolkit's GestureRecognizer and
// MultiTouchRecognizer key contacts by.
//
// Only the PRIMARY contact also yields a compatibility mouse event. Most
// widgets, and every widget written before touch existed, listen for
// EventClick; but a second finger must not fire a second click, or a pinch
// would read as two taps to every widget in the tree. A browser draws the line
// in the same place, for the same reason.
//
// The mouse half mirrors the wasmbox and X11 mappings: a press is a click, a
// move with a finger down is a drag. A touch screen has no hover, so a move
// with nothing down cannot occur and is mapped to a plain move rather than
// dropped, keeping a synthetic host (a test, a replay) honest.
func MapTouch(t Touch, held, primary bool) []toolkit.Event {
id := strconv.Itoa(t.ID)
var touch, mouse toolkit.Event
switch t.Action {
case TouchDown:
touch = toolkit.Event{Kind: toolkit.EventTouchStart, X: t.X, Y: t.Y, Code: id}
mouse = toolkit.Event{Kind: toolkit.EventClick, X: t.X, Y: t.Y}
case TouchUp:
touch = toolkit.Event{Kind: toolkit.EventTouchEnd, X: t.X, Y: t.Y, Code: id}
mouse = toolkit.Event{Kind: toolkit.EventMouseUp, X: t.X, Y: t.Y}
case TouchMove:
touch = toolkit.Event{Kind: toolkit.EventTouchMove, X: t.X, Y: t.Y, Code: id}
kind := toolkit.EventMouseMove
if held {
kind = toolkit.EventMouseDrag
}
mouse = toolkit.Event{Kind: kind, X: t.X, Y: t.Y}
default:
return nil
}
if !primary {
return []toolkit.Event{touch}
}
return []toolkit.Event{touch, mouse}
}
// Android KeyEvent key codes the toolkit has a named key for. Only the codes
// that map to a toolkit key are listed; anything else reaches the tree as its
// unicode rune, or not at all.
const (
akeycodeDpadUp = 19
akeycodeDpadDown = 20
akeycodeDpadLeft = 21
akeycodeDpadRight = 22
akeycodeEnter = 66
akeycodeDel = 67 // backspace
akeycodeTab = 61
akeycodeEscape = 111
akeycodeForwardDel = 112
akeycodeMoveHome = 122
akeycodeMoveEnd = 123
akeycodePageUp = 92
akeycodePageDown = 93
akeycodeSpace = 62
)
// androidKeys maps an Android key code to the DOM-style key name the toolkit's
// widgets switch on (toolkit.Event.Code) — the same names internal/x11's
// keysym table produces, so a widget's key handling is identical on every
// back-end.
var androidKeys = map[int]string{
akeycodeDpadUp: "ArrowUp",
akeycodeDpadDown: "ArrowDown",
akeycodeDpadLeft: "ArrowLeft",
akeycodeDpadRight: "ArrowRight",
akeycodeEnter: "Enter",
akeycodeDel: "Backspace",
akeycodeTab: "Tab",
akeycodeEscape: "Escape",
akeycodeForwardDel: "Delete",
akeycodeMoveHome: "Home",
akeycodeMoveEnd: "End",
akeycodePageUp: "PageUp",
akeycodePageDown: "PageDown",
akeycodeSpace: "Space",
}
// MapKey maps one Android key event to toolkit events, mirroring the wasmbox
// and X11 mappings: a named key is one EventKeyDown/EventKeyUp; a key that
// committed a character is an EventKeyDown followed by an EventChar on press,
// and an EventKeyUp on release. A key that is neither named nor printable
// reaches the tree as nothing.
func MapKey(k Key) []toolkit.Event {
press := k.Action == KeyDown
kind := toolkit.EventKeyDown
if !press {
kind = toolkit.EventKeyUp
}
if name, ok := androidKeys[k.Code]; ok {
return []toolkit.Event{{Kind: kind, Code: name}}
}
if k.Rune == 0 {
return nil
}
code := string(k.Rune)
if !press {
return []toolkit.Event{{Kind: toolkit.EventKeyUp, Code: code}}
}
return []toolkit.Event{
{Kind: toolkit.EventKeyDown, Code: code},
{Kind: toolkit.EventChar, Code: code},
}
}
// ClampRect clips r to a w×h surface, returning a zero-area rectangle when
// nothing of r is inside. The host trusts the rectangle it is given, so the
// application clamps before sending.
func ClampRect(r Rect, w, h int) Rect {
if r.X < 0 {
r.W += r.X
r.X = 0
}
if r.Y < 0 {
r.H += r.Y
r.Y = 0
}
if r.X+r.W > w {
r.W = w - r.X
}
if r.Y+r.H > h {
r.H = h - r.Y
}
if r.W < 0 || r.H < 0 || r.X >= w || r.Y >= h {
return Rect{}
}
return r
}
// appendInt32 appends v as a 4-byte big-endian integer.
func appendInt32(b []byte, v int) []byte {
return binary.BigEndian.AppendUint32(b, uint32(int32(v)))
}
// int32At reads the 4-byte big-endian integer at off.
func int32At(b []byte, off int) int { return int(int32(binary.BigEndian.Uint32(b[off:]))) }
// ErrUnsupported reports an environment with no Android host: every GOOS but
// Linux, where the abstract socket and the shared mapping the host protocol
// needs do not exist. A cross-built application gets this from Dial and can
// report it and exit cleanly, exactly as go-widgets/window does off its
// supported back-ends.
var ErrUnsupported = errors.New("android: no Android host on this platform")
// MapText turns text an input method committed into toolkit events.
//
// A soft keyboard is not a keyboard: it does not send keystrokes, it commits
// finished text, sometimes several characters at once — a word completion, an
// emoji, a pasted clipboard. Each rune therefore becomes the pair a printable
// key produces, EventKeyDown then EventChar, which is exactly what the X11 and
// wasmbox back-ends emit for a typed character. Every text widget in the
// toolkit already consumes that pair, so an input method needs no new path
// through the widget tree.
func MapText(s string) []toolkit.Event {
evs := make([]toolkit.Event, 0, 2*len([]rune(s)))
for _, r := range s {
code := string(r)
evs = append(evs,
toolkit.Event{Kind: toolkit.EventKeyDown, Code: code},
toolkit.Event{Kind: toolkit.EventChar, Code: code},
)
}
return evs
}
// MapTextDelete turns an input method's "delete n characters before the cursor"
// into n backspaces, which is how the toolkit's text widgets spell it.
func MapTextDelete(n int) []toolkit.Event {
if n <= 0 {
return nil
}
evs := make([]toolkit.Event, 0, n)
for i := 0; i < n; i++ {
evs = append(evs, toolkit.Event{Kind: toolkit.EventKeyDown, Code: "Backspace"})
}
return evs
}
// DecodeTextDelete parses a MsgTextDelete body.
func DecodeTextDelete(b []byte) (int, error) {
if len(b) < 4 {
return 0, ErrShortPayload
}
return int32At(b, 0), nil
}
// EncodeTextDelete builds a MsgTextDelete body.
func EncodeTextDelete(n int) []byte { return appendInt32(nil, n) }
// Scroll is one scroll notch from a wheel or a trackpad, in detents: positive
// Y scrolls toward the end of the content, positive X toward its right.
type Scroll struct{ X, Y, DetentX, DetentY int }
// EncodeScroll builds a MsgScroll body.
func EncodeScroll(s Scroll) []byte {
b := appendInt32(nil, s.X)
b = appendInt32(b, s.Y)
b = appendInt32(b, s.DetentX)
return appendInt32(b, s.DetentY)
}
// DecodeScroll parses a MsgScroll body.
func DecodeScroll(b []byte) (Scroll, error) {
if len(b) < 16 {
return Scroll{}, ErrShortPayload
}
return Scroll{
X: int32At(b, 0), Y: int32At(b, 4),
DetentX: int32At(b, 8), DetentY: int32At(b, 12),
}, nil
}
// MapScroll maps a scroll notch to a toolkit event.
//
// Android reports a wheel detent as +1 UP and -1 DOWN, the opposite of the
// toolkit's convention, where a positive Delta scrolls toward the end of the
// content. So the vertical axis is negated and the horizontal one is not:
// Android's AXIS_HSCROLL is already positive to the right.
//
// A notch with no movement on either axis produces nothing rather than a
// Delta-0 event, so a device that reports an idle scroll wakes no widget.
func MapScroll(s Scroll) []toolkit.Event {
if s.DetentX == 0 && s.DetentY == 0 {
return nil
}
return []toolkit.Event{{
Kind: toolkit.EventScroll,
X: s.X,
Y: s.Y,
Delta: -s.DetentY,
DeltaX: s.DetentX,
}}
}