aboutsummaryrefslogtreecommitdiffstats
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/AndroidManifest.xml84
-rw-r--r--android/app/src/twa/AndroidManifest.xml85
-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
5 files changed, 130 insertions, 86 deletions
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b0dddbd..0444b76 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- TWA wrapper: full-screen Chrome running https://v3.coinos.io (needs the
- site's /.well-known/assetlinks.json to name this app's signing cert, or
- Android shows a URL bar). The point of the wrapper is the intent
- filters: lightning:/bitcoin:/nostr:/lnurl: URIs open this app, and
- LauncherActivity forwards them into the web app as ?u=<encoded>. -->
+<!-- What every build shares: notifications and the UnifiedPush receiver.
+ Each flavor's own manifest adds its activities — the TWA's live in
+ src/twa, the WebView's (webview and fdroid) in src/webview — so no build
+ declares an activity whose class it doesn't compile. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- NFC is a way in (bolt cards, NDEF tags), never a requirement -->
<uses-feature android:name="android.hardware.nfc" android:required="false" />
@@ -20,81 +19,6 @@
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
- <meta-data
- android:name="asset_statements"
- android:resource="@string/asset_statements" />
- <activity
- android:name="io.coinos.app.LauncherActivity"
- android:exported="true"
- android:theme="@android:style/Theme.Translucent.NoTitleBar">
- <meta-data
- android:name="android.support.customtabs.trusted.DEFAULT_URL"
- android:value="https://v3.coinos.io/" />
- <!-- System-bar colors: without these the TWA status bar defaults to
- white. Light follows the page background, dark the PWA's
- theme_color, so the bar icons keep their contrast either way. -->
- <meta-data
- android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
- android:resource="@color/statusBarColor" />
- <meta-data
- android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
- android:resource="@color/statusBarColorDark" />
- <meta-data
- android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
- android:resource="@color/navigationBarColor" />
- <meta-data
- android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
- android:resource="@color/navigationBarColorDark" />
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- <!-- own the site's links (verified against assetlinks.json) -->
- <intent-filter android:autoVerify="true">
- <action android:name="android.intent.action.VIEW" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.BROWSABLE" />
- <data android:scheme="https" android:host="v3.coinos.io" />
- </intent-filter>
- </activity>
- <!-- Scheme + NFC intents land on this per-intent trampoline, not the
- launcher: a TWA already on screen ignores a new intent to its
- launcher, so a second lightning: tap used to do nothing. The
- trampoline relaunches the TWA with the new URL and finishes.
- lnurlw/lnurlp ride along — bolt cards and LNURL NFC tags emit
- those schemes; NDEF dispatch needs no NFC permission. -->
- <activity
- android:name="io.coinos.app.IntentActivity"
- android:exported="true"
- android:noHistory="true"
- android:theme="@android:style/Theme.Translucent.NoTitleBar">
- <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>
- <intent-filter>
- <action android:name="android.nfc.action.NDEF_DISCOVERED" />
- <category android:name="android.intent.category.DEFAULT" />
- <data android:scheme="https" android:host="v3.coinos.io" />
- </intent-filter>
- </activity>
<!-- UnifiedPush: the distributor broadcasts here. Web push needs Google
Play Services on Android, so on a de-Googled phone this is the only
way the wallet can be reached at all while it isn't open. -->
diff --git a/android/app/src/twa/AndroidManifest.xml b/android/app/src/twa/AndroidManifest.xml
new file mode 100644
index 0000000..c430cdf
--- /dev/null
+++ b/android/app/src/twa/AndroidManifest.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- TWA wrapper: full-screen Chrome running https://v3.coinos.io (needs the
+ site's /.well-known/assetlinks.json to name this app's signing cert, or
+ Android shows a URL bar). The point of the wrapper is the intent
+ filters: lightning:/bitcoin:/nostr:/lnurl: URIs open this app, and
+ LauncherActivity forwards them into the web app as ?u=<encoded>. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <meta-data
+ android:name="asset_statements"
+ android:resource="@string/asset_statements" />
+ <activity
+ android:name="io.coinos.app.LauncherActivity"
+ android:exported="true"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar">
+ <meta-data
+ android:name="android.support.customtabs.trusted.DEFAULT_URL"
+ android:value="https://v3.coinos.io/" />
+ <!-- System-bar colors: without these the TWA status bar defaults to
+ white. Light follows the page background, dark the PWA's
+ theme_color, so the bar icons keep their contrast either way. -->
+ <meta-data
+ android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
+ android:resource="@color/statusBarColor" />
+ <meta-data
+ android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
+ android:resource="@color/statusBarColorDark" />
+ <meta-data
+ android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
+ android:resource="@color/navigationBarColor" />
+ <meta-data
+ android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
+ android:resource="@color/navigationBarColorDark" />
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!-- own the site's links (verified against assetlinks.json) -->
+ <intent-filter android:autoVerify="true">
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="https" android:host="v3.coinos.io" />
+ </intent-filter>
+ </activity>
+ <!-- Scheme + NFC intents land on this per-intent trampoline, not the
+ launcher: a TWA already on screen ignores a new intent to its
+ launcher, so a second lightning: tap used to do nothing. The
+ trampoline relaunches the TWA with the new URL and finishes.
+ lnurlw/lnurlp ride along — bolt cards and LNURL NFC tags emit
+ those schemes; NDEF dispatch needs no NFC permission. -->
+ <activity
+ android:name="io.coinos.app.IntentActivity"
+ android:exported="true"
+ android:noHistory="true"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar">
+ <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>
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="https" android:host="v3.coinos.io" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
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.