This native Kotlin and Jetpack Compose app is under development. Pair with a desktop through a QR code, an intent://pair link, or manual connection details. Review the host and certificate fingerprint before you connect. The app saves encrypted connections and opens the workspace list after authentication. Direct hosts race the optional Tailcat tunnel. All routes use pinned TLS and bearer authentication. Builds without the Tailcat AAR use direct hosts only.
app: Android application with one Compose activity. Minimum Android API: 26. Compile and target API: 37 (Android 17).core/protocol: pure Kotlin/JVM module for protocol models, sessions, and pinned transport.tailcat: optional native tunnel adapter with an unavailable implementation for builds without the AAR.gradle/libs.versions.toml: pinned library and plugin versions.
The protocol source of truth is docs/protocol. The iOS companion is the feature reference.
- Install JDK 21. Set
JAVA_HOMEto its installation directory. - Download the Android command-line tools for your operating system from Android Developers.
- Extract the tools into
$ANDROID_HOME/cmdline-tools/latest. That directory must containbin/sdkmanager. - Install the SDK packages:
export ANDROID_HOME="$HOME/Library/Android/sdk" # macOS; choose a local SDK directory on Linux
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
sdkmanager --licenses
sdkmanager "platform-tools" "platforms;android-37.0" "build-tools;36.0.0"The Gradle wrapper downloads Gradle 9.3.1. Android Gradle Plugin 9.1.1 supplies built-in Kotlin support for the app.
Run the complete gate from this repository:
./gradlew build ktlintCheck detekt testRun only the protocol tests:
./gradlew :core:protocol:testFormat Kotlin sources and Gradle scripts:
./gradlew ktlintFormatThe debug APK is app/build/outputs/apk/debug/app-debug.apk. Open this directory in Android Studio to run it on an emulator or device.
Tests cover protocol fixtures, pinned transport, pairing, session bootstrap, stores, and the offline cache. Camera scanning and live desktop pairing still need device verification.
The smoke suite uses an in-process fake transport. It covers manual pairing, workspace selection, note tasks, comment threads, and conversation sending. It captures light and dark screenshots; these are review artifacts, not pixel comparison tests.
Run the same managed Pixel 8 API 34 suite as CI:
./gradlew :app:pixel8Api34DebugAndroidTest -Ptailcat.enabled=false
python3 scripts/check-ui-screenshots.pyGradle downloads the emulator image for the host architecture. The first run can take several minutes.
CI runs the full androidTest suite across three parallel runners, without a class or method filter.
AndroidJUnitRunner assigns each test to one shard with numShards=3 and shardIndex=0, 1, or 2.
CI caches the SDK, emulator, and system image. Each runner creates its own managed AVD;
snapshots are not shared between hosts. New main pushes cancel older main runs. Pull request runs use separate groups.
The shared Compose rule disables Bluetooth only on the managed emulator and captures failures before activity teardown.
Reports are in app/build/reports/androidTests/. Screenshots are in
app/build/outputs/managed_device_android_test_additional_output/.
Each CI shard retains reports and screenshots for 14 days, including failed runs.
A final job checks the required screenshots across all shard artifacts.
The tag workflow produces unsigned AAB and APK files plus a git-cliff changelog.
The app is under development. These artifacts do not establish production readiness.
The workflow uses direct connections only (-Ptailcat.enabled=false). It does not include the private Tailcat library.
It does not publish a GitHub Release, upload to Play, sign files, or join the desktop alpha release chain.
Set appVersion in gradle.properties before each release commit.
Use MAJOR.MINOR.PATCH, with each number between 0 and 999 and no leading zeros.
Gradle uses this value as versionName.
The versionCode is MAJOR * 1000000 + MINOR * 1000 + PATCH + 1 (0.1.0 gives 1001).
Each release must increase the version. Prerelease suffixes and build metadata are not supported.
Do not reuse a version code for a different published build.
After review and approval, push a tag that matches the committed version, such as v0.1.0.
The workflow rejects a tag that differs from appVersion.
Download intent-android-v<version>-unsigned from the completed Actions run within 30 days.
The archive contains app-release.aab, app-release-unsigned.apk, and release-notes.md.
For a local build and changelog preview, run:
./gradlew :app:bundleRelease :app:assembleRelease -Ptailcat.enabled=false
git-cliff --config cliff.toml --output /tmp/android-changelog.mdInstall git-cliff 2.10.1 for the same changelog engine as CI. The configuration groups conventional commits with the monorepo conventions. The CI changelog covers the latest tag; the local command includes all history.
See the Android command-line build guide for the signing tools.
Signing remains a manual release-owner step. Keep the keystore outside the repository and use interactive password prompts. The organization must choose the key owner, secure backup, upload key, and Play App Signing policy before distribution. CI has no signing secrets. Secret provisioning and alpha-channel integration need separate approval.
- Obtain the approved keystore and alias from the release owner.
- Set
ANDROID_KEYSTOREto its absolute path andANDROID_KEY_ALIASto its alias. - Copy the unsigned AAB before signing it:
cp app/build/outputs/bundle/release/app-release.aab /tmp/intent-signed.aab
jarsigner -keystore "$ANDROID_KEYSTORE" /tmp/intent-signed.aab "$ANDROID_KEY_ALIAS"
jarsigner -verify -verbose -certs /tmp/intent-signed.aab- Align the unsigned APK, then sign and verify the aligned file:
"$ANDROID_HOME/build-tools/36.0.0/zipalign" -P 16 -f -v 4 \
app/build/outputs/apk/release/app-release-unsigned.apk /tmp/intent-aligned.apk
"$ANDROID_HOME/build-tools/36.0.0/apksigner" sign \
--ks "$ANDROID_KEYSTORE" --ks-key-alias "$ANDROID_KEY_ALIAS" \
--out /tmp/intent-signed.apk /tmp/intent-aligned.apk
"$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --verbose --print-certs /tmp/intent-signed.apk- Compare the reported certificate fingerprint with the approved release certificate.
- Install the signed APK on a test device and complete the release checks before distribution.
An AAB needs Play or bundletool to produce installable APKs. Do not use apksigner to sign an AAB.
Do not put passwords in commands, Gradle properties, logs, or Git.
The library builds the shared packages/ios/TailcatKit Go wrapper. The connection manager races direct hosts against the tunnel and closes losing routes. Browser traffic uses the winning route with the same pinned TLS and bearer authentication.
The generated AAR contains arm64-v8a, armeabi-v7a, and x86_64 libraries. The AAR and license copy stay in gitignored tailcat/libs/.
- Initialize the private iOS submodule from the monorepo with
make ensure-ios-submodule. - Install Go and JDK 21. Add
goandjavactoPATH. - Set
ANDROID_HOMEas described above. - Install the NDK:
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358"- Run the build from this repository:
./scripts/build-tailcat.sh
./gradlew build ktlintCheck detekt test -Ptailcat.enabled=trueThe script reads the Go and golang.org/x/mobile versions from TailcatKit/go.mod.
Go downloads the required toolchain through GOTOOLCHAIN (currently Go 1.27.0).
The script installs pinned gomobile/gobind tools in a temporary directory and builds a temporary source copy.
The original Go source and module files stay unchanged.
For a standalone Android checkout, set TAILCAT_SOURCE_DIR to an existing TailcatKit directory.
ANDROID_NDK_HOME and GOTOOLCHAIN can override the defaults.
The gomobile documentation describes the Android archive and architecture targets.
Gradle selects the native adapter when tailcat/libs/tailcatkit.aar exists.
Without that file, normal builds use the stub. -Ptailcat.enabled=true fails early when the file is missing.
To test the stub with an AAR present, run:
./gradlew build ktlintCheck detekt test -Ptailcat.enabled=falseThe Kotlin entry point is dev.intent.tailcat.Tailcat, which implements TunnelClientFactory.
available reports build support. createClient(token) throws TunnelUnavailableException in stub builds.
TunnelClient.ping(timeoutMs) and forward(remotePort) are blocking calls for a worker thread.
The forwarder exposes localPort on 127.0.0.1. Close the forwarder, then the client, after use or cancellation.
Closing a forwarder stops new connections. Closing its client also stops the tunnel.
The adapter forwards raw TCP. Callers must verify the pinned TLS certificate before they send bearer credentials through it.
Host unit tests cover adapter selection and unavailable errors. Native tunnel traffic still needs Android device verification.
Notes and transcripts render Mermaid fences in an offline WebView using Pele 1.8.0. The app bundles all scripts. It does not need Node, a CDN, or a network connection for diagrams. The renderer follows the app theme. Invalid, incomplete, or oversized diagrams show their original code. Diagrams support scrolling and pinch zoom; editing is not supported. Links, images, file access, and network requests are disabled inside diagram views.
To rebuild the pinned assets and run the browser fixtures:
cd scripts/vendor-mermaid
npm ci
npm run build
npx playwright install chromium
npm testCommit the generated app/src/main/assets/mermaid files with changes to the vendor lockfile.
vendor.json records the package versions and SHA-256 hashes. NOTICE.txt contains the bundled licenses.
Browser fixtures cover themes, parse failures, limits, hostile source, and offline rendering.
Android unit tests cover source encoding, local asset selection, and raw-code fallback decisions.
Native WebView gestures still need device verification.