Skip to content

Commit 6ab8b0b

Browse files
committed
Add mobile VX6 chat SDK bridge
1 parent aa4c2a0 commit 6ab8b0b

11 files changed

Lines changed: 1101 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# VX6 Comms Android App Plan
2+
3+
## Target Architecture
4+
5+
Android app:
6+
7+
- Kotlin + Jetpack Compose UI
8+
- foreground service runs embedded VX6 node
9+
- `vx6mobile.aar` generated from `./mobile`
10+
- Room database for local contacts/messages
11+
- no external API server required
12+
13+
VX6 core:
14+
15+
- `sdk.Client` handles node init/start/peer/DHT behavior
16+
- `sdk/chat` handles desktop-compatible invite/envelope/ledger format
17+
- `mobile.Engine` exposes JSON/string APIs for Kotlin
18+
19+
## First MVP Flow
20+
21+
1. Setup screen calls `Engine.Init(...)`.
22+
2. Foreground service calls `Engine.StartNode()`.
23+
3. Invite screen calls `Engine.GenerateChatInvite()`.
24+
4. Add-contact screen calls `Engine.AcceptChatInvite(invite)` or `Engine.AddChatContactJSON(contact)`.
25+
5. Chat screen calls `Engine.SendText(nodeID, text)`.
26+
6. Chat screen refreshes with `Engine.MessagesJSON(nodeID)`.
27+
28+
## Android Build
29+
30+
From repo root:
31+
32+
```bash
33+
go install golang.org/x/mobile/cmd/gomobile@latest
34+
scripts/build_android_mobile.sh
35+
```
36+
37+
Output:
38+
39+
```text
40+
apps/vx6comms-android/app/libs/vx6mobile.aar
41+
```
42+
43+
Targets:
44+
45+
- `android/arm64`: real Android devices
46+
- `android/amd64`: emulator
47+
48+
## Compatibility Rule
49+
50+
Desktop and Android must share:
51+
52+
- `vx6chat://invite/...` format
53+
- `vx6chat/conv/<node-a>/<node-b>` DHT ledger key
54+
- message envelope JSON fields
55+
- AES-GCM shared-secret fallback encryption path
56+
57+
The current bridge supports the shared-secret desktop-compatible path. The next step is moving desktop X3DH/ratchet session code into `sdk/chat` so both desktop and mobile use the same advanced session state.
58+
59+
## Kotlin Implementation Guide
60+
61+
See [KOTLIN_QUICKSTART.md](./KOTLIN_QUICKSTART.md) for:
62+
63+
- Gradle setup
64+
- Android permissions
65+
- foreground service skeleton
66+
- Compose ViewModel shape
67+
- exact calls into `mobile.Engine`
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# VX6 Comms Android Kotlin Quickstart
2+
3+
This guide shows how to start a Kotlin Android chat app that embeds VX6 directly with no external API server.
4+
5+
The app calls the Go mobile bridge in `./mobile`, packaged as:
6+
7+
```text
8+
apps/vx6comms-android/app/libs/vx6mobile.aar
9+
```
10+
11+
## 1. Build the VX6 Android Library
12+
13+
From the repository root:
14+
15+
```bash
16+
go install golang.org/x/mobile/cmd/gomobile@latest
17+
scripts/build_android_mobile.sh
18+
```
19+
20+
This builds:
21+
22+
```text
23+
apps/vx6comms-android/app/libs/vx6mobile.aar
24+
```
25+
26+
Supported targets in the script:
27+
28+
- `android/arm64`: modern real Android phones
29+
- `android/amd64`: Android emulator
30+
31+
## 2. Create Android Project
32+
33+
Use Android Studio:
34+
35+
1. New Project
36+
2. Empty Activity
37+
3. Language: Kotlin
38+
4. UI: Jetpack Compose
39+
5. Minimum SDK: 26 or newer
40+
41+
Recommended package:
42+
43+
```text
44+
tech.vx6.comms
45+
```
46+
47+
## 3. Add AAR Dependency
48+
49+
Copy or keep the generated AAR here:
50+
51+
```text
52+
app/libs/vx6mobile.aar
53+
```
54+
55+
In `app/build.gradle.kts`:
56+
57+
```kotlin
58+
android {
59+
namespace = "tech.vx6.comms"
60+
compileSdk = 36
61+
62+
defaultConfig {
63+
applicationId = "tech.vx6.comms"
64+
minSdk = 26
65+
targetSdk = 36
66+
versionCode = 1
67+
versionName = "0.1.0"
68+
}
69+
}
70+
71+
dependencies {
72+
implementation(files("libs/vx6mobile.aar"))
73+
74+
implementation("androidx.core:core-ktx:1.15.0")
75+
implementation("androidx.activity:activity-compose:1.10.0")
76+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
77+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
78+
}
79+
```
80+
81+
If Gradle cannot resolve the local AAR, add this to the project-level repositories:
82+
83+
```kotlin
84+
repositories {
85+
google()
86+
mavenCentral()
87+
flatDir {
88+
dirs("app/libs")
89+
}
90+
}
91+
```
92+
93+
## 4. Android Permissions
94+
95+
In `AndroidManifest.xml`:
96+
97+
```xml
98+
<uses-permission android:name="android.permission.INTERNET" />
99+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
100+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
101+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
102+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
103+
```
104+
105+
Inside `<application>`:
106+
107+
```xml
108+
<service
109+
android:name=".VX6NodeService"
110+
android:exported="false"
111+
android:foregroundServiceType="dataSync" />
112+
```
113+
114+
## 5. Kotlin Bridge Usage
115+
116+
`gomobile bind` exposes the Go package as Java/Kotlin bindings. For this package, expect usage similar to:
117+
118+
```kotlin
119+
import mobile.Mobile
120+
import mobile.Engine
121+
```
122+
123+
Create one engine instance:
124+
125+
```kotlin
126+
val configPath = File(filesDir, "vx6/config.json").absolutePath
127+
val engine: Engine = Mobile.newEngine(configPath)
128+
```
129+
130+
Initialize a VX6 node:
131+
132+
```kotlin
133+
val dataDir = File(filesDir, "vx6/data").absolutePath
134+
val downloadsDir = File(filesDir, "vx6/downloads").absolutePath
135+
136+
val resultJson = engine.init(
137+
"alice",
138+
"[::]:4242",
139+
"", // advertise address; set explicitly for direct peer tests
140+
dataDir,
141+
downloadsDir
142+
)
143+
```
144+
145+
Start and stop the VX6 runtime:
146+
147+
```kotlin
148+
engine.startNode()
149+
engine.stopNode()
150+
```
151+
152+
Generate an invite:
153+
154+
```kotlin
155+
val invite = engine.generateChatInvite()
156+
```
157+
158+
Accept an invite:
159+
160+
```kotlin
161+
val contactJson = engine.acceptChatInvite(inviteFromPeer)
162+
```
163+
164+
Send a message:
165+
166+
```kotlin
167+
val envelopeJson = engine.sendText(peerNodeId, "hello from Android")
168+
```
169+
170+
Read messages:
171+
172+
```kotlin
173+
val messagesJson = engine.messagesJSON(peerNodeId)
174+
```
175+
176+
Read local node info:
177+
178+
```kotlin
179+
val infoJson = engine.localNodeInfoJSON()
180+
```
181+
182+
## 6. Foreground Service Shape
183+
184+
Android can kill background work. Run the VX6 node inside a foreground service.
185+
186+
```kotlin
187+
class VX6NodeService : Service() {
188+
private var engine: Engine? = null
189+
190+
override fun onCreate() {
191+
super.onCreate()
192+
startForeground(1001, buildNotification())
193+
194+
val configPath = File(filesDir, "vx6/config.json").absolutePath
195+
engine = Mobile.newEngine(configPath)
196+
engine?.startNode()
197+
}
198+
199+
override fun onDestroy() {
200+
engine?.stopNode()
201+
engine = null
202+
super.onDestroy()
203+
}
204+
205+
override fun onBind(intent: Intent?): IBinder? = null
206+
}
207+
```
208+
209+
Keep one shared engine owner in the app. For the first MVP, it is acceptable to keep the engine in the service and expose actions through a bound service or app-level controller.
210+
211+
## 7. Compose ViewModel Shape
212+
213+
```kotlin
214+
class ChatViewModel(
215+
private val engine: Engine
216+
) : ViewModel() {
217+
var messagesJson by mutableStateOf("[]")
218+
private set
219+
220+
fun send(peerNodeId: String, text: String) {
221+
viewModelScope.launch(Dispatchers.IO) {
222+
engine.sendText(peerNodeId, text)
223+
messagesJson = engine.messagesJSON(peerNodeId)
224+
}
225+
}
226+
227+
fun refresh(peerNodeId: String) {
228+
viewModelScope.launch(Dispatchers.IO) {
229+
messagesJson = engine.messagesJSON(peerNodeId)
230+
}
231+
}
232+
}
233+
```
234+
235+
## 8. MVP Screens
236+
237+
Build these screens first:
238+
239+
1. Setup
240+
- node name
241+
- listen address
242+
- optional advertise address
243+
- init/start button
244+
245+
2. My Invite
246+
- show `GenerateChatInvite()`
247+
- copy/share QR later
248+
249+
3. Add Contact
250+
- paste invite
251+
- call `AcceptChatInvite(...)`
252+
253+
4. Chat
254+
- contact list
255+
- message list from `MessagesJSON(...)`
256+
- input box calls `SendText(...)`
257+
258+
5. Status
259+
- `LocalNodeInfoJSON()`
260+
- `Logs()`
261+
262+
## 9. Compatibility Notes
263+
264+
The current mobile bridge is compatible with the desktop shared-secret chat path:
265+
266+
- same `vx6chat://invite/...` format
267+
- same DHT conversation key
268+
- same message envelope JSON
269+
- same AES-GCM shared-secret fallback encryption
270+
271+
Next protocol work:
272+
273+
- move desktop X3DH/ratchet session logic into `sdk/chat`
274+
- make desktop `apps/vx6comms` use `sdk/chat`
275+
- add desktop-to-Android integration tests
276+
- add hidden/relay-first invite flow for real mobile networks
277+
278+
## 10. Common Problems
279+
280+
If `GenerateChatInvite()` fails with advertise address error:
281+
282+
- direct invites need a reachable `advertiseAddr`
283+
- for same Wi-Fi testing, set the phone IPv6 address explicitly
284+
- for real mobile networks, prefer hidden/relay invite flow once implemented
285+
286+
If the emulator cannot receive direct connections:
287+
288+
- test with two real devices on the same IPv6-capable Wi-Fi first
289+
- or run one desktop VX6 node as reachable bootstrap/relay
290+
291+
If Android kills the node:
292+
293+
- make sure the foreground service notification is active
294+
- avoid running the node from only an Activity

