From 8fe3d0d28d5f15643c1737809c5f1773cb8f9f94 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 12 Sep 2026 16:59:10 -0700 Subject: Don't trust the first distributor on the phone — three were installed. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ntfy went onto a real phone and the handshake ran: our app broadcast REGISTER, ntfy answered with an endpoint (https://ntfy.sh/up…), the app stored it, and the web app took it off the launch URL and kept it. That was the link I'd said was unproven. It also turned up something the sandbox never would. The phone had THREE apps answering the distributor action — Element's and Yakihonne's internal ones alongside ntfy — and both of those exist to serve their own app, not anyone else's. Picking the first would have registered with a distributor that will never hand us an endpoint, and there'd be nothing to show for it. The spec's answer is to let the user choose, and this app has no settings screen to choose in, so it works through them instead: ask one, and if no endpoint has arrived by the next launch, ask the next. A refusal moves on immediately. Whoever answers is remembered for good. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx --- android/app/src/main/java/io/coinos/app/Push.java | 38 +++++++++++++++++++--- .../src/main/java/io/coinos/app/PushReceiver.java | 7 +++- 2 files changed, 39 insertions(+), 6 deletions(-) (limited to 'android/app/src') diff --git a/android/app/src/main/java/io/coinos/app/Push.java b/android/app/src/main/java/io/coinos/app/Push.java index 1c4bee5..925693a 100644 --- a/android/app/src/main/java/io/coinos/app/Push.java +++ b/android/app/src/main/java/io/coinos/app/Push.java @@ -52,6 +52,10 @@ public final class Push { private static final String KEY_DIST = "distributor"; private static final String KEY_ENDPOINT = "endpoint"; private static final String KEY_SENT = "endpointSent"; + private static final String KEY_TRY = "tryIndex"; + private static final String KEY_TRY_AT = "tryAt"; + /** How long a distributor gets to answer before we try the next one. */ + private static final long TRY_MS = 60_000; static SharedPreferences prefs(Context c) { return c.getSharedPreferences(PREFS, Context.MODE_PRIVATE); @@ -81,17 +85,41 @@ public final class Push { return out; } + /** + * Which distributor to ask. + * + *

Not simply the first one installed. Several apps embed a distributor + * for their own use and export it anyway — a phone here had three, and the + * first two were Element's and Yakihonne's internal ones, which will never + * hand an endpoint to anybody else. The spec's answer is to let the user + * choose, but this app has no settings screen to choose in, so it works + * through them instead: ask one, and if no endpoint has arrived by the next + * launch, ask the next. Whoever answers is remembered for good. + */ static String distributor(Context c) { - String saved = prefs(c).getString(KEY_DIST, null); + SharedPreferences p = prefs(c); List all = distributors(c); - // a distributor that's been uninstalled is not a distributor - if (saved != null && all.contains(saved)) return saved; if (all.isEmpty()) return null; - String pick = all.get(0); - prefs(c).edit().putString(KEY_DIST, pick).apply(); + String saved = p.getString(KEY_DIST, null); + // one that answered, and is still installed, is the one we keep + if (saved != null && all.contains(saved) && p.getString(KEY_ENDPOINT, null) != null) return saved; + // otherwise the one we're currently trying, moving on if it's had its + // chance and produced nothing + int idx = p.getInt(KEY_TRY, 0); + long since = p.getLong(KEY_TRY_AT, 0); + if (saved != null && since > 0 && System.currentTimeMillis() - since > TRY_MS) idx++; + if (idx >= all.size()) idx = 0; + String pick = all.get(idx); + p.edit().putString(KEY_DIST, pick).putInt(KEY_TRY, idx).putLong(KEY_TRY_AT, System.currentTimeMillis()).apply(); return pick; } + /** Give up on the current distributor; the next call picks another. */ + static void nextDistributor(Context c) { + SharedPreferences p = prefs(c); + p.edit().putInt(KEY_TRY, p.getInt(KEY_TRY, 0) + 1).putLong(KEY_TRY_AT, 0).remove(KEY_DIST).apply(); + } + static String endpoint(Context c) { return prefs(c).getString(KEY_ENDPOINT, null); } diff --git a/android/app/src/main/java/io/coinos/app/PushReceiver.java b/android/app/src/main/java/io/coinos/app/PushReceiver.java index 508c331..ce3e3f0 100644 --- a/android/app/src/main/java/io/coinos/app/PushReceiver.java +++ b/android/app/src/main/java/io/coinos/app/PushReceiver.java @@ -36,7 +36,12 @@ public class PushReceiver extends BroadcastReceiver { if (endpoint != null && !endpoint.isEmpty()) Push.setEndpoint(context, endpoint); break; } - case Push.ACTION_REGISTRATION_FAILED: + case Push.ACTION_REGISTRATION_FAILED: { + // this one won't have us — try the next one at the next opportunity + Push.nextDistributor(context); + Push.register(context); + break; + } case Push.ACTION_UNREGISTERED: { Push.prefs(context).edit().remove("endpoint").putBoolean("endpointSent", false).apply(); break; -- cgit v1.2.3