aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--android/app/build.gradle4
-rw-r--r--android/app/src/webview/java/io/coinos/app/MainActivity.java53
-rw-r--r--static/.well-known/assetlinks.json57
3 files changed, 106 insertions, 8 deletions
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 046358d..8dcfc04 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -145,6 +145,10 @@ android {
dependencies {
// only the TWA shell needs Chrome's helper; the WebView build carries none
twaImplementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0'
+ // WebAuthn (passkeys) inside the WebView builds — androidx, no Google services
+ webviewImplementation 'androidx.webkit:webkit:1.14.0'
+ fcmImplementation 'androidx.webkit:webkit:1.14.0'
+ fdroidImplementation 'androidx.webkit:webkit:1.14.0'
// only the fcm build carries Google's push client
fcmImplementation 'com.google.firebase:firebase-messaging:24.1.0'
}
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 b26d6c5..806fd8f 100644
--- a/android/app/src/webview/java/io/coinos/app/MainActivity.java
+++ b/android/app/src/webview/java/io/coinos/app/MainActivity.java
@@ -38,6 +38,7 @@ public class MainActivity extends Activity {
static final String BASE = "https://v3.coinos.io/";
private WebView web;
+ private android.widget.FrameLayout root;
private ValueCallback<Uri[]> filePicker;
private static final int REQ_FILE = 10, REQ_PERMS = 11;
@@ -45,8 +46,27 @@ public class MainActivity extends Activity {
protected void onCreate(Bundle saved) {
super.onCreate(saved);
web = new WebView(this);
- setContentView(web, new ViewGroup.LayoutParams(
+ // Android 15 draws every app edge to edge, so the page ran under the
+ // status bar and the navigation buttons sat on top of its bottom nav.
+ // The page goes in a frame padded by the system bars (and the keyboard,
+ // which then resizes the page as adjustResize did before); the frame's
+ // background is the theme's bar colour.
+ root = new android.widget.FrameLayout(this);
+ root.addView(web, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ // 3-button navigation gets a light scrim of the system's own over
+ // whatever colour is behind it; the frame's colour is the one wanted
+ if (Build.VERSION.SDK_INT >= 29) getWindow().setNavigationBarContrastEnforced(false);
+ if (Build.VERSION.SDK_INT >= 30) {
+ root.setOnApplyWindowInsetsListener((v, insets) -> {
+ android.graphics.Insets b = insets.getInsets(
+ android.view.WindowInsets.Type.systemBars() | android.view.WindowInsets.Type.ime()
+ | android.view.WindowInsets.Type.displayCutout());
+ v.setPadding(b.left, b.top, b.right, b.bottom);
+ return android.view.WindowInsets.CONSUMED;
+ });
+ }
+ setContentView(root);
// A debug build can be inspected from a desktop (chrome://inspect); a
// release build of a wallet must not be — anything with ADB access could
@@ -63,6 +83,16 @@ public class MainActivity extends Activity {
s.setLoadWithOverviewMode(true);
s.setUseWideViewPort(true);
CookieManager.getInstance().setAcceptCookie(true);
+ // Passkeys: a WebView offers the page no WebAuthn unless the app asks for
+ // it, so passkey sign-in and the fingerprint lock were missing here. The
+ // site's assetlinks.json vouches for this app (get_login_creds), which
+ // is what lets it use v3.coinos.io's passkeys.
+ try {
+ if (androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.WEB_AUTHENTICATION)) {
+ androidx.webkit.WebSettingsCompat.setWebAuthenticationSupport(s,
+ androidx.webkit.WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_FOR_APP);
+ }
+ } catch (Throwable ignored) {}
web.setWebViewClient(new WebViewClient() {
// The bundled build answers the site's own files from assets; the
@@ -79,6 +109,10 @@ public class MainActivity extends Activity {
@Override
public void onPageFinished(WebView v, String url) {
endpointToPage();
+ // the bars take the page's colour, now and whenever its theme changes
+ v.evaluateJavascript("(function(){var m=document.querySelector('meta[name=theme-color]');"
+ + "if(!m||!window.CoinosHost||!CoinosHost.bars)return;var f=function(){CoinosHost.bars(m.content||'')};"
+ + "f();new MutationObserver(f).observe(m,{attributes:true,attributeFilter:['content']})})()", null);
}
@Override
@@ -165,6 +199,23 @@ public class MainActivity extends Activity {
+ ",\"distributor\":" + (dist == null ? "null" : "\"" + dist + "\"")
+ ",\"endpoint\":" + (endpoint != null) + "}";
}
+ /** The page's theme colour, for the bar areas around it (see onCreate). */
+ @android.webkit.JavascriptInterface
+ public void bars(String css) {
+ final int color;
+ try { color = android.graphics.Color.parseColor(css.trim()); } catch (Exception e) { return; }
+ runOnUiThread(() -> {
+ if (root != null) root.setBackgroundColor(color);
+ if (Build.VERSION.SDK_INT >= 30 && getWindow().getInsetsController() != null) {
+ // dark icons on a light colour, light icons on a dark one
+ double lum = (0.299 * android.graphics.Color.red(color) + 0.587 * android.graphics.Color.green(color)
+ + 0.114 * android.graphics.Color.blue(color)) / 255;
+ int light = android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ | android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+ getWindow().getInsetsController().setSystemBarsAppearance(lum > 0.5 ? light : 0, light);
+ }
+ });
+ }
/** The wake job's bridge has this too; here it's a no-op. */
@android.webkit.JavascriptInterface
public void done() {}
diff --git a/static/.well-known/assetlinks.json b/static/.well-known/assetlinks.json
index 9d84b58..828c393 100644
--- a/static/.well-known/assetlinks.json
+++ b/static/.well-known/assetlinks.json
@@ -1,8 +1,51 @@
-[{
- "relation": ["delegate_permission/common.handle_all_urls"],
- "target": {
- "namespace": "android_app",
- "package_name": "io.coinos.app",
- "sha256_cert_fingerprints": ["76:64:83:AB:83:72:C0:2F:84:06:D2:95:95:E7:38:9A:28:69:19:0B:42:9E:0F:FD:09:2E:BB:0B:FA:2B:C2:F4"]
+[
+ {
+ "relation": [
+ "delegate_permission/common.handle_all_urls",
+ "delegate_permission/common.get_login_creds"
+ ],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "io.coinos.app",
+ "sha256_cert_fingerprints": [
+ "76:64:83:AB:83:72:C0:2F:84:06:D2:95:95:E7:38:9A:28:69:19:0B:42:9E:0F:FD:09:2E:BB:0B:FA:2B:C2:F4"
+ ]
+ }
+ },
+ {
+ "relation": [
+ "delegate_permission/common.get_login_creds"
+ ],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "io.coinos.beta",
+ "sha256_cert_fingerprints": [
+ "76:64:83:AB:83:72:C0:2F:84:06:D2:95:95:E7:38:9A:28:69:19:0B:42:9E:0F:FD:09:2E:BB:0B:FA:2B:C2:F4"
+ ]
+ }
+ },
+ {
+ "relation": [
+ "delegate_permission/common.get_login_creds"
+ ],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "io.coinos.webview",
+ "sha256_cert_fingerprints": [
+ "76:64:83:AB:83:72:C0:2F:84:06:D2:95:95:E7:38:9A:28:69:19:0B:42:9E:0F:FD:09:2E:BB:0B:FA:2B:C2:F4"
+ ]
+ }
+ },
+ {
+ "relation": [
+ "delegate_permission/common.get_login_creds"
+ ],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "io.coinos.fdroid",
+ "sha256_cert_fingerprints": [
+ "76:64:83:AB:83:72:C0:2F:84:06:D2:95:95:E7:38:9A:28:69:19:0B:42:9E:0F:FD:09:2E:BB:0B:FA:2B:C2:F4"
+ ]
+ }
}
-}]
+]