The Android client under mobile/ is a remote view onto an xd host. It now uses
the same architecture as remote desktop mode:
Android ── SSH exec stdin/stdout ── xd-host stdio
There is no mobile daemon, pairing code, bearer token, TLS listener, local IPC, on-device agent, Git process, SQLite database, or offline chat cache.
The setup screen accepts:
- hostname or Tailscale IP;
- SSH port, default
22; - SSH username;
- either an SSH password or an imported private key with an optional passphrase.
The first connection stops before SSH authentication and displays the presented SSH host-key algorithm and SHA-256 fingerprint. The user must compare it with a trusted source and explicitly confirm it. Android then reconnects and pins the exact host-key bytes and algorithm. A changed host key is fatal until the saved connection is forgotten.
Password or private-key material, connection details, and the pinned host key are stored as one AES-256-GCM record backed by Android Keystore. The app never offers a trust-anyway path after a key mismatch.
The remote command is:
exec "$HOME/.local/share/xd/runtime/v1/xd-host" stdio \
--data "$HOME/.local/share/xd"The matching stable xd host must already be installed on the remote machine. Connecting once with the desktop deploys the current host automatically. Mobile does not upload native host binaries because the phone cannot determine or ship every remote OS and architecture safely.
JSch supplies the Android SSH client. Bouncy Castle is bundled for modern EdDSA and XDH algorithms on Android versions whose platform providers do not expose them.
The shared Kotlin Multiplatform module owns JSON Lines framing, request matching, reconnect policy, tree/chat stores, terminal events, and transcript state. The Android source set supplies SSH and encrypted credential storage.
The app keeps no foreground service. Leaving it closes SSH. Returning reconnects and takes fresh snapshots because the host has no resumable event log.
The mobile client remains remote-only by design. Do not add local transport, on-device agents, an offline database, or a cache without revisiting this model.
- Docker with BuildKit
The checked-in Gradle wrapper, JDK 21, and Android SDK 35 run inside the build image. No host Gradle, JDK, or Android SDK is required.
From the repository root:
make mobile-test
make mobile-androidmobile-test builds the Docker test stage and runs the shared JVM and Android
unit suites. mobile-android also builds, signs, aligns, verifies, and exports:
dist/mobile/xd-mobile-debug.apk
Install it with:
adb install -r dist/mobile/xd-mobile-debug.apkThe rolling nightly release publishes xd-nightly-android.apk beside the Linux
and macOS artifacts. Published stable and nightly APKs require the protected CI
release key and are checked after building for a valid non-debug signature and
non-debuggable manifest. Nightly, stable, and dev builds use separate package
IDs and matching remote data channels.
APKs published before protected signing used the public debug certificate. They cannot safely update in place to a private trust root, so the protected build uses a new package ID and installs beside the legacy app. Remove the legacy app after reconnecting the protected build; Android does not migrate its saved SSH credentials automatically.
mobile/shared declares iosArm64 and iosSimulatorArm64 so common protocol and
state code cannot drift into Android-only APIs. Linux Docker builds configure but
cannot compile Apple targets. The macOS job in .github/workflows/mobile.yml is
the build authority for them.
The current SSH transport is Android-only. An iOS application will need native SSH transport and Keychain-backed credentials while preserving the same strict host-key confirmation contract.