mobile/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# VX6 Mobile Bridge
2+
3+
This package is the mobile-safe bridge for Android and iOS.
4+
5+
It intentionally exposes simple `string`/JSON APIs so `gomobile bind` can generate stable Kotlin/Java and Swift/Objective-C bindings without leaking Go-specific types such as maps, channels, contexts, or complex structs.
6+
7+
## Android
8+
9+
```bash
10+
go install golang.org/x/mobile/cmd/gomobile@latest
11+
gomobile init
12+
gomobile bind -target=android/arm64,android/amd64 -o apps/vx6comms-android/app/libs/vx6mobile.aar ./mobile
13+
```
14+
15+
Targets:
16+
17+
- `android/arm64`: real modern Android devices
18+
- `android/amd64`: emulator
19+
20+
## iOS
21+
22+
```bash
23+
gomobile bind -target=ios -o apps/vx6comms-ios/VX6Mobile.xcframework ./mobile
24+
```
25+
26+
## First App Flow
27+
28+
1. `NewEngine(configPath)`
29+
2. `Init(name, listenAddr, advertiseAddr, dataDir, downloadsDir)`
30+
3. `StartNode()`
31+
4. `GenerateChatInvite()` or `AcceptChatInvite(invite)`
32+
5. `SendText(contactNodeID, text)`
33+
6. `MessagesJSON(contactNodeID)`
34+
35+
The bridge currently supports the desktop-compatible shared-secret chat envelope path. The next step is to move the full desktop X3DH/ratchet session implementation into `sdk/chat` so desktop and mobile both use the same advanced session state.

0 commit comments

Comments
 (0)