Diogramos Android client
Native Android client for Diogramos. Talks to the JSON API at
/api/v1/... and the realtime channel at /socket. Sister app to
../../waxx/android — same Compose shell, OKLCh palettes and B612-mono
typography, so the two feel like one product family.
The design doc and phase plan live in docs/android.md. Shipped so far: phase 1b — the scaffold and both auth paths. The canvas list is a placeholder that proves the credentials work; rendering arrives in phase 2.
Stack
| Layer | Choice |
|---|---|
| Language / UI | Kotlin 2.0 · Jetpack Compose · Material 3 |
| HTTP | Retrofit · OkHttp · kotlinx.serialization |
| Realtime | com.github.dsrees:JavaPhoenixClient (phase 3) |
| Storage | EncryptedSharedPreferences (AES via the Android Keystore) |
| Scanner | com.journeyapps:zxing-android-embedded (ScanContract) |
| Build | Gradle 9.0 · AGP 8.5.2 · JDK 17 |
| minSdk | 26 (Android 8) · targetSdk 34 |
Single-activity, single-module, no DI framework — dependencies are constructed inline. Adding Hilt when the screen count justifies it is a fine follow-up.
Building
cd android
./gradlew :app:assembleDebug # → app/build/outputs/apk/debug/app-debug.apk
./gradlew :app:testDebugUnitTest # JVM unit tests, no emulator needed
./gradlew :app:installDebug # onto a running emulator or device
The SDK location comes from local.properties (sdk.dir=…), which is
not committed. The first build downloads Gradle and a few hundred MB of
dependencies; later ones are seconds.
JDK: build with 17–21. AGP 8.5 does not support JDK 25, and lint throws on it with only the version string as the error message, so a build on a modern default JDK fails for reasons that have nothing to do with this code:
JAVA_HOME=/path/to/jdk21 ./gradlew :app:assembleDebug
Running against a local dev server
- Start Phoenix (
mix phx.server); it bindshttp://localhost:4000. - The emulator reaches the host as
http://10.0.2.2:4000. A physical device on the same LAN uses the host’s LAN IP. - Pair with one of:
- QR — sign in on the web, Settings → Connected devices →
Generate, scan the code. The server picks the URL it encodes from
DIOGRAMOS_PUBLIC_URL/X-Forwarded-*/Host, so for emulator testing start Phoenix withDIOGRAMOS_PUBLIC_URL=http://10.0.2.2:4000or the QR will point atlocalhost, which on the emulator is the emulator. - Magic link — enter the same URL and your email, then read the
link out of the dev mailbox at
/dev/mailboxand paste it into the second field.
- QR — sign in on the web, Settings → Connected devices →
Generate, scan the code. The server picks the URL it encodes from
TokenStore upgrades http://<real domain> to https:// on save and
load; IPs, localhost and unqualified hostnames are left alone, so the
LAN and emulator paths keep working.
Auth
Two paths, because they fail in different situations:
- QR pairing needs a logged-in browser and physical access to its screen. No mail server, no App Links — which is why it leads on self-hosted installs.
- Magic link is the answer for “I’ve only ever used the phone”. It needs the server to send mail, and App Links for the seamless version.
Either yields a bearer token, and the backend treats them identically
afterwards. The token is verified with GET /api/v1/users/me before
being stored: a scanned QR can name a server this phone cannot reach,
and storing it unverified produces an app that fails later with no clue
why.
App Links
The https://<host>/m/<token> intent filter is autoVerify="true",
which needs /.well-known/assetlinks.json on the server declaring this
app’s SHA-256 signing fingerprints (debug and release). Without it the
link still works — Android falls back to the default-handler chooser,
and the paste box on the magic-link screen always works. The host comes
from the appLinksHost placeholder in app/build.gradle.kts, currently
diogramos.example.com.
Read the fingerprints with:
keytool -list -v -keystore ~/.android/debug.keystore \
-storepass android -alias androiddebugkey | grep SHA256
keytool -list -v -keystore release.jks -alias diogramos | grep SHA256
Themes
ui/theme/AppTheme.kt carries all nine daisyUI themes from
assets/css/app.css, token for token, with keys matching
Diogramos.Themes.all/0.
It holds every token in Themes.color_tokens/0, not just the ones
Material needs, because a canvas stores colors as token names and
resolves them against the canvas’s own theme. That is what lets a diagram
authored in forest render in forest colors while the app chrome stays
in whatever the user picked — and it is why these values are copied from
the CSS rather than eyeballed.
Tests
app/src/test/ holds plain JVM tests — no emulator, no Robolectric. That
is why PairPayload parses with java.net.URI instead of
android.net.Uri: the pairing parser is the one place where a mistake
silently stores the wrong server, so it is worth being able to test it
directly.
Not yet shipped
Everything from phase 2 on: canvas rendering, the realtime channel, editing, export, the launcher icon, and the release/F-Droid pipeline. See docs/android.md.