aboutsummaryrefslogtreecommitdiffstats
path: root/android/app/src/fcm
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-06 23:01:15 -0700
committerAdam Soltys <[email protected]>2026-11-06 23:01:15 -0700
commit8969744783e2851b287e937f71033ce4b6bb4dc3 (patch)
tree682652e80d9119da9106a2e5ce2d11eeadb08d6f /android/app/src/fcm
parent1fe10057f301b09bbad0cfccdfecac9826733048 (diff)
downloadcoinosv3-8969744783e2851b287e937f71033ce4b6bb4dc3.tar.gz
coinosv3-8969744783e2851b287e937f71033ce4b6bb4dc3.zip
A third Android build, fcm (io.coinos.beta, "Coinos Beta"): the wallet in its own WebView, as the webview build, with Firebase Cloud Messaging for push on phones with Google Play Services — no distributor app to install. A user on the TWA APK found launching slow next to Minibits (2026-11-05); measured on a Pixel 9, a WebView paints ~0.7 s sooner than a cold Chrome. This build installs beside the TWA for a side-by-side trial before deciding whether it replaces it. Fcm asks for the token and stores "fcm:<token>" as the push endpoint, which reaches the page down the UnifiedPush road (?up= on the next launch, or written straight into the page's localStorage once it has loaded, since the token arrives after the first load starts); nwcpush (coinosv3-server 0c50417) sends an fcm: endpoint through Firebase. FcmService hands a message's "body" to the same handler UnifiedPush messages use (PushReceiver.onPush: wake the wallet off-screen, notify unless it answered a silent request). Firebase is set up in code (CoinosApp → Fcm.init) from values build.gradle reads out of app/google-services.json (gitignored) — without it the build still builds and falls back to UnifiedPush. The webview and fdroid builds get a stub Fcm (src/nofcm) and carry no Google code; the notification permission is asked when Firebase or a distributor can deliver; CoinosHost.env reports app "fcm"
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
Diffstat (limited to 'android/app/src/fcm')
-rw-r--r--android/app/src/fcm/AndroidManifest.xml66
-rw-r--r--android/app/src/fcm/java/io/coinos/app/CoinosApp.java12
-rw-r--r--android/app/src/fcm/java/io/coinos/app/Fcm.java64
-rw-r--r--android/app/src/fcm/java/io/coinos/app/FcmService.java19
4 files changed, 161 insertions, 0 deletions
diff --git a/android/app/src/fcm/AndroidManifest.xml b/android/app/src/fcm/AndroidManifest.xml
new file mode 100644
index 0000000..71e75b9
--- /dev/null
+++ b/android/app/src/fcm/AndroidManifest.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- The fcm build: the WebView build (src/webview/AndroidManifest.xml — keep
+ the two in step) plus Firebase Cloud Messaging for push on phones with
+ Google Play Services. See Fcm.java. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.INTERNET" />
+ <!-- the wake job asks to run when there's a network; the scheduler won't
+ take that constraint without this -->
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" android:required="false" />
+ <application
+ android:name="io.coinos.app.CoinosApp"
+ android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:theme="@style/AppTheme"
+ android:supportsRtl="true"
+ android:usesCleartextTraffic="false">
+ <activity
+ android:name="io.coinos.app.MainActivity"
+ android:exported="true"
+ android:launchMode="singleTask"
+ android:configChanges="orientation|screenSize|keyboardHidden|uiMode"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!-- the payment schemes the TWA build claims, claimed here too -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="lightning" />
+ <data android:scheme="bitcoin" />
+ <data android:scheme="nostr" />
+ <data android:scheme="lnurl" />
+ <data android:scheme="lnurlw" />
+ <data android:scheme="lnurlp" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="lightning" />
+ <data android:scheme="bitcoin" />
+ <data android:scheme="nostr" />
+ <data android:scheme="lnurl" />
+ <data android:scheme="lnurlw" />
+ <data android:scheme="lnurlp" />
+ </intent-filter>
+ </activity>
+ <!-- pushes through Firebase -->
+ <service
+ android:name="io.coinos.app.FcmService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
+ </intent-filter>
+ </service>
+ <!-- the off-screen wallet a push wakes -->
+ <service
+ android:name="io.coinos.app.WakeJob"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+ </application>
+</manifest>
diff --git a/android/app/src/fcm/java/io/coinos/app/CoinosApp.java b/android/app/src/fcm/java/io/coinos/app/CoinosApp.java
new file mode 100644
index 0000000..855db91
--- /dev/null
+++ b/android/app/src/fcm/java/io/coinos/app/CoinosApp.java
@@ -0,0 +1,12 @@
+package io.coinos.app;
+
+import android.app.Application;
+
+/** Sets Firebase up before anything in the process (a push included) needs it. */
+public class CoinosApp extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Fcm.init(this);
+ }
+}
diff --git a/android/app/src/fcm/java/io/coinos/app/Fcm.java b/android/app/src/fcm/java/io/coinos/app/Fcm.java
new file mode 100644
index 0000000..02ce12c
--- /dev/null
+++ b/android/app/src/fcm/java/io/coinos/app/Fcm.java
@@ -0,0 +1,64 @@
+package io.coinos.app;
+
+import android.content.Context;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.messaging.FirebaseMessaging;
+
+/**
+ * Firebase Cloud Messaging, for a phone with Google Play Services: the same
+ * channel web push rides in Chrome, so the wallet is reached with no
+ * distributor app to install. Its token goes to the web app as the push
+ * endpoint "fcm:<token>", down the road a UnifiedPush endpoint takes (?up=,
+ * or straight into the page's storage), and the notifier sends an fcm:
+ * endpoint through Firebase. A phone without Play Services, or a build made
+ * without android/app/google-services.json, falls back to UnifiedPush.
+ *
+ * <p>Firebase is set up here from the values build.gradle read out of that
+ * file, rather than by Google's Gradle plugin.
+ */
+final class Fcm {
+ private Fcm() {}
+ static final String PREFIX = "fcm:";
+
+ static boolean configured() { return !BuildConfig.FCM_APP_ID.isEmpty(); }
+
+ static boolean available(Context c) {
+ if (!configured()) return false;
+ return Push.playServices(c);
+ }
+
+ /** Called from CoinosApp, before anything (FcmService included) asks for Firebase. */
+ static void init(Context c) {
+ if (!configured()) return;
+ try {
+ if (!FirebaseApp.getApps(c).isEmpty()) return;
+ FirebaseApp.initializeApp(c, new FirebaseOptions.Builder()
+ .setApplicationId(BuildConfig.FCM_APP_ID)
+ .setApiKey(BuildConfig.FCM_API_KEY)
+ .setProjectId(BuildConfig.FCM_PROJECT_ID)
+ .setGcmSenderId(BuildConfig.FCM_SENDER_ID)
+ .build());
+ } catch (Throwable ignored) {}
+ }
+
+ /**
+ * Ask for the token. It arrives later; onEndpoint runs (on any thread) once
+ * it's stored as this app's push endpoint.
+ * @return whether Firebase will deliver this app's pushes
+ */
+ static boolean register(Context c, Runnable onEndpoint) {
+ if (!available(c)) return false;
+ init(c);
+ try {
+ FirebaseMessaging.getInstance().getToken().addOnSuccessListener((token) -> {
+ if (token == null || token.isEmpty()) return;
+ String ep = PREFIX + token;
+ if (!ep.equals(Push.endpoint(c))) Push.setEndpoint(c, ep);
+ if (onEndpoint != null) onEndpoint.run();
+ });
+ return true;
+ } catch (Throwable t) { return false; }
+ }
+}
diff --git a/android/app/src/fcm/java/io/coinos/app/FcmService.java b/android/app/src/fcm/java/io/coinos/app/FcmService.java
new file mode 100644
index 0000000..24d4aca
--- /dev/null
+++ b/android/app/src/fcm/java/io/coinos/app/FcmService.java
@@ -0,0 +1,19 @@
+package io.coinos.app;
+
+import com.google.firebase.messaging.FirebaseMessagingService;
+import com.google.firebase.messaging.RemoteMessage;
+
+/** Firebase's half of PushReceiver: the notifier's JSON arrives as data "body". */
+public class FcmService extends FirebaseMessagingService {
+ @Override
+ public void onNewToken(String token) {
+ // a rotated token is a new endpoint; the page gets it on its next load
+ if (token != null && !token.isEmpty()) Push.setEndpoint(this, Fcm.PREFIX + token);
+ }
+
+ @Override
+ public void onMessageReceived(RemoteMessage msg) {
+ String body = msg.getData() == null ? null : msg.getData().get("body");
+ if (body != null) PushReceiver.onPush(this, body);
+ }
+}