The block carried seven comment lines over four of code, against 0.17 for
the file as a whole. Most of it restated the identifiers: enabled =
canHandleBack does not need a sentence explaining that Back is intercepted
while there is state to unwind.
What is left is the part that is not derivable. That enabled is a variable
rather than true is a predictive-back decision, and finish() being reachable
at all only makes sense once you know the flag trails the state it mirrors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26
BackHandler's enabled flag trails the state it mirrors by a coroutine
dispatch and a recomposition. A second Back press inside that window finds
the handler still enabled while handleBackPressed() already has nothing to
unwind, and ignoring its result consumed the press instead of letting the
system act on it. The callback the handler replaced did forward it.
Falls back to finish() on an unhandled press, which is what the dispatcher
reached before once no enabled callback consumed it.
Reported by Codex review on #912.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26
The chat branch of OnboardingFlowScreen built an OnBackPressedCallback and
called addCallback(this, ...) from inside a composable body. Composable
bodies re-run on recomposition, and that function observes eight
MainViewModel StateFlows, so every Bluetooth, location, or loading change
registered another callback. They were bound to the activity, so none were
released until onDestroy.
Each accumulated callback also multiplied the work of an unhandled Back
press: the callback disabled itself, re-dispatched, and the next one down
consulted ChatViewModel again. Five recompositions meant one Back press
consulted it five times before reaching the system.
BackHandler keeps a single registration across recompositions and disposes
it when the branch leaves composition. Driving its enabled flag from
ChatState.canHandleBack also retires the disable/re-dispatch/re-enable
dance: with nothing to unwind the handler is simply disabled, so the
dispatcher falls through to the system.
canHandleBack mirrors the branches of ChatViewModel.handleBackPressed and
is covered by unit tests, since the handler being enabled and the press
being consumed have to agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26
If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.
A relay that fails is retried on an exponential backoff, then abandoned
for the lifetime of the process. Nothing brings it back: the relay layer
registers no connectivity callback, the periodic subscription validator
only repairs subscriptions on sockets that are already open (and returns
immediately when connectedRelayCount is 0, which is exactly the state
after an outage), and connect() runs once from NostrClient.initialize().
The remaining paths that reset reconnectAttempts are a manual retry, a
Tor state change, and a successful open.
Two ways a relay died permanently:
- Any error whose message mentioned DNS returned before scheduling
anything at all. "Unable to resolve host" is what this device reports
when it simply has no network, so a moment in a tunnel killed every
relay at once, with no retry ever.
- Otherwise the schedule stopped at MAX_RECONNECT_ATTEMPTS. With
INITIAL=1s and MULTIPLIER=2 that is nine waits totalling about eight
and a half minutes, after which the relay was dead. MAX_BACKOFF_INTERVAL
was unreachable: attempt 9 asks for 256s and attempt 10 gave up, so the
five-minute ceiling the constant defines never applied to anything.
Let the backoff saturate at MAX_BACKOFF_INTERVAL and keep retrying there.
A name-resolution failure now backs off like any other error. Steady state
costs one connection attempt per relay per five minutes; the previous
behaviour cost the user every internet DM, delivery receipt and geohash
channel until they noticed and restarted the app.
The schedule moves into RelayReconnectPolicy so it is unit-testable
without OkHttp or a Context.