100 Commits
Author SHA1 Message Date
greenart7c3 10240aa63f Remove broken android-test-report-action step 2026-09-11 06:41:45 -03:00
greenart7c3 609e523b0c Update GitHub Actions to latest versions and replace archived release actions 2026-09-11 06:21:44 -03:00
greenart7c3 a414037583 Bump version to 6.6.3
6.6.2 never shipped (release bundle build was broken by AGP 9.4.0), so
its changelog entries are folded into 6.6.3 together with the AGP
revert fix.
2026-09-09 19:06:54 -03:00
greenart7c3 3382aa14a1 Revert AGP to 9.3.2 to fix release bundle failure
R8 re-serializes .kotlin_module files with ':' in the name (Kotlin 2.4
module naming) and AGP 9.4.0's new zip-entry validation rejects colons,
failing buildFreeReleasePreBundle. Fixed upstream in AGP 9.5.0
(issuetracker 543685256); pin stays until then.
2026-09-09 18:55:29 -03:00
greenart7c3 894f1daf0e Bump version to 6.6.2 2026-09-09 17:15:57 -03:00
greenart7c3 d2ba0b8836 chore: install ngit repository skill
Add repository guidance for supported coding agents.
2026-09-09 09:27:09 -03:00
greenart7c3 4c1930323c Respect start service on boot setting after app updates 2026-09-09 09:26:06 -03:00
greenart7c3 277f242405 Update AGP to 9.4.0 2026-09-09 08:32:19 -03:00
greenart7c3 6f4128c0f8 Fix offline SecurityException from WorkManager network tracking
WorkManager's persisted work database and JobScheduler jobs survive an
upgrade from the free flavor (shared applicationId). On startup,
WorkManagerInitializer reschedules that stale network-constrained work,
and WorkManager 2.11.2 tracks it via
ConnectivityManager.registerDefaultNetworkCallback, which throws
SecurityException because the offline flavor removes
ACCESS_NETWORK_STATE. The flavor guards in Amber only prevent new
enqueues and cannot stop this.

WorkManager is unused in the offline flavor (all enqueues are
flavor-guarded and ConnectivityService never starts), so strip all of
its manifest components there: WorkManagerInitializer plus
SystemJobService, SystemForegroundService, ForceStopRunnable receiver,
RescheduleReceiver and DiagnosticsReceiver, which would otherwise crash
via stale jobs or BOOT_COMPLETED after initialization is disabled.

Also guard cancelBackupApplicationsAlarm (reachable from
ApplicationsBackupScreen) with isOfflineFlavor like its siblings, and
extend check-offline-permissions.yml to fail if WorkManagerInitializer
or SystemJobService reappear in the offline merged manifest.
2026-09-09 08:32:19 -03:00
greenart7c3 ed9100f825 Bump version to 6.6.1 2026-09-04 07:22:07 -03:00
greenart7c3 f44be77180 Add tests for the permission serializer and deserializer 2026-09-04 06:20:17 -03:00
greenart7c3 573a12010a Add missing Marmot protocol event kind translations 2026-09-04 06:13:04 -03:00
greenart7c3 41a0ec3eb4 Fix parsing of permissions when kind is not present inside the json 2026-09-04 05:58:03 -03:00
greenart7c3 6871557bbc Fix rejection intent not returning the request id 2026-09-02 09:38:12 -03:00
greenart7c3 4aeaedb303 Bump version to 6.6.0 2026-08-31 07:48:25 -03:00
greenart7c3 73279c0827 refactor: extract AccountPickerRow, ProfilePictureIcon and SectionLabel to separate files 2026-08-28 12:43:43 -03:00
greenart7c3 fde80307fc Merge #ad529600: Fix account picker scroll and title visibility
nostr:nevent1qqs2655kqp2pe63dp0e9rlat20z3ljn9708savw26fyss28wsv0qt9qpz3mhxue69uhhyetvv9ujumn8d96zuer9wcjjv5kk

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5
2026-08-28 12:23:47 -03:00
greenart7c3 ad48cb0d52 Fix account picker scroll and title visibility 2026-08-28 12:13:17 -03:00
greenart7c3 3787b171fe Merge #7eb29178: feat(security): require biometric authentication to to…
feat(security): require biometric authentication to toggle the biometrics setting

nostr:nevent1qqs8av530rq448g5r8a7rajhn9r5g2zsvc34hr92jryv65g5deakvkcpz3mhxue69uhhyetvv9ujumn8d96zuer9wck9amw6

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Toggling the 'Enable biometrics' slider in the Security screen now triggers
the biometric/keyguard prompt before the switch changes state. This verifies
the biometric sensors work before activation and prevents deactivating the
setting without authorization.

Closes greenart7c3/Amber#519
2026-08-26 13:05:40 -03:00
greenart7c3 d5f67a4ad7 feat(security): require biometric authentication to toggle the biometrics setting
Toggling the 'Enable biometrics' slider in the Security screen now triggers
the biometric/keyguard prompt before the switch changes state. This verifies
the biometric sensors work before activation and prevents deactivating the
setting without authorization.

Closes greenart7c3/Amber#519
2026-08-26 11:54:32 -03:00
greenart7c3 39130f5dcb Merge #290a348d: feat(permissions): add option to pre-approve permissio…
feat(permissions): add option to pre-approve permissions under manual approval

nostr:nevent1qqszjz353hwg2g3pcmf8h6qmc49af4ce0mvj057r9zxk6pffjh30s8gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcxmzkuh

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

## What

When connecting a new app while the `Manually approve each permission` policy is selected, a new `Add permission` button lets the user pick extra permissions to be approved automatically 342200224 everything else keeps asking.

## Changes

