Lint reported 113 warnings; this fixes them and turns on warningsAsErrors so new ones fail the build. The pre-commit hook now runs `ktlintCheck lint` and the pre-push hook runs `test lint`. Fixes: - Replace "..." with the typographic ellipsis in every locale's strings.xml - Remove unused resources: psbt_amount_sats and enable_applications_backup strings, white/amber/gray colors, ic_launcher_background_white drawable - Remove launcher bitmap fallbacks (background/foreground/monochrome/round and density ic_launcher.webp): minSdk 26 always resolves the adaptive icon - Move ic_qrcode.png to drawable-nodpi - Shrink ic_tor from a declared 512dp to 24dp (only ever drawn at 20dp) - Make modifier the first optional parameter of RemoteAppIcon - Use the lambda Modifier.offset overload for the animated toggle indicator - Replace deprecated Icons.Default.Article with the AutoMirrored version - Drop a redundant when branch, opt in to resetReplayCache's experimental API Suppressed with a stated reason: ScopedStorage (legacy backup import), InsecureBaseConfiguration (local/onion ws:// relays), PluralsCandidate, VectorPath/VectorRaster (artwork), and ObsoleteSdkInt for mipmap-anydpi-v26 (AAPT cannot resolve the launcher icon from plain mipmap-anydpi). Version freshness checks are disabled so hooks stay deterministic over time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZCeWRzzk4dxptDiEZyXjM
5.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & development commands
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build (requires signing keystore)
./gradlew ktlintCheck # Kotlin style check (also runs on every git commit via pre-commit hook)
./gradlew ktlintFormat # auto-fix Kotlin style issues
./gradlew lint # Android Lint, warnings fail the build (runs on commit and push via hooks)
./gradlew test --no-daemon # unit tests (also runs on every git push via pre-push hook)
./build.sh # builds both offline and free release variants to ~/release/
Git hooks are auto-installed via the root build.gradle.kts preBuild task — no manual setup needed.
Build flavors
| Flavor | Purpose |
|---|---|
free (default) |
Online variant with full networking (OkHttp, Coil, relay connectivity) |
offline |
No network stack; use BuildFlavorChecker.isOfflineFlavor() to guard network code |
Architecture
Request ingestion — three paths
External apps and relays reach the signer through three distinct paths:
nostrsigner://Intent →SignerActivity→ parsed byIntentUtils→ shown as bottom sheet- ContentProvider IPC →
SignerProvider→ synchronous signing viarunBlocking - NIP-46 relay events (kind 24133) →
NotificationSubscription→EventNotificationConsumer→BunkerRequestUtils
All three paths converge on Account.sign() / encrypt/decrypt methods backed by NostrSignerInternal.
Global state — Amber.kt
Amber is the Application class and acts as the DI container. Key singletons it owns:
applicationIOScope—CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler), used for all background workclient: NostrClient— the Quartz Nostr relay clientnotificationSubscription— keeps the NIP-46 filter alive in the backgroundprofileSubscription— per-account, throttled one-shot metadata fetch; first fetches the user's NIP-65 relay list (kind 10002) and saves it locally, then fetches the metadata (kind 0) from the default profile relays plus the saved user relays; started/stopped by the composables that display each account viaProfileSubscriptionEffect(not app-wide)isStartingAppState: MutableStateFlow<Boolean>— set totrueduringrunMigrations(); code that must wait for startup callsisStartingAppState.first { !it }settings.killSwitch— when true, all relays are disconnected; checked before every relay operation
Per-account isolation
Every npub gets its own:
SharedPreferencesfile (prefs_${npub})AppDatabase(amber_db_${npub}) — apps + permissionsLogDatabase— operation logsHistoryDatabase— request history
All databases are lazy-loaded and cached in ConcurrentHashMaps in Amber. Account data (including decrypted keys) is loaded via LocalPreferences.loadFromEncryptedStorage() and cached in LargeCache.
Permission system
Permissions are stored in ApplicationEntity + ApplicationPermissionsEntity (Room). Each permission entry has:
rememberType— auto-accept, auto-reject, or always-askacceptUntil/rejectUntil— time-bound grantskind— event-kind-specific rules
Before showing the approval UI, all three ingestion paths query the database; if a matching auto-accept rule exists, signing proceeds silently.
Biometric / PIN lock is UI-only
The biometric/PIN prompt (useAuth / usePin in AmberSettings, configured in SecurityScreen) is only an app-launch UI gate, not a signing gate. It is rendered by BiometricAuthScreen, which is invoked exclusively from the two UI entry points — MainActivity and SignerActivity — to unlock the app's screens before any approval bottom sheet is shown.
It does not protect the signing operations themselves:
- ContentProvider IPC (
SignerProvider) and NIP-46 relay events (EventNotificationConsumer→BunkerRequestUtils) never touchBiometricAuthScreen,Biometrics,useAuth, orusePin. They sign in the background based purely on the permission database. - When an auto-accept rule matches, all three paths sign silently without triggering the biometric/PIN prompt — including
nostrsigner://intents, which auto-finish before the UI is interacted with.
In other words, the lock controls who can open and navigate the app UI; it does not stand between a request and Account.sign(). Authorization for automatic signing is governed solely by the permission system (rememberType / acceptUntil / kind).
Key files
| File | Purpose |
|---|---|
Amber.kt |
Application singleton, Nostr client, relay connectivity |
LocalPreferences.kt |
Account/settings persistence, encrypted key storage |
IntentUtils.kt |
Parses nostrsigner:// URIs, creates IntentData objects |
SignerProvider.kt |
ContentProvider IPC signing |
BunkerRequestUtils.kt |
NIP-46 protocol handling, relay responses |
AccountStateViewModel.kt |
Auth state, account switching, toast notifications |
ConnectivityService.kt |
Foreground service, network monitoring, relay reconnection |
BiometricAuthScreen.kt |
UI-only app-launch lock (biometric/PIN); not a signing gate |
Biometrics.kt |
Wraps BiometricPrompt / keyguard credential prompt |
SecurityScreen.kt |
Toggles useAuth / usePin and the re-prompt interval |