aboutsummaryrefslogtreecommitdiffstats
path: root/android/app/src/webview/java/io/coinos
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/src/webview/java/io/coinos')
-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.