- New `AddPermissionsSheet` bottom sheet: searchable, deduped list of the full supported permission catalog with checkbox multi-select
- Custom `sign_event` kind input (numeric-only) for kinds not in the catalog, with validation against existing permissions and in-session duplicates
- Wired into all three surfaces:
 - `LoginWithPubKey` (nostrsigner:// intent connect flow)
 - `BunkerConnectRequestScreen` (NIP-46 connect flow)
 - `EditPermission` screen 342200224 persisted as Allow / Always rows, still tunable afterwards
- 6 new strings translated in all 14 locales

## Validation

- ktlintCheck 342234205
- lintFreeDebug 342234205
- testFreeDebugUnitTest 342234205
2026-08-24 10:32:47 -03:00
greenart7c3 17bfc5dd49 feat(permissions): add option to pre-approve permissions under manual approval
When connecting a new app with the 'Manually approve each permission'
policy selected, an 'Add permission' button now lets the user pick
permissions from the full supported catalog to be approved
automatically, while everything else still asks for confirmation.

- New AddPermissionsSheet bottom sheet with search, deduped catalog
  list and an 'Add' confirmation bar
- Support for adding a custom sign_event kind not present in the
  catalog, with duplicate/existing-permission validation
- Wired into LoginWithPubKey (intent path), BunkerConnectRequestScreen
  (NIP-46 path) and the EditPermission screen (persisted as
  Allow/Always rows)
- New strings translated in all 14 locales
2026-08-24 09:57:55 -03:00
greenart7c3 f4505cae7c Merge #88343fcf: feat(kill-switch): prompt to disable when a connect re…
feat(kill-switch): prompt to disable when a connect request arrives + settings toggle

nostr:nevent1qqsgsdplelp3l6zgh95dh0mzsnhggydcqhvjhttq6ns3fqjyf7tgy7spz3mhxue69uhhyetvv9ujumn8d96zuer9wc5u5k4h

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

## What

With the kill switch enabled Amber is disconnected from all relays, so a NIP-46 connect request arriving on screen can never be answered (the response must be published to a relay).

- When a connect request is displayed while the kill switch is enabled, ask the user once whether to disable it. Confirming reuses `KillSwitchReceiver` 342200224 the same toggle path as the Applications screen banner and the notification action (flips the setting, persists it, reconnects relays).
- Wired into both connect-request UIs: the single-request screen (`BunkerSingleEventHomeScreen`) and the grouped multi-request screen (`BunkerMultiEventHomeScreen`).
- New Kill switch item in the Settings network section: title flips between Enable/Disable kill switch, error tint while enabled. Hidden on the offline flavor since it has no network stack.

## Strings

3 new resources translated in all 13 shipped locales.

## Validation

`ktlintCheck`, `lint`, `test` (all flavors) and `compileFreeDebugKotlin` all pass locally.
2026-08-24 08:22:18 -03:00
greenart7c3 185d3bbf94 feat(kill-switch): prompt to disable when a connect request arrives and add settings toggle
A NIP-46 connect request can only be answered while Amber is connected to
its relays, but with the kill switch enabled every relay connection is
dropped. When a connect request is displayed in that state, ask the user
once whether to disable the kill switch.

- New KillSwitchConnectPrompt composable reuses KillSwitchReceiver (same
  path as the Applications screen banner and the notification action) to
  flip, persist and reconnect.
- Wired into BunkerSingleEventHomeScreen (single connect request) and
  BunkerMultiEventHomeScreen (connect grouped with other requests).
- Kill switch item added to the Settings network section: title flips
  between Enable/Disable, error tint while enabled; hidden on the offline
  flavor (no network stack).
- New strings translated in all 13 locales.
2026-08-24 08:16:10 -03:00
greenart7c3 ef241b7699 Polish permission rows in EditPermission screen
- Drop the per-permission delete action; the remove-all-permissions
  dialog remains the way to clear permissions
- Localize the CONNECT permission label via toLocalizedString
- Let permission descriptions wrap fully instead of truncating to one
  line
2026-08-24 07:52:24 -03:00
greenart7c3 11c186bde3 Adapt to Quartz 1.14.0 suspend listener changes
Quartz 1.14.0 made WebSocketListener.onMessage and
RelayConnectionListener.onIncomingMessage suspend functions, which broke
compilation.

Bridge the suspend boundary in OkHttpWebSocket the same way Amethyst and
Quartz's own BasicOkHttpWebSocket do: pump incoming frames through an
unbounded Channel consumed by a Dispatchers.IO coroutine, reusing
BasicOkHttpWebSocket's exceptionHandler. Kept Amber's needsReconnect()
proxy/timeout diffing that the Quartz builtin lacks. Mark all
onIncomingMessage overrides suspend and wrap direct test invocations in
runBlocking.

Also bumps AGP to 9.3.2.
2026-08-24 07:39:49 -03:00
greenart7c3 7c43530422 Update Kotlin to 2.4.10, Gradle to 9.7.1, and dependency stack
- Gradle wrapper 9.6.1 -> 9.7.1, AGP 9.3.0 -> 9.3.1
- Kotlin 2.4.0 -> 2.4.10, KSP 2.3.5 -> 2.3.11, ktlint plugin 14.1.0 -> 14.2.0
- quartz 1.12.6 -> 1.13.1: adapt RelayAuthenticator auth callback to its
  new (relay, event, challenge) signature in Amber.kt
- Compose ui 1.11.3 -> 1.12.0
- okhttp 5.4.0 -> 5.5.0, storage 2.2.0 -> 3.0.1
- appcompat 1.7.1 -> 1.8.0, biometric-ktx 1.2.0-alpha05 -> 1.4.0-alpha02
- paging 3.5.0 -> 3.5.1, kotlinx-collections-immutable 0.5.0 -> 0.5.1
- secp256k1-kmp 0.23.0 -> 0.24.0

Validated with ktlintCheck, lint, and test.
2026-08-21 13:34:03 -03:00
greenart7c3 236fa7f234 Merge #6e30bc49: fix(profile-subscriptions): synchronize account map it…
fix(profile-subscriptions): synchronize account map iteration against concurrent removal

nostr:nevent1qqsxuv9uf8nvwg5cdfln3r82g6jjqx47kfvpgz7yxcstj4sxf5d3jvqpz3mhxue69uhhyetvv9ujumn8d96zuer9wcql9j3h

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Fixes the NoSuchElementException reported in nevent1qqszh6nmwv9nl6wt6zg8qm8azhqlhk76hemgn5us0pg4cx7twmayv6cgn4dga

updateFilters() snapshotted the accounts map (accounts.values.toList()) while closeSub() and the EOSE path concurrently removed entries. A weakly-consistent ConcurrentHashMap iterator can run off the end when the map shrinks mid-iteration, throwing NoSuchElementException out of updateFilters().

Fix: iteration and mutation of the accounts map are now mutually exclusive 342200224 updateFilters() copies the value list under synchronized(accounts) and iterates the snapshot outside the lock; all mutation sites (put in updateFilter, remove in closeSub(account), clear in closeSub()) take the same monitor. Single-key lookups stay lock-free.

Verified: ktlintCheck, lint, full test suite (all variants) green; ProfileSubscriptionTest concurrency tests 10/10 clean repeated runs.
2026-08-21 06:40:01 -03:00
greenart7c3 00a09ab090 fix(profile-subscriptions): synchronize account map iteration against concurrent removal 2026-08-21 06:35:23 -03:00
greenart7c3 09aeb733e6 Trim image and trust-score caches on memory pressure
Coil 3 registers no ComponentCallbacks of its own, so its 32MB bitmap
memory cache stayed fully resident in this long-lived signer process
even with the UI invisible and under system memory pressure.

onTrimMemory now clears the Coil memory cache on TRIM_MEMORY_UI_HIDDEN
and, additionally, TrustScoreService's cache from
TRIM_MEMORY_BACKGROUND up. Since API 34 the deprecated RUNNING_*/
MODERATE/COMPLETE levels are never delivered, so onLowMemory is
overridden as the foreground-pressure stand-in (verified against the
SDK 36 android.jar: only UI_HIDDEN and BACKGROUND are non-deprecated).

Also ignores the local trace_processor wrapper downloaded for the
heapprofd profiling session.
2026-08-21 06:01:15 -03:00
greenart7c3 591ed2324d Cache decrypted getAll results in CachingApplicationDao
heapprofd profiling showed NotificationSubscription.updateFilter()'s ~30s
relay-refresh cycle re-running ApplicationDao.getAll on the raw Room dao,
re-decrypting every application row through AndroidKeyStore (a keystore2
binder round-trip per encrypted field) forever. That path was the app's
dominant native allocator: ~15% of live native memory plus all periodic
~400KB allocation bursts (1.33MB/90s steady-state churn).

getAll is now a read-through cache: a second small LruCache keyed by
account pubKey, handing out defensive copies. Every application-table
mutation evicts the affected account's entry (or all entries when only
the app key is known); permission-only writes never do.

Call sites that could bypass the wrapper are routed through the cached
dao so the cache cannot go stale: updateFilter's read, HistoryDao's
updateLastUsed write, and ConnectivityService's reconnect write.

Re-profiled post-fix: decrypt-path stacks 0 rows/0 bytes (was 65 rows/
1.14MB per 90s); steady-state churn ~0.26MB/90s (was ~1.33MB); no
periodic bursts.
2026-08-21 06:01:02 -03:00
greenart7c3 ee745e806c Remove unused RelayDisconnectService
Dead code: the class was never scheduled and its TODO noted the new
Quartz-based Nostr client path made it obsolete.
2026-08-19 14:32:26 -03:00
greenart7c3 a49caa0881 Convert legacy Long millis overloads to kotlin.time.Duration
Replace delay/withTimeoutOrNull/debounce Long-millis calls with their
kotlin.time.Duration overloads, convert millis timeout constants to
Duration vals, and switch retryWithBackoff, the relay reconnect backoff
and the sensitive-clipboard clear delay to Duration parameters.
2026-08-19 14:04:19 -03:00
greenart7c3 abcf2f7703 Warm up tzdata and cache DateTimeFormatters off the main thread
The first ZoneId.systemDefault() call lazily mmaps the tzdata file
(ZoneInfoDb class init), which tripped StrictMode DiskReadViolation
(~169 ms) on the main thread during LazyColumn composition in
ApplicationsScreen. Mirror the Coil ImageLoader pre-warm: force the
load on applicationIOScope in Amber.onCreate, and cache the two
DateTimeFormatter instances instead of re-parsing the pattern per item.
2026-08-19 11:55:14 -03:00
greenart7c3 33221752e2 Retry flaky relay URL parsing benchmark before failing
Single-shot wall-clock timing can be inverted by JIT compilation
variance and CPU scheduling noise. Retry the measurement up to
3 times (with JIT warmup before each attempt and a short cooldown
between failed attempts) and report per-attempt timings on failure.
2026-08-19 08:12:55 -03:00
greenart7c3 347eb4848c Use subject-form when expression in EventNotificationConsumer 2026-08-19 07:58:44 -03:00
greenart7c3 47128e4192 Merge #c55e6511: Loading indicator + do-not-close warning for "require…
Loading indicator + do-not-close warning for "require unlocked device" toggle

nostr:nevent1qqsv2hn9z9yyc9m7z49s9tjmd585u7apya29k6t6ac48h6ta9w42tkgpz3mhxue69uhhyetvv9ujumn8d96zuer9wcpez6vu

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Toggling "Require unlocked device for key access" rotates the AMBER_AES_KEY Keystore key and re-encrypts every stored secret, which can take a while. This PR makes that visible:

- While rotation runs, the Security screen shows a small spinner with "Re-encrypting all stored keys342200246" plus "Do not close the app or lock the screen until this finishes" in error color.
- The row and switch are disabled during rotation to prevent overlapping rotations.
- Rotation still runs on the application IOScope (completes even if the user leaves); on failure it logs and reverts the switch to the persisted setting.
- New strings translated into all 13 shipped locales.
- AGENTS.md now instructs agents to always translate new/changed strings into every values-*/strings.xml in the same change.

Validation: ktlintCheck, compileFreeDebugKotlin, lintFreeDebug, and resource processing for free+offline flavors all pass (lintOfflineDebug has pre-existing failures also present on master).
2026-08-18 16:31:51 -03:00
greenart7c3 df74f4c6cd Show loading indicator and do-not-close warning when toggling require unlocked device
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.
2026-08-18 16:26:10 -03:00
greenart7c3 939f5b8def Bump version to 6.5.2 2026-08-18 10:08:22 -03:00
greenart7c3 6c79600a27 Merge #4a9cd287: Fix Applications screen slowness after envelope-encryp…
Fix Applications screen slowness after envelope-encrypting secrets

nostr:nevent1qqsy48xjslclznph8hy78dyart9cwurd4v3lya9mycqv3r3822nth8qpz3mhxue69uhhyetvv9ujumn8d96zuer9wccae2pp

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

The GHSA-5fjp-ghh8-wch8 envelope-encryption change made every ApplicationDao read decrypt the secret/localKey columns, and each decryption re-fetched the AMBER_AES_KEY Keystore key (3 sequential binder IPCs per call). The Applications screen paid 2 x rows of those per page plus the TEE cipher op, fully serialized 342200224 several seconds on a populated account.

Two fixes:

1. SecureCryptoHelper caches the SecretKey handle (material never leaves the TEE/StrongBox). All entry points retry exactly once with a re-fetched handle on stale-handle errors (InvalidKeyException/KeyStoreException/UnrecoverableKeyException/ProviderException), so concurrent rotateKey or the unlocked-device policy cannot wedge reads. AEADBadTagException still propagates, preserving the decryptField failure contract.

2. The Applications list now uses a dedicated ApplicationListItem projection (key, name, relays, icon, lastUsed) that never selects the encrypted columns 342200224 the screen renders neither field, so the load performs zero Keystore operations. DecryptingPagingSource removed with its only caller. The Pager is also remember(account.hexKey)-ed instead of being rebuilt on every recomposition.

Also adapts ApplicationsScreen to the NavHostControllerWrapper stability pattern used by the other screens.

