Sign-in that cannot finish now says so, and a missing log leaf heals
**android#19 — sign-in sat for four minutes and said nothing.** The failure path was always correct: anything `authenticate` throws is reported. What it could not report was a call that never RETURNS — observed 2026-09-05 with the requests demonstrably going out and the screen unchanged. Nothing was thrown, so nothing was shown. ⚠️ Sixty seconds, not ten. Sign-in derives a key from the passphrase and then talks to two services; an emulator or an older handset is legitimately slow at the first part. This is a backstop for "never", not a latency budget. **android#6 — a failed key-log append was permanent.** `signIn` calls `registerDevice`; `restore` did not. So a device registers exactly once, and if the transparency-log append did not land — a slow write, a relay mid-restart — it stays registered with no leaf, and every peer that enforces the log refuses it forever with an error that reads as an accusation of tampering. Relaunching did not help, which is the cruel part: the one thing anybody would try was the one thing that could not work. Restore now checks for a leaf and republishes if there is none. ⚠️ Checks first. Registering unconditionally would publish twenty fresh prekeys every launch and churn the log for nothing — iOS's `registerDeviceIfNeeded` tops up only when low for the same reason. Silent on failure, because this repairs a state almost nobody is in. **android#17 — a photo sent as a file keeps its GPS.** Same as ios#69 and the same answer: the picker strips through ImagePrep and MediaScrub, `Session.sendFile` passes the vault's bytes straight through, and the send screen now says which path you are on rather than leaving the safer one looking like a shortcut.
This commit is contained in:
@@ -640,6 +640,19 @@ fun ColumnScope.PickFileToSendSheet(
|
||||
+ "same way on their phone — it doesn't stop them reading it.",
|
||||
fontSize = 12.sp, color = Palette.SecondaryText,
|
||||
)
|
||||
Spacer(Modifier.height(Space.xs))
|
||||
// 🔴 **The photo button strips location; this one does not
|
||||
// (android#17).** A picture attached from the picker is re-encoded
|
||||
// through ImagePrep and video through MediaScrub, so no metadata
|
||||
// container survives. `Session.sendFile` passes the vault's bytes
|
||||
// straight through, EXIF and all — correct for "send this file",
|
||||
// and dangerous only because nothing said which path you were on.
|
||||
Text(
|
||||
"A file is sent exactly as it is — including anything inside "
|
||||
+ "it, like where a photo was taken. The camera button "
|
||||
+ "leaves that out; this doesn't.",
|
||||
fontSize = 12.sp, color = Palette.SecondaryText,
|
||||
)
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
for (entry in entries) {
|
||||
Row(
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.flamenet.e2e.FNID
|
||||
import io.flamenet.e2e.HistoryExport
|
||||
import io.flamenet.e2e.Vault
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import io.flamenet.client.IdentityTrust
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -417,7 +418,10 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
onSuccess = { restored ->
|
||||
identity = restored.first
|
||||
_phase.value = restored.second ?: Phase.SignedOut()
|
||||
if (restored.second != null) startPolling()
|
||||
if (restored.second != null) {
|
||||
startPolling()
|
||||
republishIfMissingFromTheLog()
|
||||
}
|
||||
},
|
||||
onFailure = { failure ->
|
||||
_phase.value = Phase.VaultUnreadable(
|
||||
@@ -426,6 +430,47 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Put this device back in the transparency log if it is not in it (android#6).
|
||||
*
|
||||
* 🔴 **A device registers exactly once, at sign-in, and never again — so a
|
||||
* key-log append that did not land is permanent.** `signIn` calls
|
||||
* `registerDevice`; `restore` did not. If the append failed for any reason
|
||||
* — a transient write, a relay mid-restart — the device stays registered
|
||||
* with no leaf, and every peer that enforces the log then refuses to send
|
||||
* to it *forever*, with an error that reads as an accusation:
|
||||
* "the keys offered by this contact are not in the public log."
|
||||
*
|
||||
* ⚠️ Relaunching did not help, which is the cruel part: the one action a
|
||||
* person would try was the one that could not fix it.
|
||||
*
|
||||
* ⚠️ **Checks first, republishes only if the leaf is absent.** Registering
|
||||
* unconditionally on every launch would publish twenty fresh prekeys each
|
||||
* time and churn the log for nothing — iOS's `registerDeviceIfNeeded` tops
|
||||
* up only when low for the same reason.
|
||||
*
|
||||
* ⚠️ Silent on failure, deliberately. This is a repair for a state almost
|
||||
* nobody is in; a relay too old for `/e2e/log/device` or simply down must
|
||||
* not produce an error about a problem the person does not have.
|
||||
*/
|
||||
private fun republishIfMissingFromTheLog() = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val device = identity ?: return@runCatching
|
||||
val relay = session?.relay ?: return@runCatching
|
||||
val entries = relay.logDeviceEntries(device.deviceId)
|
||||
.optJSONArray("entries")
|
||||
if (entries != null && entries.length() > 0) return@runCatching
|
||||
|
||||
relay.registerDevice(
|
||||
device,
|
||||
device.generateOneTimePreKeys(20),
|
||||
device.generatePQOneTimePreKeys(20),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget whatever the last sign-in attempt said.
|
||||
*
|
||||
@@ -450,9 +495,32 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
_phase.value = Phase.SignedOut(busy = true)
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching { authenticate(config, handle, passphrase, register, email) }
|
||||
}
|
||||
// 🔴 **A sign-in that cannot finish has to SAY so (android#19).**
|
||||
// The failure path below was always here and always correct — it
|
||||
// reports anything `authenticate` throws. What it could not report
|
||||
// was a call that never returns: observed 2026-09-05, four minutes
|
||||
// on an unchanged screen, no error, no spinner that resolves, with
|
||||
// the requests demonstrably going out. Nothing was thrown, so
|
||||
// nothing was shown.
|
||||
//
|
||||
// ⚠️ A person reads that as the app being broken, and they are not
|
||||
// wrong. Whatever stalls underneath — a socket with no read
|
||||
// timeout, a service that accepts and never answers — the screen
|
||||
// owes them a sentence.
|
||||
//
|
||||
// ⚠️ Sixty seconds, not ten. Sign-in derives a key from the
|
||||
// passphrase and then talks to two services, and an emulator or an
|
||||
// old handset is legitimately slow at the first part. This is a
|
||||
// backstop for "never", not a latency budget.
|
||||
val result = withTimeoutOrNull(SIGN_IN_TIMEOUT_MS) {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching { authenticate(config, handle, passphrase, register, email) }
|
||||
}
|
||||
} ?: Result.failure(
|
||||
IllegalStateException(
|
||||
"The server did not answer. It may be down, or the address may be "
|
||||
+ "wrong — check it and try again."))
|
||||
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
_phase.value = it
|
||||
@@ -3459,6 +3527,17 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* How long sign-in may take before it reports failure (android#19).
|
||||
*
|
||||
* ⚠️ A backstop for "never answers", not a latency budget. Deriving the
|
||||
* account key from a passphrase is deliberately slow, and an emulator
|
||||
* or an older handset is slower still — a tight value here would turn
|
||||
* a working sign-in into a confusing error on exactly the devices least
|
||||
* able to spare the confusion.
|
||||
*/
|
||||
private const val SIGN_IN_TIMEOUT_MS = 60_000L
|
||||
|
||||
/**
|
||||
* The presence chips, in order.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user