Native Android client for the Hadron knowledge-memory platform, and the counterpart of hadron-ios. v1 scope: sign in with your Hadron account and search across your memories.
app/— the app module (Kotlin, Jetpack Compose / Material 3, min SDK 26).hadronkit/— the shared client stack: OAuth 2.1 PKCE sign-in, Keystore-backed secure storage, GraphQL client. A direct port of hadron-ios'HadronKitSwift package, kept a separate module so the UI can't reach around it into raw HTTP.gradle/libs.versions.toml— the version catalog; the single place versions live.
./gradlew :app:assembleDebugOr open the project in Android Studio and run. local.properties (with
sdk.dir) is generated by Studio and gitignored; create it by hand for a
headless build.
Release builds are minified with R8:
./gradlew :app:bundleReleaseThe upload key never lives in the repo. keystore.properties, *.jks and
*.keystore are all gitignored; with none of them present the release build
stays unsigned and still succeeds, so a fresh clone can run
assembleRelease and lintRelease without any secrets. A Play upload needs a
signed bundle, though.
Create the upload key once — keep the .jks and its passwords in a password
manager, because losing them means you can no longer update the app unless
you're on Play App Signing and request a key reset:
keytool -genkeypair -v -keystore hadron-upload.jks -alias hadron-upload -keyalg RSA -keysize 4096 -validity 10000Then cp keystore.properties.example keystore.properties and fill it in.
storeFile resolves relative to the repo root. CI can skip the file and set
HADRON_KEYSTORE_FILE, HADRON_KEYSTORE_PASSWORD, HADRON_KEY_ALIAS and
HADRON_KEY_PASSWORD instead.
keyPassword is the same value as storePassword. Since JDK 9 keytool
writes PKCS12 keystores by default — even when the file is named .jks — and
PKCS12 has no separate key password. There is no second secret to create.
Two guards, because an unsigned artifact that builds successfully is only discovered at upload time:
keystore.propertiespresent but with a missing or blank value fails configuration, naming the offending keys.- A
storeFilethat doesn't exist fails at:app:validateSigningRelease.
With no keystore.properties and no env vars at all, the release build is
unsigned and succeeds — that's the fresh-clone case, so assembleRelease and
lintRelease stay runnable without secrets.
The app signs in via Hadron's OAuth 2.1 authorization server
(srv.hadronmemory.com): discovery → dynamic client registration (client id
cached in the secure store) → PKCE S256 authorization in a Chrome Custom Tab
(provider choice: Apple, GitHub, Google, or an email magic link) → token
exchange. The issued hdr_user_* key is a long-lived bearer; there is no
refresh token — a 401 drops the app back to signed-out.
Because Android can kill the process while the browser is in front, the flow is
split around a durable pending-authorization record rather than an in-memory
continuation (see OAuthService). The email magic link, which completes in the
system browser after a detour through the mail app, gets home the same way: via
the manifest's com.hadronmemory.android intent-filter.
The token is sealed with an AES-256-GCM key in the AndroidKeyStore and excluded
from both cloud backup and device-to-device transfer — the Android equivalent of
the iOS ThisDeviceOnly posture.
Owner-side, none of it in this repo:
- Play Console app record for
com.hadronmemory.android; upload key or Play App Signing. - Data safety form: email address, name, user ID; purpose app functionality;
no tracking; no third-party sharing. Mirrors
hadron-ios/Hadron/PrivacyInfo.xcprivacy. - Account deletion: Play requires in-app initiation and a web deletion URL.
In-app lives under the account menu → Manage Account → portal
/app/account; declarehttps://hadronmemory.com/app/accountas the deletion URL. - Store listing assets live in
store/. Note the two have opposite alpha requirements — easy to get backwards:play-icon-512.png— 512×512, 32-bit PNG with alpha. The iOS source (hadron-ios/…/AppIcon.appiconset/icon-1024.png) is 1024×1024 8-bit RGB with no alpha, so it fails both counts and the Console offers to crop it, which would chop the artwork rather than fix it. The committed file is that source box-downsampled 2:1 with an opaque alpha channel added.play-feature-graphic-1024x500.png— 1024×500, 24-bit PNG, no alpha. Required to publish; the App Store has no equivalent, so there was nothing to port. Regenerate withstore/make-feature-graphic.py.screenshots/— five phone shots at 1080×1920, 24-bit PNG. Play caps aspect at 2:1, so the stock emulator's 1080×2400 (2.22:1) is rejected; capture on an AVD built from thepixel_2profile.adb screencapalso emits RGBA, which Play rejects, so the shots are converted. Recapture withstore/capture-screenshots.sh(see its header — it expects the app already signed in).
- Review access: the demo account / bypass code from hadron-server#810 works verbatim.