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
|
# MeshBay — Android client
A client, not a host: no node runs on a phone (`docs/MESHBAY_DESIGN.md` §11.3).
The shell is a system WebView showing the interface **from the package** —
`meshbay-hub/src/meshbay_hub/static/` copied at build time into
`build/generated/`, never committed (§8.3) — with a bridge
(`app/src/main/assets/bridge/meshbay-bridge.js`) that offers the page the same
`window.meshbay` as the desktop preload, wherever it offers anything at all.
Hub calls leave from native code, to the signed-in hub only. The device key,
the bundle key and every node identity are held natively under an Android
Keystore key; the page is told public keys and handed signatures, asked for by
kind — never bytes. `meshbay-hub/tests/vectors/keyring.json` holds that keyring
to the desktop's and to the specification.
Downloads are written to disk as they arrive — into the folder chosen in
Settings (a Storage Access Framework tree, as `<name>.part` until complete) or
the system Downloads collection (a pending entry until complete) — and the
page holds an opaque id, never a URI. Uploads come through the system picker.
Casting: the page pushes the decrypted stream to a local HTTP relay (a port of
the desktop's `cast-relay.js`, bound to the Wi-Fi address only); receivers are
found and driven through the platform cast SDK with the default media receiver.
While a cast runs, a media-playback foreground service holds the CPU and the
Wi-Fi, and the WebView is kept reported visible — without that, Chromium
freezes the page 60 s after the screen goes off. Where play services are
absent, the page is offered no cast at all. Music playing on the phone itself
holds the same service and the same visibility, for as long as it plays
(`playback:keep-alive`), so the next track still loads with the screen off.
```bash
# needs JDK 17+ and an Android SDK (ANDROID_HOME, or sdk.dir in local.properties)
./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # JVM unit tests
./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk
```
A release is signed with the release key, which never enters the repository.
`assembleRelease` reads it from `~/.gradle/gradle.properties`, and stops if
any of these is missing rather than signing with the debug key:
```properties
meshbayReleaseStoreFile=/path/to/meshbay-release.jks
meshbayReleaseStorePassword=...
meshbayReleaseKeyAlias=meshbay
meshbayReleaseKeyPassword=...
```
`apksigner verify --print-certs app-release.apk` prints the certificate's
SHA-256 fingerprint, the one the download page publishes (§8.2). A release
does not install over a debug build, or the reverse: the keys differ.
The security contract is also pinned from the Python suite by reading this
source: `packages/meshbay-hub/tests/test_android_shell.py`.
Notifications while closed, with nothing to install: `notify/PollJob` (a
system job, no library) fetches what is new from the hub every fifteen minutes
with the phone's poll secret — never a session. When a UnifiedPush distributor
is already on the phone (ntfy, …) it is used too: the hub pushes at once,
encrypted to the phone (RFC 8291), `notify/PushReceiver` draws what the
connector could decrypt, and the fetch slows to a four-hour net. The page turns
it on in Settings and registers the phone with the hub; muting and "disable
all" are decided on the hub, which then creates nothing (§11.3).
Photo backup (`photos/`, §9.12): MediaStore is listed natively, a ledger of
what was sent is kept per account, group and folder, and each photo's bytes are
handed to the page at `/photosync/<token>` on the packaged origin — never a
URI. The page decides when a run is due and sends; a `dataSync` foreground
service (`BackupService`) keeps it going with the screen off. No
`ACCESS_MEDIA_LOCATION`, so the platform redacts a photo's location from what
is read.
Not built yet: phone-specific behaviour (back button, network handover,
keeping a download alive with the screen off), updates through a store.
## Icon
The desktop client's icon, `meshbay-client/build/icon-square.png`, placed in
the adaptive icon's safe zone (72 dp of the 108 dp layer, which is what every
launcher mask leaves visible) over its own edge colour, so no mask crops the
M. The five `mipmap-*/ic_launcher_foreground.png` are generated from it:
```python
from PIL import Image, ImageDraw, ImageFilter
src = Image.open("../meshbay-client/build/icon-square.png").convert("RGBA")
for name, L in [("mdpi", 108), ("hdpi", 162), ("xhdpi", 216), ("xxhdpi", 324), ("xxxhdpi", 432)]:
S = L * 72 // 108; b = max(2, S // 25)
img = src.resize((S, S), Image.LANCZOS)
mask = Image.new("L", (S, S), 0)
ImageDraw.Draw(mask).rectangle([b, b, S - b - 1, S - b - 1], fill=255)
img.putalpha(mask.filter(ImageFilter.GaussianBlur(b)))
layer = Image.new("RGBA", (L, L), (0, 0, 0, 0))
layer.paste(img, ((L - S) // 2, (L - S) // 2), img)
layer.save(f"app/src/main/res/mipmap-{name}/ic_launcher_foreground.png", optimize=True)
```
No monochrome layer: a themed icon keeps only the layer's alpha, and this one
would be a filled square.
|