5.1 KiB · text History b9a6d14

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

  1. Start Phoenix (mix phx.server); it binds http://localhost:4000.
  2. 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.
  3. 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 with DIOGRAMOS_PUBLIC_URL=http://10.0.2.2:4000 or the QR will point at localhost, 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/mailbox and paste it into the second field.

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.

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.