aboutsummaryrefslogtreecommitdiffstats
path: root/android/app/src/webview/java
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-04 23:02:15 -0700
committerAdam Soltys <[email protected]>2026-10-04 23:02:15 -0700
commite1bc34923bed8fd00c5a3cdacdfb7f102c87d7a3 (patch)
tree93ba587663ff503c84b96b077676086159d2f9ce /android/app/src/webview/java
parentbd935e0cab30461dc4c8702ad54704b6e3d90726 (diff)
downloadcoinosv3-e1bc34923bed8fd00c5a3cdacdfb7f102c87d7a3.tar.gz
coinosv3-e1bc34923bed8fd00c5a3cdacdfb7f102c87d7a3.zip
Android: TWA activities only in the TWA build, push wakes run the bundled app, the origin fails closed; 1.9 (code 10).fdroid-v1.9
F-Droid review of 1.8: the shared manifest declared the TWA's LauncherActivity/IntentActivity in the webview and fdroid builds, which don't compile them (a second, crashing launcher icon). They move to src/twa/AndroidManifest.xml. WakeJob's off-screen WebView now answers the origin from assets like MainActivity's, and Assets returns 404 for any v3.coinos.io path it doesn't carry outside the live API list instead of fetching it. POST_NOTIFICATIONS is asked only when a UnifiedPush distributor is installed. The description names the third-party hosts (DoH, post media). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'android/app/src/webview/java')
-rw-r--r--android/app/src/webview/java/io/coinos/app/Assets.java21
-rw-r--r--android/app/src/webview/java/io/coinos/app/MainActivity.java4
-rw-r--r--android/app/src/webview/java/io/coinos/app/WakeJob.java22
3 files changed, 41 insertions, 6 deletions
diff --git a/android/app/src/webview/java/io/coinos/app/Assets.java b/android/app/src/webview/java/io/coinos/app/Assets.java
index ff3390b..b946500 100644
--- a/android/app/src/webview/java/io/coinos/app/Assets.java
+++ b/android/app/src/webview/java/io/coinos/app/Assets.java
@@ -6,6 +6,7 @@ import android.net.Uri;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@@ -22,6 +23,10 @@ import java.util.Map;
* the paths the site's own nginx and service worker treat as live. A path
* with no file behind it and no extension is a deep link (/g/…, /invite/…,
* /npub…): it gets index.html, as the site's SPA fallback would.
+ *
+ * <p>Nothing else under the origin reaches the network: a file the APK
+ * doesn't carry (coinos.apk, say) is a 404 here, not a fetch from the live
+ * site, so no code this build didn't ship can run in the app's origin.
*/
final class Assets {
private Assets() {}
@@ -48,20 +53,20 @@ final class Assets {
/** The asset answering this request, or null to let the network have it. */
static WebResourceResponse serve(Context ctx, WebResourceRequest req) {
Uri u = req.getUrl();
- if (u == null || !"https".equals(u.getScheme()) || !HOST.equals(u.getHost())) return null;
- if (!"GET".equalsIgnoreCase(req.getMethod())) return null;
+ if (u == null || !HOST.equals(u.getHost())) return null;
String path = u.getPath();
if (path == null || path.isEmpty()) path = "/";
- for (String live : LIVE) if (path.startsWith(live)) return null;
+ if ("https".equals(u.getScheme())) for (String live : LIVE) if (path.startsWith(live)) return null;
+ if (!"GET".equalsIgnoreCase(req.getMethod()) || !"https".equals(u.getScheme())) return missing();
String rel = path.equals("/") ? "index.html" : path.substring(1);
AssetManager am = ctx.getAssets();
InputStream in = open(am, rel);
if (in == null) {
String last = rel.substring(rel.lastIndexOf('/') + 1);
- if (last.contains(".")) return null; // a real file we don't carry (coinos.apk): the network's
+ if (last.contains(".")) return missing(); // a real file we don't carry (coinos.apk)
rel = "index.html";
in = open(am, rel);
- if (in == null) return null;
+ if (in == null) return missing();
}
String ext = rel.substring(rel.lastIndexOf('.') + 1).toLowerCase();
String mime = MIME.containsKey(ext) ? MIME.get(ext) : "application/octet-stream";
@@ -73,6 +78,12 @@ final class Assets {
return res;
}
+ /** Fail closed: the origin's files are the APK's or nobody's. */
+ private static WebResourceResponse missing() {
+ return new WebResourceResponse("text/plain", "utf-8", 404, "Not Found",
+ new HashMap<>(), new ByteArrayInputStream(new byte[0]));
+ }
+
private static InputStream open(AssetManager am, String rel) {
try { return am.open(rel); } catch (IOException e) { return null; }
}
diff --git a/android/app/src/webview/java/io/coinos/app/MainActivity.java b/android/app/src/webview/java/io/coinos/app/MainActivity.java
index e75e723..d88a969 100644
--- a/android/app/src/webview/java/io/coinos/app/MainActivity.java
+++ b/android/app/src/webview/java/io/coinos/app/MainActivity.java
@@ -118,7 +118,9 @@ public class MainActivity extends Activity {
}
});
- if (Build.VERSION.SDK_INT >= 33
+ // Notifications only ever come from UnifiedPush, so the permission is
+ // asked for only when a distributor is installed to deliver them.
+ if (Build.VERSION.SDK_INT >= 33 && !Push.distributors(this).isEmpty()
&& checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
try { requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS }, REQ_PERMS); } catch (Exception ignored) {}
}
diff --git a/android/app/src/webview/java/io/coinos/app/WakeJob.java b/android/app/src/webview/java/io/coinos/app/WakeJob.java
index a6cb1a2..a6175f7 100644
--- a/android/app/src/webview/java/io/coinos/app/WakeJob.java
+++ b/android/app/src/webview/java/io/coinos/app/WakeJob.java
@@ -6,8 +6,11 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.webkit.JavascriptInterface;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
+import android.webkit.WebViewClient;
/**
* The wallet, woken without being opened.
@@ -49,6 +52,25 @@ public class WakeJob extends JobService {
s.setDomStorageEnabled(true);
s.setDatabaseEnabled(true);
web.addJavascriptInterface(new Host(), "CoinosHost");
+ // The same files the visible app runs: the bundled build answers the
+ // site's own paths from assets here too, or a wake would run whatever
+ // the live site serves today in this app's origin and storage.
+ web.setWebViewClient(new WebViewClient() {
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView v, WebResourceRequest r) {
+ if (BuildConfig.BUNDLED) {
+ WebResourceResponse res = Assets.serve(WakeJob.this, r);
+ if (res != null) return res;
+ }
+ return super.shouldInterceptRequest(v, r);
+ }
+
+ // off-screen, nothing navigates anywhere: no one is there to see it
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView v, WebResourceRequest r) {
+ return true;
+ }
+ });
// The payload rides in as a parameter the page can read: it carries the
// event that woke us, so the wallet needn't hunt for it on a relay.