158 lines
9.3 KiB
Markdown
158 lines
9.3 KiB
Markdown
# Qortal GO 2.0 — Native Mobile Qortal Client
|
||
|
||
**Qortal GO 2.0** is the native Android port of [Qortal Hub](https://github.com/Qortal/Qortal-Hub)
|
||
(this repo is a fork of `Qortal/Qortal-Hub`, branch `feature/qortal-go-mobilize`). It wraps the Hub's
|
||
React UI in a Capacitor/Android shell and adds a **native Reticulum (RNS) stack on the phone**, giving
|
||
mobile users the Hub 2.0 P2P feature set: presence, voice calls, and direct file transfer over
|
||
Reticulum — plus a mobile-first UI, OS notifications, biometric login, and battery-conscious
|
||
background behavior.
|
||
|
||
> **For Hub developers:** two small Hub-side changes are needed for full mobile ↔ Hub interop.
|
||
> Both are described in [Required Hub changes](#required-hub-changes) below. Everything else is
|
||
> wire-compatible with the existing Hub.
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
| Path | What it is |
|
||
|---|---|
|
||
| `src/` | Shared React UI (same codebase as desktop Hub, with mobile components added) |
|
||
| `src/bridge/capacitorBridge.ts` | The mobile equivalent of Electron's main-process bridges: installs `window.presence` / `window.call` / `window.groupCall`, presence overlay coordinator, Q-Chat file transfer sender logic |
|
||
| `src/plugins/Reticulum.ts` | Capacitor plugin typings for the native Reticulum bridge |
|
||
| `src/utils/qchatFileApi.ts` | Unified desktop/mobile Q-Chat file transfer API (desktop → `electronAPI`, mobile → Capacitor) |
|
||
| `src/components/Mobile/` | Mobile shell: top bar, tab bar, tabs overlay with folders, etc. |
|
||
| `android/` | Capacitor Android project |
|
||
| `android/app/src/main/java/.../ReticulumPlugin.kt` | Native plugin: Kotlin RNS transport (reticulum-kt) + Chaquopy host for the Python bridge + file staging helpers |
|
||
| `android/app/src/main/java/.../QortalForegroundService.java` | Foreground service: persistent status notification, alert/call notifications |
|
||
| `android/app/src/main/python/presence_bridge.py` | The full Reticulum protocol bridge (presence, calls, group audio, file transfer) — **shared with desktop**, runs under Chaquopy on Android |
|
||
| `electron/resources/presence_bridge.py` | The **desktop copy** of the same bridge — kept in sync; contains the file-transfer fixes the Hub needs to ship |
|
||
| `electron/` | Desktop Electron shell (unchanged upstream behavior, plus the bridge fix) |
|
||
|
||
## Building the Android app
|
||
|
||
Prerequisites: Node 20+, Android SDK (API 34), JDK 17.
|
||
|
||
```bash
|
||
npm install
|
||
npx vite build && npx cap copy android
|
||
cd android && ./gradlew assembleDebug
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
The Python bridge (`presence_bridge.py`) and vendored RNS ship inside the APK via Chaquopy; no
|
||
on-device Python setup is needed.
|
||
|
||
## Mobile architecture in one paragraph
|
||
|
||
The WebView runs the same renderer as desktop Hub. Where desktop talks to Electron's main process
|
||
(`window.electronAPI`, IPC to `reticulum-daemon.ts`), mobile installs equivalent bridges from
|
||
`capacitorBridge.ts` that talk to `ReticulumPlugin.kt`, which hosts the *identical*
|
||
`presence_bridge.py` in an embedded CPython (Chaquopy). Commands flow as JSON frames
|
||
(`presenceCommand` with request/response correlation); events stream back over a single `presence`
|
||
listener. The result: the phone speaks the exact same RNS wire protocol as the Hub — same presence
|
||
envelopes, same call signaling, same `QGAU` audio framing, same `QGCCTL1` link auth, same file
|
||
transfer Resources.
|
||
|
||
## Feature status (mobile ↔ Hub interop)
|
||
|
||
| Feature | Status |
|
||
|---|---|
|
||
| Presence (announce/heartbeat over RNS overlay) | ✅ Works both ways |
|
||
| Q-Chat (blockchain chat) | ✅ Works |
|
||
| Voice calls **Hub → mobile** | ✅ Works — two-way audio over verified RNS link |
|
||
| Voice calls **mobile → Hub** | ⚠️ Blocked by a Hub-side ordering precondition — see [Hub change #1](#1-voice-calls-admit-verified-link-auth-joins) |
|
||
| Q-Chat file transfer **mobile → Hub** | ✅ Works (mobile sender has the robustness fixes) |
|
||
| Q-Chat file transfer **Hub → mobile**, large files | ⚠️ Reliable only after the Hub ships [Hub change #2](#2-file-transfer-ship-the-updated-presence_bridgepy) |
|
||
| OS notifications (PM summary + sender avatar), biometric login, mobile tabs/folders | ✅ Mobile-only features |
|
||
|
||
---
|
||
|
||
## Required Hub changes
|
||
|
||
### 1. Voice calls: admit verified link-auth joins
|
||
|
||
*(See `MOBILE_VOICE_HUB_DEV_NOTE.md` in this repo for the full write-up.)*
|
||
|
||
**Symptom:** Hub → mobile calls connect with clean two-way audio. Mobile → Hub calls stay on
|
||
"connecting…" on the Hub and drop after a few seconds — even though the phone sends the exact same
|
||
signed link auth the Hub accepts in the other direction.
|
||
|
||
**Root cause (traced in `electron/src/group-call.ts`):** it is *not* a protocol or signature
|
||
problem — the `GC_JOIN` verifies. It's an ordering precondition:
|
||
|
||
- The audio-link owner is chosen by address ordering (`isLocalAddressReticulumAudioLinkOwner`),
|
||
not by who placed the call.
|
||
- Inbound link audio is dropped unless the link is marked verified (`audio-unverified-address`).
|
||
- `applyVerifiedReticulumLinkAuthJoin` only marks a link verified **if the sender is already in
|
||
`room.participants`** at that instant.
|
||
|
||
When the phone initiates, its verified link-auth `GC_JOIN` arrives *before* the Hub has registered
|
||
the caller as a participant, so the Hub closes the link (`link-auth-no-participant`), drops the
|
||
audio, and never leaves "connecting". In the Hub-initiated direction the participant state is
|
||
already in place, so the identical handshake succeeds.
|
||
|
||
**The change:** in `applyVerifiedReticulumLinkAuthJoin`, when the signed `GC_JOIN` **verifies** and
|
||
the sender is a valid Qortal member of the call, **admit the participant from the verified join**
|
||
instead of bailing with `link-auth-no-participant`. The verified join is itself authenticated proof
|
||
of membership (Ed25519 over `buildGcJoinSignedFields`, already checked), so this removes the race
|
||
without weakening security. No changes to wire format, audio framing, signing, key delivery, or the
|
||
link-owner rule; Hub-initiated calls are unaffected.
|
||
|
||
### 2. File transfer: ship the updated `presence_bridge.py`
|
||
|
||
**Symptom:** large transfers (e.g. ~80 MB Hub → mobile) stall at exactly the first window of
|
||
parallel chunks (8 × 1 MB) and the retry restarts from zero.
|
||
|
||
**Root cause:** the per-chunk `QCHAT_FILE_CHUNK_ACK` is a single link packet with no retry. Under
|
||
congestion a lost ACK caused:
|
||
|
||
1. the sender's 90 s ACK timeout to **fail the whole transfer** — and the un-ACKed chunk was never
|
||
re-queued (`next_chunk_index` had already advanced → permanent hole in the file);
|
||
2. dead links were never replaced, so the 8-link pool only shrank;
|
||
3. re-accepting restarted the download from byte 0 (and could never fill the holes anyway).
|
||
|
||
**The change (already implemented in this repo's `electron/resources/presence_bridge.py`,
|
||
mirrored in the Android copy):**
|
||
|
||
- **`retry_chunks` queue** on the send root — an ACK timeout or mid-chunk link failure re-queues
|
||
the chunk (consumed with priority by `_start_qchat_file_resource_for_state`) and emits
|
||
`retrying` instead of `failed`; only the suspect link is torn down.
|
||
- **Receiver link reopen** — `on_qchat_file_link_closed` opens a replacement link while a receive
|
||
is pending (the receiver is the link initiator; bounded by
|
||
`_QCHAT_FILE_RECEIVER_MAX_LINK_REOPENS = 64`).
|
||
- **Resume** — the receiver persists a `<savePath>.part.meta` ledger (transferId, size, sha256,
|
||
completed chunks) per chunk; a re-accept preloads it and resumes instead of restarting. The meta
|
||
is removed on completion, and both `.part` and meta are discarded on a final hash mismatch.
|
||
|
||
**What to do:** include this file in the next Hub release — nothing else changes. No new packet
|
||
types; fully wire-compatible with old peers. The sender-side re-queue is the critical half, so
|
||
Hub → mobile transfers of large files only become reliable once the Hub ships it.
|
||
|
||
---
|
||
|
||
## Mobile-specific work in this fork (overview)
|
||
|
||
- **Q-Chat file transfer on mobile:** native file staging (`qchatFilePrepare` copies content://
|
||
URIs to app cache + SHA-256), auto save paths, export of completed downloads to public
|
||
Downloads (MediaStore), an **Open** button on received files, renderer-side Ed25519 verification
|
||
of downloader link auth, pending-send persistence across app restarts.
|
||
- **Notifications:** single in-place-updating direct-message summary notification
|
||
("You got N messages from X and Y") with the sender's avatar (fetched in the WebView and passed
|
||
as a data URL — node MIME quirks and OEM skin differences handled), MIUI-aware status
|
||
notification branding, in-app Qortino notifications with auto-dismiss countdown.
|
||
- **Performance/battery:** deduped + rate-limited foreground service updates (was ~7 native
|
||
calls/s, now ≤0.2/s), in-memory cache for hot secure-storage keys (−73% bridge reads),
|
||
visibility-aware polling (Reticulum status, balance, dashboard), WASM bcrypt wallet KDF
|
||
(verified byte-identical to bcryptjs), lazy i18n locales (EN + active language only), avatar
|
||
fetch outcome caching.
|
||
- **Mobile UX:** tab bar + tabs overlay with folders, app library list layouts, biometric unlock
|
||
flow (no keyboard pop on autofill), blocked-accounts flows for public/private nodes, Android
|
||
back-button handling, DM voice call UI.
|
||
|
||
## Upstream
|
||
|
||
Forked from [Qortal/Qortal-Hub](https://gitea.qortal.link/Qortal/Qortal-Hub) — see that repo for
|
||
the desktop Hub documentation, i18n guidelines (`docs/i18n_languages.md`), and development docs
|
||
(`docs/development.md`). License: GPL-3.0 (unchanged).
|