Toggling the setting rotates the AMBER_AES_KEY Keystore key and re-encrypts every stored secret, which can take a while. While the rotation runs the screen now shows a small progress spinner with "Re-encrypting all stored keys…" and "Do not close the app or lock the screen until this finishes", and the toggle/row are disabled to prevent overlapping rotations. The work still runs on the application IOScope so it completes even if the user leaves; on completion (or failure, which now logs and reverts the switch to the persisted value) the indicator is dismissed. The two new strings are translated into all 13 shipped locales, and AGENTS.md now instructs agents to always translate new/changed strings into every locale file in the same change.
7.6 KiB
Repository instructions for Codex / OpenCode
Amber is a single-module Android app (:app, package com.greenart7c3.nostrsigner) — a Nostr event signer (NIP-46 / NIP-55). Despite top-level lib/, commonMain/, androidMain/ dirs, settings.gradle.kts includes only :app; those are not separate Gradle modules.
Toolchain
- JDK 21 required (source/target compat 21). CI and
.codex/setup.shuse Temurin 21. compileSdk = 37,minSdk = 26, R8 full mode enabled, Gradle parallel + configuration-cache.- Room schemas are exported to
app/schemasvia KSP (room.schemaLocation).
Build and validation commands
./gradlew ktlintCheck— Kotlin style check (pre-commit hook runs this)../gradlew ktlintFormat— auto-fix formatting issues../gradlew lint— Android Lint on the default variant;warningsAsErrorsis on, so any new warning fails the build (pre-commit and pre-push hooks run this). Version-freshness checks (GradleDependencyetc.) are disabled inapp/build.gradle.kts; per-path exemptions live inapp/lint.xml../gradlew test --no-daemon— JVM unit tests for all variants (pre-push hook runs./gradlew test lint).- Run one test:
./gradlew :app:testFreeDebugUnitTest --tests "com.greenart7c3.nostrsigner.SomeTest"(or--tests "*SomeTest.method"). ./gradlew assembleDebug --no-daemon— debug APK for both product flavors../gradlew assembleRelease --no-daemon— release APK; signing only activates when envSIGN_RELEASEis set andkeystore.propertiesexists. CI otherwisetouches an empty one../build.sh <version> <appName>— builds free + offline release APKs/AABs into~/release/and callsgenerate_manifest.sh.
Required order when pushing: ktlintCheck → lint → test → build (mirrored by the hooks, but run them directly; do not rely on hooks).
Releases / version bumps
When asked to "bump the version" / cut a release, do all of the following in a single change — never omit the verification block:
- Bump
versionCode(+1) andversionName(newX.Y.Z) inapp/build.gradle.kts(defaultConfig). These are the only places the version lives. - Prepend a new
## Amber X.Y.Zblock at the top ofCHANGELOG.md, summarizing the non-merge commits since the previousvX.Y.Ztag (git log vX.Y.Z..HEAD --no-merges), grouped as user-facing bullets. End the block with the standard "Download it with …" line (update thereleases/tag/vX.Y.ZURL) and the "If you like my work …" donation line. - Always include the
## Verifying the releaseblock immediately after the donation line (before the next## Amberheading). Copy it verbatim from the previous release entry and only change themanifest-vX.Y.Z.txt/manifest-vX.Y.Z.txt.sigfilenames to the new version. Do not change thegpg --recv-keyskey id, thegpg: Signature made Fri 13 Sep 2024 …block, or the surrounding prose — those are key-specific, not release-specific. This block must be part of the version-bump commit, not a follow-up. - Do not commit unless explicitly asked (per the global rule). Do not edit
build.sh— it takes the version as an argument.
Product flavors (dimension version)
| Flavor | Notes |
|---|---|
free (default) |
Online: OkHttp, Coil, kmptor, relay connectivity. Owns INTERNET/network permissions. |
offline |
No network stack. app/src/offline/AndroidManifest.xml removes INTERNET/CHANGE_NETWORK_STATE/ACCESS_NETWORK_STATE with tools:node="remove". |
benchmark |
Mirrors free network deps; applicationIdSuffix=.benchmark, versionNameSuffix=-BENCHMARK; CI builds a signed release per push for side-by-side install. |
- Guard any network-only code with
BuildFlavorChecker.isOfflineFlavor(). There is alsoBuildConfig.IS_FDROID_BUILD(false by default; the F-Droid release workflow flips it true and stripsREQUEST_INSTALL_PACKAGESviased) — use it to disable self-update / Zapstore. - Offline permissions gate:
check-offline-permissions.ymlruns./gradlew processOfflineDebugManifestand greps the merged manifest for the three network permissions. Any new dependency that leaksINTERNETetc. will fail this check — verify the offline merged manifest when adding network-capable deps.
Architecture notes (not obvious from filenames)
Three request ingestion paths all converge on Account.sign() / encrypt-decrypt:
nostrsigner:///nostrconnect://Intent →SignerActivity→IntentUtils→ approval bottom sheet.- ContentProvider IPC →
SignerProvider(synchronous,runBlocking). - NIP-46 relay kind 24133 →
NotificationSubscription→EventNotificationConsumer→BunkerRequestUtils.
Amber.kt (the Application class) is the DI container: owns applicationIOScope, the Quartz NostrClient, notificationSubscription, isStartingAppState (set during runMigrations() — wait on isStartingAppState.first { !it }), and settings.killSwitch (disconnects all relays when true).
Per-account isolation: every npub gets its own SharedPreferences (prefs_${npub}), AppDatabase (amber_db_${npub}), LogDatabase, and HistoryDatabase — all lazily cached in ConcurrentHashMaps in Amber. Decrypted keys loaded via LocalPreferences.loadFromEncryptedStorage() and cached in LargeCache.
Biometric/PIN lock (useAuth/usePin, SecurityScreen, BiometricAuthScreen) is a UI-only app-launch gate, not a signing gate. SignerProvider and the NIP-46 path never touch it; auto-accept permission rules sign silently on all three paths. Authorization for automatic signing is governed solely by the permission system (ApplicationEntity / ApplicationPermissionsEntity: rememberType, acceptUntil/rejectUntil, kind). Do not wire signing through the biometric prompt.
See CLAUDE.md for the key-files table (verified accurate against the current tree).
Conventions
- ktlint
android_studiocode style (.editorconfig); star imports effectively disabled; trailing commas allowed;@Composablefunctions exempt from the function-naming rule. RunktlintFormatrather than hand-formatting. - Translations live in
app/src/main/res/values-<locale>/strings.xml;MissingTranslationlint is intentionally disabled and the shipped locales are pinned byandroidResources.localeFiltersinapp/build.gradle.kts. - Always translate string resources. Whenever you add or change a string in
app/src/main/res/values/strings.xml, add the translated entry to every existingapp/src/main/res/values-*/strings.xmllocale file in the same change (list them withls app/src/main/res | grep '^values-', ignoringvalues-night). Do not ship English placeholders in locale files — write the actual translation, matching the existing tone/registers of that file. Escape apostrophes as\'in languages that use them (e.g. French, Italian, Turkish), exactly like neighboring entries. - Git hooks (
git-hooks/pre-commit,pre-push) are auto-installed by the rootbuild.gradle.ktsinstallGitHooktask wired into:apppreBuild. Do not rely on them as a substitute for running checks directly.
Codex Web / cloud setup
Use the committed scripts for cloud environments:
- Setup:
bash .codex/setup.sh— installs/verifies Java 21, bootstraps Android cmdline SDK, installs API 36 / build-tools 36.0.0, accepts licenses, and prewarms Gradle forfreedebug unit-test sources. - Maintenance:
bash .codex/maintenance.sh— refreshes Gradle metadata in cached containers.
Reproducibility
Dockerfile + apkdiff.py verify reproducible builds: docker build -t amber-repro --build-arg VERSION=vX.Y.Z --build-arg APK_TYPE=free-arm64-v8a . then docker run --rm amber-repro (expect APKs match!).