diff options
| author | Adam Soltys <[email protected]> | 2026-09-12 10:32:01 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-12 10:32:01 -0700 |
| commit | 60e755286302647d93bc82070c61fcf93ddbc3b9 (patch) | |
| tree | 5cbe3c780010e975d61193138a29870f295427af /android/app/src/main/java | |
| parent | a846211262f37238c001e5210938749294f8db6a (diff) | |
| download | coinosv3-60e755286302647d93bc82070c61fcf93ddbc3b9.tar.gz coinosv3-60e755286302647d93bc82070c61fcf93ddbc3b9.zip | |
A second Android build that can wake the wallet, because it holds it.
The TWA can't. Chrome renders the site, so the wallet's keys live in Chrome's
storage for its own origin, and nothing outside Chrome can reach them — a
UnifiedPush message could raise a notification and no more. On a phone with
Google Play Services none of this matters; without it, background answering
was simply unavailable.
So: a second flavour, io.coinos.webview, "Coinos (UnifiedPush)". The wallet
runs in a WebView this app owns, which means a push can start it off-screen
and let it answer for itself. WakeService loads it with ?wake=<payload> into
the same storage the visible app uses, the page subscribes and answers
whatever the payload was about, then calls CoinosHost.done() so the service
stops — capped at 25 seconds from the page and 30 from the service, because
nothing here is allowed to sit in the background costing battery. Its
foreground notification says what it's doing rather than apologising for
existing.
The WebView carries what the wallet actually needs: DOM storage and
IndexedDB, the camera for the QR scanner (granted only once Android has
granted it to us), a file chooser for attaching pictures, and links to
anywhere but coinos.io handed to a real browser.
The trade is named in the code and worth naming here: a separate app is a
separate wallet. Its storage is its own, so signing in there is signing in
again. That's why this is a second build rather than a replacement — the TWA
is untouched, still the better browser for anyone whose phone has push.
The shared push receiver now asks the system for whatever front door this
build has, and asks a per-flavour Wake class whether the wallet can be woken
at all — no flavour checks in the middle of it. Both APKs build and ship:
coinos.apk as before, coinos-unifiedpush.apk beside it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'android/app/src/main/java')
3 files changed, 9 insertions, 91 deletions
diff --git a/android/app/src/main/java/io/coinos/app/IntentActivity.java b/android/app/src/main/java/io/coinos/app/IntentActivity.java deleted file mode 100644 index 3de1d62..0000000 --- a/android/app/src/main/java/io/coinos/app/IntentActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.coinos.app; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -/** - * Trampoline for scheme + NFC intents. A TWA already on screen ignores a - * new intent delivered to its (still-alive) launcher, so a second - * lightning: tap used to do nothing. This activity exists per-intent: - * it maps the URI to the web app's ?u= form and relaunches LauncherActivity - * with CLEAR_TOP — the Chrome activity above it is cleared, the launcher is - * recreated with the new data, and the TWA navigates. Then it's gone. - */ -public class IntentActivity extends android.app.Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Uri url = Uri.parse("https://v3.coinos.io/"); - Uri data = getIntent().getData(); - if (data != null) { - String scheme = data.getScheme(); - if (scheme != null && !scheme.equals("https") && !scheme.equals("http")) { - url = Uri.parse("https://v3.coinos.io/?u=" + Uri.encode(data.toString())); - } else { - url = data; - } - } - Intent i = new Intent(this, LauncherActivity.class); - i.setData(url); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); - finish(); - } -} diff --git a/android/app/src/main/java/io/coinos/app/LauncherActivity.java b/android/app/src/main/java/io/coinos/app/LauncherActivity.java deleted file mode 100644 index 654265b..0000000 --- a/android/app/src/main/java/io/coinos/app/LauncherActivity.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.coinos.app; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; - -import java.net.URLEncoder; - -/** - * The TWA launcher. Scheme/NFC intents arrive via IntentActivity (the - * trampoline that survives the running-TWA case); this activity only needs - * to honor a full URL handed to it, falling back to the site root. - * - * <p>It also carries the UnifiedPush endpoint across: the Android side is - * what a distributor talks to, and the web app is what knows which pubkeys - * to watch, so the endpoint rides into the web app as ?up= exactly the way - * payment intents ride in as ?u=. - */ -public class LauncherActivity - extends com.google.androidbrowserhelper.trusted.LauncherActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Ask the distributor on every launch: it's idempotent, and it's how a - // distributor installed after us ever gets noticed. - Push.register(this); - // Android 13+ won't show our notifications without this, and the TWA's - // own permission covers the browser's, not ours. - if (Build.VERSION.SDK_INT >= 33 - && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED - && Push.distributor(this) != null) { - try { requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS }, 1); } catch (Exception ignored) {} - } - } - - @Override - protected Uri getLaunchingUrl() { - Uri data = getIntent().getData(); - Uri base = (data != null && "v3.coinos.io".equals(data.getHost())) ? data : super.getLaunchingUrl(); - String endpoint = Push.endpoint(this); - if (endpoint == null || Push.endpointDelivered(this)) return base; - try { - String sep = base.toString().contains("?") ? "&" : "?"; - Uri withEndpoint = Uri.parse(base + sep + "up=" + URLEncoder.encode(endpoint, "UTF-8")); - Push.markEndpointDelivered(this); - return withEndpoint; - } catch (Exception e) { - return base; - } - } -} 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 07cabeb..faabfaf 100644 --- a/android/app/src/main/java/io/coinos/app/PushReceiver.java +++ b/android/app/src/main/java/io/coinos/app/PushReceiver.java @@ -44,7 +44,11 @@ public class PushReceiver extends BroadcastReceiver { case Push.ACTION_MESSAGE: { byte[] bytes = intent.getByteArrayExtra(Push.EXTRA_BYTES_MESSAGE); String body = bytes != null ? new String(bytes) : intent.getStringExtra(Push.EXTRA_MESSAGE); - notify(context, body); + // On the WebView build the wallet lives in THIS app's storage, so it + // can be woken and told to deal with whatever arrived — no tap. On + // the TWA build the wallet is Chrome's and out of reach, so the + // notification is the whole of what we can do. + if (!Wake.start(context, body)) notify(context, body); Push.ack(context, intent.getStringExtra(Push.EXTRA_ID)); break; } @@ -84,7 +88,10 @@ public class PushReceiver extends BroadcastReceiver { nm.createNotificationChannel(ch); } - Intent open = new Intent(c, LauncherActivity.class); + // whichever front door this build has — the TWA's launcher or the + // WebView's main activity; the receiver is shared by both + Intent open = c.getPackageManager().getLaunchIntentForPackage(c.getPackageName()); + if (open == null) open = new Intent(Intent.ACTION_VIEW); open.setAction(Intent.ACTION_VIEW); open.setData(Uri.parse(deepLink != null && deepLink.startsWith("https://v3.coinos.io") ? deepLink : "https://v3.coinos.io/")); |
