dm calls mvp: phase 5.35 polish: cancel notif on null session, redact token-leak in JSON parse log, guard drain from resume race
This commit is contained in:
parent
2c1063f4c9
commit
6000596c54
2 changed files with 28 additions and 11 deletions
|
|
@ -36,11 +36,11 @@ import java.util.concurrent.Executors;
|
||||||
* - On non-2xx or exception: leave tombstone; flusher drains on next resume.
|
* - On non-2xx or exception: leave tombstone; flusher drains on next resume.
|
||||||
*
|
*
|
||||||
* Null-session edge case (fresh reinstall + first push before first login):
|
* Null-session edge case (fresh reinstall + first push before first login):
|
||||||
* we deliberately do NOT cancel the notification so the ring keeps playing.
|
* we cannot send the decline at all — there's no access token, and the
|
||||||
* The user will unlock, MainActivity will boot, and the existing JS-path
|
* JS-path can't cover us either (a logged-out client has no Matrix session
|
||||||
* consumer (pendingCallActionAtom) can still handle the decline through the
|
* to call sendRtcDecline against). Cancelling the notification is the only
|
||||||
* `pushNotificationActionPerformed` listener — silently dropping the ring
|
* feedback we can give; leaving the ring would trap the user on a call they
|
||||||
* here would be worse UX than preserving it. See techdebt §5.35 Item 6.
|
* can't accept or decline until the A-side times out. See techdebt §5.35.
|
||||||
*
|
*
|
||||||
* Note on idempotency: the flusher's retry generates a new txnId, so on a
|
* Note on idempotency: the flusher's retry generates a new txnId, so on a
|
||||||
* split-success-fail sequence (receiver HTTP timed out, flusher succeeds)
|
* split-success-fail sequence (receiver HTTP timed out, flusher succeeds)
|
||||||
|
|
@ -89,11 +89,15 @@ public class CallDeclineReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
final String sessionJson = prefs.getString(SESSION_KEY, null);
|
final String sessionJson = prefs.getString(SESSION_KEY, null);
|
||||||
if (sessionJson == null) {
|
if (sessionJson == null) {
|
||||||
// Fresh reinstall / logged-out state. Do NOT cancel the notification:
|
// Fresh reinstall / logged-out: no access token, no Matrix client.
|
||||||
// the ring keeps playing, user unlocks, MainActivity boots, and the
|
// We can't send m.call.decline and neither can the JS-path (no
|
||||||
// JS-path consumer handles the decline. Silent-drop here would leave
|
// session to decline from). Cancel the notif so the user isn't
|
||||||
// A-side spinning to no-answer timeout with no feedback on B.
|
// stuck on a ring they can't action. Skip the tombstone — a
|
||||||
Log.w(TAG, "onReceive: no session in prefs, leaving ring intact");
|
// retry without a session would be equally impotent.
|
||||||
|
Log.w(TAG, "onReceive: no session in prefs, cancelling notif without HTTP");
|
||||||
|
if (notifTag != null && notifId != -1) {
|
||||||
|
NotificationManagerCompat.from(appContext).cancel(notifTag, notifId);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +108,9 @@ public class CallDeclineReceiver extends BroadcastReceiver {
|
||||||
accessToken = session.optString("accessToken", null);
|
accessToken = session.optString("accessToken", null);
|
||||||
baseUrl = session.optString("baseUrl", null);
|
baseUrl = session.optString("baseUrl", null);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
Log.e(TAG, "onReceive: prefs JSON parse failed", t);
|
// Do NOT pass the Throwable — JSONException.getMessage() embeds
|
||||||
|
// the malformed input, which here contains the access token.
|
||||||
|
Log.e(TAG, "onReceive: prefs JSON parse failed: " + t.getClass().getSimpleName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (accessToken == null || accessToken.isEmpty() || baseUrl == null || baseUrl.isEmpty()) {
|
if (accessToken == null || accessToken.isEmpty() || baseUrl == null || baseUrl.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,18 @@ export const usePendingDeclinesFlusher = (): void => {
|
||||||
if (!isNativePlatform()) return undefined;
|
if (!isNativePlatform()) return undefined;
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
let inFlight = false;
|
||||||
let resumeHandle: { remove: () => Promise<void> } | undefined;
|
let resumeHandle: { remove: () => Promise<void> } | undefined;
|
||||||
|
|
||||||
|
// Mount + an immediate `resume` (e.g. receiver fired, app foregrounded
|
||||||
|
// right after the hook remounts) can both call drain() before the first
|
||||||
|
// run finishes its per-key `get → sendRtcDecline → remove` loop. Without
|
||||||
|
// this lock both passes observe the same tombstone and fire parallel
|
||||||
|
// sendRtcDecline requests — benign at the server (idempotent on the
|
||||||
|
// rel event_id) but it doubles the timeline noise.
|
||||||
const drain = async () => {
|
const drain = async () => {
|
||||||
|
if (inFlight) return;
|
||||||
|
inFlight = true;
|
||||||
try {
|
try {
|
||||||
const { Preferences } = await import('@capacitor/preferences');
|
const { Preferences } = await import('@capacitor/preferences');
|
||||||
const { keys } = await Preferences.keys();
|
const { keys } = await Preferences.keys();
|
||||||
|
|
@ -60,6 +69,8 @@ export const usePendingDeclinesFlusher = (): void => {
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* preferences import failed — non-fatal */
|
/* preferences import failed — non-fatal */
|
||||||
|
} finally {
|
||||||
|
inFlight = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue