diff options
| -rw-r--r-- | android/app/build.gradle | 4 | ||||
| -rw-r--r-- | android/app/src/webview/java/io/coinos/app/MainActivity.java | 53 | ||||
| -rw-r--r-- | static/.well-known/assetlinks.json | 57 |
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" + ] + } } -}] +] |