Verified: ktlintCheck, lint, test (all variants), assembleFreeDebug, assembleOfflineDebug.
2026-08-18 09:48:14 -03:00
greenart7c3 f439b08abe Fix Applications screen slowness after envelope-encrypting secrets
The GHSA-5fjp-ghh8-wch8 change made every ApplicationDao read decrypt
the `secret`/`localKey` columns, and each decryption re-fetched the
AMBER_AES_KEY Keystore key: KeyStore.getInstance().load(null) +
containsAlias() + getEntry(), three sequential binder IPCs to the
keystore daemon, per call. The Applications screen paid 2 x rows of
those per page (Paging's initial load is 3 x pageSize rows), fully
serialized, plus the TEE cipher operation itself — several seconds on
a populated account.

Two independent fixes:

- SecureCryptoHelper: cache the SecretKey handle (the material never
  leaves the TEE/StrongBox; the object is a lightweight reference).
  Fast path is a volatile read; miss path double-checks under a
  synchronized block. All four public entry points run through
  withFreshKeyRetry, which retries exactly once with a re-fetched
  handle on stale-handle errors (InvalidKeyException, KeyStoreException,
  UnrecoverableKeyException, ProviderException) so concurrent key
  rotation or the opt-in unlocked-device policy cannot wedge reads.
  GCM AEADBadTagException (corrupt/wrong-key ciphertext) still
  propagates immediately, preserving the decryptField failure contract.
  rotateKey updates the cache in both of its branches.

- ApplicationDao: the Applications list now queries a dedicated
  projection (ApplicationListItem: key, name, relays, icon, lastUsed)
  that never selects the encrypted columns, so the screen performs
  zero Keystore operations regardless of row count. The screen renders
  neither secret nor localKey, so the SELECT * decrypts were pure
  overhead. DecryptingPagingSource is removed with its only caller.
  The Pager is also remember(account.hexKey)-ed: it was previously
  rebuilt on every recomposition (killSwitch / backup-warning state
  flips), re-running the initial load each time.

Also adapts ApplicationsScreen to the NavHostControllerWrapper
stability pattern used by the other screens.

Verified: ktlintCheck, lint, test (all variants), assembleFreeDebug,
assembleOfflineDebug.
2026-08-18 09:43:18 -03:00
greenart7c3 97653747b9 Merge #1ed68583: Debounce relay-status counter updates (notification ra…
Debounce relay-status counter updates (notification rate limit)

nostr:nevent1qqspa459s04qpe5h3k6m0dg2amne9fl247pxjk3w6ylnq7m4rw3pk2cpz3mhxue69uhhyetvv9ujumn8d96zuer9wc4mf4tk

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Fixes the relay-status notification (id=2) hitting Android's per-app notification rate limit during relay activity bursts.

addSent/addFailed called notify(2) synchronously for every relay event (NostrClientLoggerListener.onSent), so bursts of 7-34 notifies/sec against the ~5/sec limit were shed by NotificationManagerService and the status notification froze on stale text.

The counter path now emits Unit ticks into a MutableSharedFlow (capacity 1, DROP_OLDEST) collected with debounce(300): a burst collapses into one notification update, the last tick always renders, and tryEmit never suspends or fails on Quartz IO threads. No numeric counter, so nothing overflows or wraps no matter how long the process runs. Connection-state updates keep their existing debounce so connect/disconnect still shows promptly.

Issue: nevent1qy28wumn8ghj7un9d3shjtnwva5hgtnyv4mqqg80gw6ushd8jyj6rt8qseup7hngj0xhqa2ms3dvam60lhc760zvkyq6uts9

Validated: ktlintCheck, lint, :app:testFreeDebugUnitTest.
2026-08-18 08:27:54 -03:00
greenart7c3 1ab593b827 Debounce relay stat counter updates to avoid notification rate limit
AmberRelayStats.addSent/addFailed called notify(id=2) synchronously for
every relay event, driven by NostrClientLoggerListener.onSent. Bursts of
relay traffic (7-34 notifies/sec vs Android's ~5/sec per-app limit) made
NotificationManagerService shed the updates, freezing the status
notification on stale text, and wasted a full BigTextStyle build plus
Binder IPC per dropped event.

Route the counter path through a Unit event flow
(MutableSharedFlow, capacity 1, DROP_OLDEST) collected with
debounce(300): a burst collapses into one notification update and the
last tick always renders. No numeric counter, so nothing overflows or
wraps no matter how long the process runs; tryEmit never suspends or
fails on Quartz IO threads. Connection-state updates keep their
existing debounce so connect/disconnect still shows promptly.

Fixes ngit issue nevent1qy28wumn8ghj7un9d3shjtnwva5hgtnyv4mqqg80gw6ushd8jyj6rt8qseup7hngj0xhqa2ms3dvam60lhc760zvkyq6uts9
2026-08-18 08:21:23 -03:00
greenart7c3 6151a0247b Merge #c91c82d5: Warm account cache at app start to fix switch-accounts…
Warm account cache at app start to fix switch-accounts button

nostr:nevent1qqsvj8yz64w9mdtzee0kzrh6kr56c8fxw9jxgcrq24etkta5xtshstspz3mhxue69uhhyetvv9ujumn8d96zuer9wc8pjy2a

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

## Problem

Since 96ee4107 (GHSA-8844-q5vh-9j8f, I3) made `loadFromEncryptedStorageSync` lazy-load only the requested npub, the account switcher in approval screens only ever shows the current account.

The full-account cache warm was expected to happen in `reloadApp()`, but that runs from `ConnectivityService.onCreate()` which is started via an AlarmManager PendingIntent 342200224 a race with composition 342200224 and the service does not exist in the offline flavor at all. By the time `LoginWithPubKey` / `BunkerConnectRequestScreen` snapshot `allCachedAccounts()` inside `remember {}`, only one account is cached.

## Fix

- Extract the eager all-account load from `reloadApp()` into a new `warmAccountCache()`.
- Call it from `AccountStateViewModel.tryLoginExistingAccount()` before any UI state is emitted 342200224 the deterministic app start covering both entry activities and both flavors. On switch/logout it is a no-op (cache hits) or a reload of the remaining accounts.
- `reloadApp()` behavior unchanged; `loadFromEncryptedStorageSync` stays lazy for SignerProvider / NIP-46, so the I3 hardening is preserved.

## Validation

`ktlintCheck`, `test` (all variants) and `lint` all pass.
2026-08-18 07:45:46 -03:00
greenart7c3 d98a42fd3b Merge #33634760: Fix LazyColumn duplicate-key crash in activity history…
Fix LazyColumn duplicate-key crash in activity history screens

nostr:nevent1qqsrxc68vrmt2ypjyswald6ex53rcndanf60jtxqahy8ezqdv3y4wkcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc6m5qe7

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

History rows are inserted continuously (every sign/encrypt, including auto-accepted NIP-46 requests). Room pages are independent LIMIT/OFFSET queries, so an insert between the initial load and a scroll-triggered append shifts the offsets and the same row can be returned in two loaded pages at once. Keying LazyColumn items by the database id then crashes with IllegalArgumentException: Key \"X\" was already used when scrolling down the activity screen.

Fall back to positional keys, which are always unique. Rows are stateless (ActivityRow produceState is keyed on the entity itself), so behavior is unchanged.

Fixes the reported crash: IllegalArgumentException: Key \"5329\" was already used (6.3.0-FREE, scrolling the activity screen).
2026-08-18 07:41:24 -03:00
greenart7c3 f4ee7ddf3e Warm account cache at app start to fix switch-accounts button
Since 96ee4107 made loadFromEncryptedStorageSync lazy-load only the
requested npub, the full account cache was expected to be populated by
reloadApp(). But reloadApp() runs from ConnectivityService.onCreate(),
which is started via an AlarmManager PendingIntent — a race with the UI
— and the service doesn't exist in the offline flavor at all. By the
time the approval bottom sheet composes and snapshots
allCachedAccounts() inside remember {}, only the current account is
cached, so the switch-accounts button only ever offered one account.

Extract the eager all-account load from reloadApp() into
warmAccountCache() and call it from AccountStateViewModel before any UI
state is emitted — the deterministic app start covering both entry
activities and both flavors. reloadApp() behavior is unchanged, and
loadFromEncryptedStorageSync stays lazy for the SignerProvider / NIP-46
paths, preserving the I3 hardening from GHSA-8844-q5vh-9j8f.
2026-08-18 07:38:24 -03:00
greenart7c3 16559c8e8e Bump version to 6.5.1 2026-08-14 13:49:04 -03:00
greenart7c3 41a37aacdc Re-encrypt Room application secrets during Keystore key rotation
Toggling "require unlocked device for key access" rotates the
AMBER_AES_KEY Keystore key, but rotateKey only re-encrypted the
DataStore secrets (account keys, PIN, WebDAV password). The
envelope-encrypted `secret`/`localKey` columns of the per-account
`application` Room tables were left under the old key, making every
NIP-46 connection row undecryptable after the toggle: getBySecret
lookups miss and localKey turns into undecryptable ciphertext.

rotateKey now stages those rows before deleting the old key and
rewrites them re-encrypted with the new key via the new
updateEncryptedColumnsRaw DAO query (ciphertext-in/ciphertext-out,
empty sentinel preserved, no schema change). Columns that fail
old-key decryption are written back verbatim so rotation never
destroys data it cannot recover, matching the decryptField failure
contract in ApplicationEntityCrypto.kt.
2026-08-14 13:23:13 -03:00
greenart7c3 50ec57b5bc Fix crash in EditPermission due to invalid localKey 2026-08-14 11:17:22 -03:00
greenart7c3 0f313eb882 Bump version to 6.5.0 2026-08-14 08:15:03 -03:00
greenart7c3 4c733d6b47 Fix CI test failures and linter violations 2026-08-14 07:44:14 -03:00
greenart7c3 24a22590a3 Translate new string resources to supported languages 2026-08-14 07:07:20 -03:00
greenart7c3 482d6bb2c8 fix(I4): warn on explicit ws:// non-onion relays (GHSA-8844-q5vh-9j8f)
network_security_config.xml permits cleartext globally (deliberate, for
local/onion relays). EditRelaysDialog auto-prefixes ws:// only for .onion /
private IPs but accepts user-typed explicit ws:// URLs, which then carry
kind-24133 NIP-46 traffic (E2E ciphertext + metadata over cleartext). Add
an in-app warning below the relay text field when the user explicitly
types ws:// for a non-onion / non-private-network host, so cleartext relays
are opt-in and informed. Update the network_security_config comment to
document the deliberate global scope and the in-app warning.
2026-08-14 07:07:19 -03:00
greenart7c3 96ee410767 fix(I3): lazy-load account keys on cache miss; zeroize on logout (GHSA-8844-q5vh-9j8f)
Two defense-in-depth reductions in decrypted-key cache exposure:

1. loadFromEncryptedStorageSync previously warmed the ENTIRE account cache
   (runBlocking over all saved accounts) on any cold cache / size mismatch,
   decrypting every account's private key process-wide on the first
   SignerProvider / NIP-46 lookup. Lazy-load only the requested npub; direct
   lookups always know which account they want. The UI account switcher's
   allCachedAccounts() path is still populated by reloadApp() at startup.

2. updatePrefsForLogout dropped the cache entry but left the Account's
   KeyPair.privKey ByteArray intact in heap until GC. Best-effort zeroize
   the key bytes in place before removing the reference.

reloadApp keeps its eager all-account load (the UI account switcher depends
on allCachedAccounts() being populated synchronously); this change targets
the other eager-decrypt-all surface and the logout zeroization gap.
2026-08-14 07:07:19 -03:00
greenart7c3 2ee9b3ba0b fix(I2): parseRememberType fails closed to NEVER for unknown values (GHSA-8844-q5vh-9j8f)
parseRememberType mapped unknown screenCode values to RememberType.ALWAYS.
Enforcement never reparses the stored value (IntentUtils uses the
acceptUntil/rejectUntil computed at write time); the only caller is the
EditPermission settings dropdown default. Defense-in-depth: map unknown ->
NEVER so a corrupted/unrecognized stored policy cannot grant indefinite
auto-accept.
2026-08-14 07:07:19 -03:00
greenart7c3 d61c4649f4 fix(I1): authorize before decrypt in SignerProviderQuery (GHSA-8844-q5vh-9j8f)
The V2/V3 DECRYPT path (SignerProviderQuery.kt:288-322) invoked the
private key to decrypt + classify plaintext BEFORE checking whether the
caller had any permission. Results were withheld on denial, so there was
no direct disclosure, but an unauthorized caller triggered key usage and
timing/error oracles.

Add a pre-authorization gate: for non-encrypt operations, look up the
signPolicy and ANY relevant permission row (generic type, V2 classified
content types, NIP-level grant, or V3 kind-scoped/all-kinds) before
touching the key. If none exists and signPolicy != 2 (accept-all),
short-circuit with the same null cursor the existing no-permission path
already returns — externally identical behavior, but the private key is
never invoked for a totally unknown caller.

Classified-specific grant semantics (e.g. auto-accept DECRYPT_EVENT only)
are preserved: ANY relevant row counts as pre-authorized, so we still
proceed to decrypt + classify and the final per-type check honors
accept/reject precisely. Auto-reject rows still pass the gate by design,
since the user has already made an explicit decision for that caller.

EventNotificationConsumer's pre-decrypt (:190-196) wraps the NIP-46
envelope and is protocol-forced (the request type lives inside it);
deferring its pre-sign (:231-232) preview requires refactoring the
approval UI's signedEvent contract and is left for a follow-up. The
advisary rates I1 as info; this change closes the primary surface it
calls out (the specific line range 288-322).
2026-08-14 07:07:19 -03:00
greenart7c3 fd15d87bce fix(L4): set FLAG_SECURE on raw nsec/ncryptsec QR surfaces (GHSA-8844-q5vh-9j8f)
QrCodeDialog (raw nsec) and QrCodeScreen (Route.QrCode, ncryptsec/nsec)
render raw key material as a QR code but omitted FLAG_SECURE. Capture
requires a user-initiated, device-auth-gated reveal, so low severity, but
the protection exists elsewhere (SeedWordsPage, RandomPinInput) and was
omitted here. Set FLAG_SECURE on the activity window for the lifetime of
each composable, mirroring the existing screens, to block screenshots and
screen recording.
2026-08-14 07:07:19 -03:00
greenart7c3 e78d0d651f fix(L3): redact key material from crash reports before persistence (GHSA-8844-q5vh-9j8f)
ReportAssembler previously appended e.toString()/cause.toString() and the
full stack trace verbatim. Parser/crypto exceptions can embed attacker-
controlled request data (hex keys, npub/nsec bech32, base64 ciphertexts).
Add a redaction step that replaces 64-char hex keys, Nostr bech32 prefixes
and long base64 blobs with placeholders before the report is written to
internal storage by UnexpectedCrashSaver.

Adds ReportAssemblerTest covering hex/bech32/base64/cause-chain redaction
and non-sensitive content preservation.
2026-08-14 07:07:19 -03:00
greenart7c3 ee975e9833 fix(L2): redact raw relay frames from debug logs (GHSA-8844-q5vh-9j8f)
NostrClientLoggerListener.onSent/onIncomingMessage previously passed raw
cmdStr/msgStr to AmberLog.d (gated only by BuildConfig.DEBUG). Frames can
carry NIP-46 envelopes and DM/gift-wrap ciphertexts. Log only the command/
message type and the wire byte size, enough for debugging without leaking
payloads.
2026-08-14 07:07:18 -03:00
greenart7c3 286f8416e5 fix(L1): opt-in unlocked-device requirement for Keystore key (GHSA-8844-q5vh-9j8f)
AMBER_AES_KEY (AES-256-GCM, optional StrongBox) was built without
setUnlockedDeviceRequired, so any code in Amber's process could decrypt
every account key while the screen was locked — the PIN/biometric lock
is UI-only. The advisory suggests an opt-in user-auth-bound key for at
least interactive signing, acknowledging the trade-off with background
NIP-46 signing.

Add an opt-in toggle on the Security screen: when enabled, the Keystore
key is generated with setUnlockedDeviceRequired(true) (API 28+), so the
TEE refuses key use while the device is locked. This does NOT prompt for
biometrics per operation — it only refuses use while locked, so
foreground NIP-46 signing keeps working.

The flag is set at key-generation time, so toggling requires key
rotation: decrypt all stored secrets (per-account DataStore
NOSTR_PRIVKEY/SEED_WORDS, app DataStore PIN, WebDAV password) with the
old key, delete it, generate a new one with the new policy, and
re-encrypt everything. The rotation holds SecureCryptoHelper's mutex for
the entire operation and uses internal non-locking cipher methods +
raw DataStore helpers to avoid mutex reentrancy (Kotlin's Mutex is not
reentrant). The rotation is safe-by-design: all secrets are decrypted
before the old key is deleted.
2026-08-14 07:07:18 -03:00
greenart7c3 e7cba54ae0 Merge upstream/master and resolve database version conflict by bumping to 20 2026-08-14 06:50:48 -03:00
greenart7c3 98dfca7095 Envelope-encrypt NIP-46 connection secrets at rest (GHSA-5fjp-ghh8-wch8)
The per-account Room database (amber_db_<npub>) stored two NIP-46 secret
values as cleartext TEXT columns: the bunker connection `secret` and the
`localKey` — the latter being a full Nostr private key. Anyone with access
to the app's internal storage (rooted device, privilege-escalating malware,
or a future bug exporting the DB) could recover these values and bypass the
Keystore protection the main account nsec enjoys (CWE-312).

Fix: envelope-encrypt both columns with the existing Keystore-backed AES-256-GCM
key (SecureCryptoHelper) before Room persistence, and decrypt on read, so every
existing consumer continues to see the plaintext values it already expects.

- SecureCryptoHelper: add non-suspend encryptBlocking/decryptBlocking so
  Migration.migrate() and getByKeySync() can call them without a runBlocking
  bridge; suspend variants now delegate to the blocking implementations.
- ApplicationEntityCrypto.kt (new): encryptForStorage/decryptFromStorage
  mappers + DecryptingPagingSource. Sentinel rule: empty values stay "" at
  rest (matches the WebDAV password idiom in LocalPreferences.kt:661-681),
  preserving `WHERE localKey != ''` enumeration in NotificationSubscription
  and the `localPubKey` derivation on empty localKey.
- ApplicationDao: split methods touching `secret`/`localKey` into Room-
  generated `*Raw` (encrypted columns) and default-method wrappers that
  apply the mappers. `getBySecret` rewritten to decrypt and filter in Kotlin
  (random GCM IV breaks `WHERE secret = :secret`).
- CachingApplicationDao: add delegating `*Raw` overrides so the decorator
  still instantiates; cache logic unchanged.
- AppDatabase: add MIGRATION_18_19 (in-place envelope-encrypt of existing
  plaintext rows via compiled statement + transaction; empty values stay
  empty). Bump @Database version to 19.
- Backup/restore: no changes — ApplicationBackup.buildPayload reads via the
  wrapped DAO (plaintext) and the JSON is already NIP-44 encrypted by the
  account key; restore goes through the wrapped insert (auto-encrypts).
- Tests: new androidTest ApplicationEntityCryptoTest covers round-trip,
  raw-column-ciphertext assertion, empty sentinel, localPubKey derivation,
  getAllWithLocalKey filter, getBySecret (hit/miss/empty),
  insertApplicationWithPermissions, getAll, and the MIGRATION_18_19 row
  re-encryption. Requires a device/emulator (AndroidKeyStore unavailable
  under JVM test).
- New room-testing androidTestImplementation dependency.

Verified: ktlintCheck, lint (no issues), testFreeDebugUnitTest (0 failures),
compileFreeDebugAndroidTestKotlin, assembleFreeDebug, assembleOfflineDebug,
and the offline merged manifest check (no INTERNET/ACCESS_NETWORK_STATE/
CHANGE_NETWORK_STATE permissions leaked).
2026-08-10 14:55:51 -03:00
greenart7c3 e73a407bb3 Fix GHSA-h9fv-9247-3582: NIP-46 freshness and replay protection 2026-08-10 11:29:29 -03:00
greenart7c3 24a5e2071c Fix relay-auth whitelist authorizing any requester (GHSA-vx4h-56qj-wcp7)
The auth whitelist auto-accepted kind-22242 (NIP-42) signing for any
caller: whitelistAutoAccept bypassed the per-requester permission check
in the SignerProvider/NIP-46 query path, letting any installed app or
bunker client silently obtain relay-auth signatures for whitelisted
relays (confused deputy, CWE-863).

The whitelist is now only a relay constraint: non-whitelisted relays
still auto-reject, but membership no longer grants signing. Kind-22242
requests always require a requester-scoped SIGN_EVENT permission
(relay-specific or wildcard grant) or fall through to the user prompt,
matching the whitelist's documented behavior and the approval UIs.

Adds SignerProviderQueryTest covering: no silent signing without a
grant, silent signing with relay-scoped/wildcard grants, auto-reject of
non-whitelisted relays before any permission lookup, and unchanged
empty-whitelist behavior.
2026-08-10 08:48:50 -03:00
greenart7c3 d46974733a Fix LazyColumn duplicate-key crash in activity history screens
History rows are inserted continuously (every sign/encrypt, including
auto-accepted NIP-46 requests). Room pages are independent LIMIT/OFFSET
queries, so an insert between the initial load and a scroll-triggered
append shifts the offsets and the same row can be returned in two loaded
pages at once. Keying LazyColumn items by the database id then crashes
with 'IllegalArgumentException: Key X was already used' when scrolling
down the activity screen.

Fall back to positional keys, which are always unique. Rows are
stateless (ActivityRow's produceState is keyed on the entity itself),
so behavior is unchanged.
2026-08-07 12:15:06 -03:00
greenart7c3 65fea7d6b3 Merge #27ad4e74: Bump version to 6.4.0
nostr:nevent1qqsz0t2ww3uuw89jzqmzndz4rq6f26kq3kdnt4ek3wnw86w8s0wwn9gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcyy9z6n

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

- Bump versionCode 197 -> 198, versionName 6.3.0 -> 6.4.0 in app/build.gradle.kts
- Add Amber 6.4.0 release notes to CHANGELOG.md (replaces 6.3.0 block, kept as latest only)
- Create docs/changelogs/6.4.0.md with the full release notes and verifying block
- Add 6.4.0 entry to docs/changelogs/README.md index
2026-08-07 07:09:18 -03:00
greenart7c3 4b4acfd880 Bump version to 6.4.0
- Bump versionCode 197 -> 198, versionName 6.3.0 -> 6.4.0 in app/build.gradle.kts
- Add Amber 6.4.0 release notes to CHANGELOG.md (replaces 6.3.0 block, kept as latest only)
- Create docs/changelogs/6.4.0.md with the full release notes and verifying block
- Add 6.4.0 entry to docs/changelogs/README.md index
2026-08-07 07:02:35 -03:00
greenart7c3 cfaccda931 Merge #15e2902d: Ignore platform finalizer-watchdog crashes in crash re…
Ignore platform finalizer-watchdog crashes in crash reports

nostr:nevent1qqsptc5s9kvxe6ahr4yxj6cc7ur8sgg03tu7epn0hyz05s7m248uedqpz3mhxue69uhhyetvv9ujumn8d96zuer9wc5kul9c

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

BinderInternal$GcWatcher.finalize() TimeoutExceptions are a known
AOSP issue raised by the finalizer watchdog on the FinalizerDaemon
thread. They contain no app frames and cannot be caught or prevented
by app code, so the report only nags users with an unactionable
"send crash report?" prompt.

Extend the existing OOM junk filter in UnexpectedCrashSaver to drop
TimeoutExceptions whose message or stack matches the finalizer
watchdog path. App-thrown TimeoutExceptions and all other crashes
are still reported.
2026-07-31 07:05:04 -03:00
greenart7c3 86ab6e86e6 Ignore platform finalizer-watchdog crashes in crash reports
BinderInternal$GcWatcher.finalize() TimeoutExceptions are a known
AOSP issue raised by the finalizer watchdog on the FinalizerDaemon
thread. They contain no app frames and cannot be caught or prevented
by app code, so the report only nags users with an unactionable
"send crash report?" prompt.

Extend the existing OOM junk filter in UnexpectedCrashSaver to drop
TimeoutExceptions whose message or stack matches the finalizer
watchdog path. App-thrown TimeoutExceptions and all other crashes
are still reported.
2026-07-31 06:55:48 -03:00
greenart7c3 0985d4171c Merge #3c71aec4: Add 113 event kinds with localized labels
nostr:nevent1qqsrcudwcn87h26c788r8zy48z23yd0629uaxcm9ejy69a6a5petdcspz3mhxue69uhhyetvv9ujumn8d96zuer9wcg8g99j

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Sync the supported sign_event kinds with quartz 1.13.1's KindNames
registry: 108 kinds that had no localized label in Amber, plus the
Concord kinds (13302, 20013, 20014), NIP-51 git repository bookmarks
(10018) and NIP-53 room presence (10312).

Each kind now has:
- a localized label in all 13 shipped locales,
- a sign_event branch and supportedKindNumbers entry so it can be
  approved and filtered in the activity screens,
- a kindsByNip mapping where the kind belongs to a numbered NIP,
  enabling NIP-level permission grouping.
2026-07-30 17:20:15 -03:00
greenart7c3 9f2a88a19c Add 113 event kinds with localized labels
Sync the supported sign_event kinds with quartz 1.13.1's KindNames
registry: 108 kinds that had no localized label in Amber, plus the
Concord kinds (13302, 20013, 20014), NIP-51 git repository bookmarks
(10018) and NIP-53 room presence (10312).

Each kind now has:
- a localized label in all 13 shipped locales,
- a sign_event branch and supportedKindNumbers entry so it can be
  approved and filtered in the activity screens,
- a kindsByNip mapping where the kind belongs to a numbered NIP,
  enabling NIP-level permission grouping.
2026-07-30 17:08:12 -03:00
greenart7c3 6e11c75616 Merge #02be4e74: Fix NegativeArraySizeException crash in relay subscrip…
Fix NegativeArraySizeException crash in relay subscriptions

nostr:nevent1qqsq90jwwn6z4na0s9lmf694j08f72655p390u00z05uma3e9fjztxcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc65tc6r

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

ProfileSubscription and NotificationSubscription kept their subscription
state in plain LinkedHashMaps that are mutated from UI/coroutine threads
(updateFilter, closeSub, updateFilters) while relay I/O threads iterate
them in onIncomingMessage. Iterating a LinkedHashMap's values/entries
while another thread structurally modifies it crashes with
NegativeArraySizeException on Android, e.g. from
ProfileSubscription.updateFilters via checkForNewRelaysAndUpdateAllFilters.

- Use ConcurrentHashMap for all shared maps in both subscriptions and
  concurrent key sets for the per-subscription relay sets
- Replace containsKey + get(!!) with an atomic getOrPut in
  NotificationSubscription.updateFilter

Add unit tests: functional coverage for the subscribe/unsubscribe
lifecycle and concurrency regression tests that reliably reproduce the
race on the pre-fix implementation.
2026-07-29 11:43:43 -03:00
greenart7c3 651b4c3d1d Merge #438a7e60: Add Compose previews for the multi-event approval scre…
Add Compose previews for the multi-event approval screens

nostr:nevent1qqsy8zn7vrj2slsmmx92pp4naumg6lq3z4gp52g4un2fcug3cyzu6gcpz3mhxue69uhhyetvv9ujumn8d96zuer9wcjughul

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Add light/dark previews for BunkerMultiEventHomeScreen and
IntentMultiEventHomeScreen with sample request groups (connect,
sign event, NIP-44 decrypt) so group headers, options and cards
all render.

Guard side effects that reach the Amber singleton, which does not
exist in the preview renderer:
- ProfileSubscriptionEffect: skip the relay subscription in
  inspection mode (also fixes every preview embedding SigningAs)
- BunkerMultiEventHomeScreen: skip the encrypted-storage/DB lookup
- rememberAppDisplayInfo: fall back to the package name when any
  part of the PackageManager lookup fails instead of crashing
2026-07-29 11:42:19 -03:00
greenart7c3 1056fb39db Fix NegativeArraySizeException crash in relay subscriptions
ProfileSubscription and NotificationSubscription kept their subscription
state in plain LinkedHashMaps that are mutated from UI/coroutine threads
(updateFilter, closeSub, updateFilters) while relay I/O threads iterate
them in onIncomingMessage. Iterating a LinkedHashMap's values/entries
while another thread structurally modifies it crashes with
NegativeArraySizeException on Android, e.g. from
ProfileSubscription.updateFilters via checkForNewRelaysAndUpdateAllFilters.

- Use ConcurrentHashMap for all shared maps in both subscriptions and
  concurrent key sets for the per-subscription relay sets
- Replace containsKey + get(!!) with an atomic getOrPut in
  NotificationSubscription.updateFilter

Add unit tests: functional coverage for the subscribe/unsubscribe
lifecycle and concurrency regression tests that reliably reproduce the
race on the pre-fix implementation.
2026-07-29 07:06:07 -03:00
greenart7c3 7180632a19 Add Compose previews for the multi-event approval screens
Add light/dark previews for BunkerMultiEventHomeScreen and
IntentMultiEventHomeScreen with sample request groups (connect,
sign event, NIP-44 decrypt) so group headers, options and cards
all render.

Guard side effects that reach the Amber singleton, which does not
exist in the preview renderer:
- ProfileSubscriptionEffect: skip the relay subscription in
  inspection mode (also fixes every preview embedding SigningAs)
- BunkerMultiEventHomeScreen: skip the encrypted-storage/DB lookup
- rememberAppDisplayInfo: fall back to the package name when any
  part of the PackageManager lookup fails instead of crashing
2026-07-28 16:33:09 -03:00
greenart7c3 932434440d git: ignore the .ai-jail directory
Add the .ai-jail entry to the project's root .gitignore file. This ensures that local AI-related configuration or temporary metadata directories are not tracked by version control.
2026-07-28 11:41:45 -03:00
greenart7c3 1305e3c605 Merge #7811888b: Redesign multi-request approval screen with Approve/De…
Redesign multi-request approval screen with Approve/Deny toggles (#497)

nostr:nevent1qqs8syvg3ddd7l3cwmfcuwy8f6hdags9usa706kprs506fm9cxsujjcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc06qjx5

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Replace the confusing "select + Approve/Discard" interaction on the
multi-event approval screen with an explicit Approve/Deny model:

- Each request card and each group header now has an Approve/Deny
  segmented toggle (approve = primary, deny = error).
- Removed the top "Approve/Deny all" toggle and the per-group
  TriStateCheckbox; group headers carry their own Approve/Deny toggle.
- Replaced the two bottom buttons (Approve selected / Discard selected)
  with a single Confirm button. Confirm commits every per-request
  decision: approve signs and (when remembered) persists an accept rule;
  deny rejects and (when remembered) persists a deny rule, preserving
  existing deny-always behavior.
- Bunker path now sends a proper bunker error response for denied
  requests instead of leaving the client to time out.
- Relabeled the per-group remember control to "Remember my choice for"
  since it now covers both approve and deny rule persistence.
- Fixed deny toggle contrast: selected text now uses onError for the
  deny segment instead of hardcoded black on dark-red.

AmberToggles gains an optional indicatorColor and selectedTextColor;
ToggleOption takes a selectedTextColor param. Removed unused
discard_all/approve_all strings across all locales.

Closes #497
2026-07-28 11:39:42 -03:00
greenart7c3 f98fc2b9c4 Translate new approval strings (approve, confirm, remember_my_choice_for)
Add the three strings introduced by the Approve/Deny redesign to all
13 supported locales (de, es, fr, in, it-rIT, ja, ko-rKR, pt-rBR, ru,
th, tr, vi-rVN, zh), matching the base values/strings.xml ordering.
2026-07-27 15:52:52 -03:00
greenart7c3 1549da8838 Redesign multi-request approval screen with Approve/Deny toggles (#497)
Replace the confusing "select + Approve/Discard" interaction on the
multi-event approval screen with an explicit Approve/Deny model:

- Each request card and each group header now has an Approve/Deny
  segmented toggle (approve = primary, deny = error).
- Removed the top "Approve/Deny all" toggle and the per-group
  TriStateCheckbox; group headers carry their own Approve/Deny toggle.
- Replaced the two bottom buttons (Approve selected / Discard selected)
  with a single Confirm button. Confirm commits every per-request
  decision: approve signs and (when remembered) persists an accept rule;
  deny rejects and (when remembered) persists a deny rule, preserving
  existing deny-always behavior.
- Bunker path now sends a proper bunker error response for denied
  requests instead of leaving the client to time out.
- Relabeled the per-group remember control to "Remember my choice for"
  since it now covers both approve and deny rule persistence.
- Fixed deny toggle contrast: selected text now uses onError for the
  deny segment instead of hardcoded black on dark-red.

AmberToggles gains an optional indicatorColor and selectedTextColor;
ToggleOption takes a selectedTextColor param. Removed unused
discard_all/approve_all strings across all locales.

Closes #497
2026-07-27 15:40:51 -03:00
greenart7c3 ca21a9cd53 Add LeakCanary to debug builds
Adds the LeakCanary dependency to the version catalog and includes it as a debugImplementation in the app module to help detect memory leaks during development.
2026-07-15 16:32:56 -03:00
greenart7c3 bc45c572eb Upgrade Gradle to 9.6.1 and AGP to 9.3.0
Updates the project build tools and configuration:
* Upgrade Gradle wrapper from 9.5.1 to 9.6.1.
* Upgrade Android Gradle Plugin (AGP) from 9.2.1 to 9.3.0.
* Enable `org.gradle.tooling.parallel` in `gradle.properties` for faster syncs in supported Gradle versions.
* Refresh `gradlew` and `gradlew.bat` scripts to align with modern Gradle templates, including switching to `-jar` execution for the wrapper and adding SPDX license identifiers.
* Add download retry settings to `gradle-wrapper.properties`.
2026-07-15 10:40:32 -03:00
greenart7c3andClaude Opus 4.8 e6a7e6d1af Only use fullscreen layout when there is a single bunker request
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:24:19 -03:00
greenart7c3 b3d420819e Add privacy mode setting to disable logs and activity stats
Adds a privacy mode toggle in the Security screen that disables logging
and activity statistics. When enabled, existing logs and activity history
are wiped immediately and future writes are blocked at the DAO layer.
Translated into all 13 supported locales.
2026-07-06 17:34:48 -03:00
greenart7c3 f58d26ab98 Bump version to 6.2.3 and update changelog 2026-07-01 11:23:03 -03:00
greenart7c3 4772009bc0 Fix settings section header contrast in light theme
Use onSurfaceVariant instead of primary (yellow) for section header text,
which is unreadable on a light/white background.
2026-07-01 09:36:02 -03:00
greenart7c3 0d107904ad Expand AGENTS.md with toolchain, flavors, architecture, and CI gotchas 2026-07-01 09:28:02 -03:00
greenart7c3 c30e180363 Add error handling to bunker permission parsing
Wrap the bunker permission parsing logic in a `try-catch` block and use `mapNotNull` to prevent crashes when encountering malformed permission strings. Parsing errors are now logged via `AmberLog` and the invalid permissions are skipped.
2026-06-24 09:02:07 -03:00
greenart7c3andClaude Opus 4.8 e88891c13a chore: bump dependency versions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:12:28 -03:00
greenart7c3 7ad501fb9e Use requesterId directly in SignerProviderQuery
This change removes the redundant `packageName` local variable and replaces its occurrences with `requesterId` throughout the `SignerProviderQuery` class.

The refactoring affects:
- Permission and sign policy lookups in `permDao`.
- History and log entry creation for `HistoryEntity` and `LogEntity`.
- Account existence checks and logging of operation errors.
2026-06-19 05:47:43 -03:00
greenart7c3andClaude Opus 4.8 01b0fd5c19 Add event_kind_30618 translations
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:33:13 -03:00
greenart7c3 16c8bbbfda Add support for event kind 30618
This change updates the `Permission` model to include a mapping for Nostr event kind 30618, allowing it to return a localized description.
2026-06-17 09:48:54 -03:00
greenart7c3andClaude Opus 4.8 e21cef4011 Fix release update check missing file subscription on EOSE race
Each incoming relay message was dispatched into its own coroutine, so
the release EOSE could run before the release EVENT had populated
pendingFileEventIds. onReleaseEose() then saw an empty list and called
finishCheck() instead of opening the file subscription, so no update was
ever found.

Route messages through a FIFO channel consumed by a single coroutine so
the release EVENT is always processed before its EOSE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:29:56 -03:00
greenart7c3andClaude Opus 4.8 d72b16cec6 Build per-account databases atomically to stop a connection leak
getDatabase/getLogDatabase/getHistoryDatabase used a non-atomic
check-then-put on their ConcurrentHashMaps. Since AppDatabase.getDatabase
(and the Log/History equivalents) builds a fresh RoomDatabase on every
call, two racing callers could each build one; the loser of the put() was
dropped without close() and its SQLiteConnection was reported by
StrictMode's CloseGuard (LeakedClosableViolation) when finalized.

Switch the three getters to computeIfAbsent, matching dao(), so each npub
builds exactly one database.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:56:15 -03:00
greenart7c3andClaude Opus 4.8 64f81e941f Call fastFallback reflectively so the offline flavor compiles
fastFallback is an OkHttp 5 API, but the offline flavor compiles the
shared HttpClientManager against OkHttp 4 (pulled in transitively by
Coil, since okhttp itself is freeImplementation-scoped), which broke
:app:compileOfflineDebugKotlin.

Invoke fastFallback(false) via reflection and no-op when it's absent.
OkHttp 4 has no fast fallback and already connects sequentially, so
skipping it there is the correct behavior — not just a compile fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:01:32 -03:00
greenart7c3andClaude Opus 4.8 d345993be5 Tag SOCKS proxy sockets via interceptor + sequential connect
The EventListener approach didn't cover the relay path: relay connections
are WebSockets, and RealWebSocket.connect rebuilds the client with
eventListener(EventListener.NONE), so the listener never fired. With Tor
on, those WebSockets also use a SOCKS proxy, whose socket OkHttp builds
via Socket(proxy) — bypassing the SocketFactory too. So neither hook
tagged the relay sockets and the UntaggedSocketViolation persisted.

Application interceptors do survive the WebSocket client rebuild. Pair a
TaggingInterceptor with fastFallback(false) on proxied clients:
SequentialExchangeFinder then connects synchronously on the call thread —
the thread the interceptor already tagged — so the SOCKS socket fd is
tagged when it's created. fastFallback is left on for direct clients
(a single localhost SOCKS route gains nothing from Happy Eyeballs), which
keep relying on TaggedSocketFactory.

Restore TaggedSocketFactory (direct/HTTP routes) and TaggingInterceptor,
drop the WebSocket-blind TrafficStatsEventListener.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:40:53 -03:00
greenart7c3andClaude Opus 4.8 1318f0ae60 Tag sockets via OkHttp EventListener to cover proxied routes
The SocketFactory-based tagging missed SOCKS proxy (Tor) connections:
ConnectPlan.connectSocket builds those sockets with Socket(proxy)
directly, bypassing the configured SocketFactory, so the raw fd was
created untagged on the TaskRunner thread and tripped StrictMode's
UntaggedSocketViolation. The TaggingInterceptor didn't help either — it
runs on the call/dispatch thread, not the connect thread.

Replace TaggedSocketFactory, TaggedDns, and TaggingInterceptor with a
single TrafficStatsEventListener that sets the thread stats tag in
dnsStart/connectStart. Those callbacks fire on the exact thread that then
opens the socket, for every route type (direct, HTTP, and SOCKS), so one
listener covers them all. Wire it into the shared root client and the
WebDav client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:13:00 -03:00
greenart7c3andClaude Opus 4.8 1a40be3efa Build OkHttp clients off the main thread, drop StrictMode juggling
OkHttp creates the default SSLSocketFactory eagerly in the OkHttpClient
constructor, which Android10Platform reports as a ~55ms StrictMode slow
call (newSSLContext). The first build was being forced onto the main
thread by setDefaultUserAgent (called from setContent), tripping the
detector.

Make the setDefault*/clearProxy mutators invalidate the cached clients
instead of eagerly rebuilding them, so the expensive build happens
lazily inside getHttpClient — only ever reached from network/background
threads. With no main-thread build, the buildSafe wrapper that
temporarily relaxed the StrictMode thread policy is no longer needed, so
remove it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:52:28 -03:00
greenart7c3andClaude Opus 4.8 4005c0ba5b Tag all sockets with TrafficStats to satisfy StrictMode
Route every OkHttp client through TaggedSocketFactory/TaggedDns and a
TaggingInterceptor so DNS lookups and socket creation set a thread stats
tag, avoiding StrictMode's untagged-socket violations. Tag the Tor proxy
probe sockets in Amber too, and build clients under a relaxed thread
policy. Warm up the HTTP clients at startup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 10:40:28 -03:00
greenart7c3 bcb83f1a5d Add support for event kind 39701 (Public web bookmark)
This change adds a human-readable string for Nostr event kind 39701 and updates the `Permission` model to return this localized string when mapping event kinds to descriptions.
2026-06-03 15:50:10 -03:00
greenart7c3 ce7e54a5f1 Update Kotlin to 2.4.0 and Gradle to 9.5.1
This commit updates the project's build configuration and several core dependencies:
- **Build Tools**: Gradle updated from 9.5.0 to 9.5.1.
- **Languages/Plugins**: Kotlin updated to 2.4.0.
- **Libraries**:
    - Compose UI: 1.11.2
    - Paging Common: 3.5.0
    - Quartz: 1.11.0
    - JNA: 5.19.0
- **Test Dependencies**:
    - Coroutines Test: 1.11.0
    - MockK: 1.14.11
2026-06-03 11:07:51 -03:00