A revoked device wipes itself

Mirror of the iOS change: a 410 from the poll cancels polling, destroys
the vault (Android's sign-out is already a full wipe), and lands on the
sign-in screen with a sentence that says what happened and what signing
in again means.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-28 17:52:07 -04:00
co-authored by Claude Fable 5
parent b6557d6409
commit 3ab8e6c641
@@ -1614,9 +1614,20 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
if (current != null) {
// A story arriving must appear without the user pulling
// to refresh; it is filed by `receive()` and read back here.
val threads = withContext(Dispatchers.IO) {
runCatching { current.receive() }.getOrDefault(emptySet())
val received = withContext(Dispatchers.IO) {
runCatching { current.receive() }
}
// 🔴 410 from the poll: this device's registration is gone
// — revoked from another phone, or expired by the sweep.
// Wipe and sign out with an honest sentence, rather than
// hammering a mailbox that no longer exists while sitting
// on ciphertext the account's owner asked to be gone.
val failure = received.exceptionOrNull()
if (failure is RelayException.Http && failure.status == 410) {
signOutRevoked()
return@launch
}
val threads = received.getOrDefault(emptySet())
// 🔴 **Retry the outbox on every tick.** A message to
// somebody with no registered device waits here rather than
// being lost or dumped on the sender to remember — see
@@ -1705,6 +1716,22 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
const val CALL_SETUP_POLL_MS = 1_000L
}
/// The relay said this device was removed. Same teardown as a sign-out —
/// which on Android is already a full vault destroy — plus the sentence
/// that says what happened and what signing in again means.
private fun signOutRevoked() = viewModelScope.launch {
polling?.cancel()
polling = null
session = null
withContext(Dispatchers.IO) { runCatching { KeystoreVault.destroy(getApplication()) } }
identity = null
vault = KeystoreVault.open(getApplication())
_phase.value = Phase.SignedOut(
error = "This device was removed from the account, so its local "
+ "messages were cleared. Signing in again registers it as a "
+ "new device.")
}
fun signOut() = viewModelScope.launch {
polling?.cancel()
polling = null