@crossbind/core-app-host 0.0.0-bootstrap.0 → 2.0.0-beta.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +55 -3
  2. package/android/app/src/main/AndroidManifest.xml +15 -0
  3. package/android/app/src/main/java/dev/crossbind/app/CrossbindActivity.java +366 -0
  4. package/android/app/src/main/res/xml/network_security_config.xml +8 -0
  5. package/android/build.gradle +14 -0
  6. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  7. package/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  8. package/android/gradle.properties +2 -0
  9. package/android/gradlew +248 -0
  10. package/android/gradlew.bat +112 -0
  11. package/android/settings.gradle +14 -0
  12. package/cpp/CMakeLists.txt +110 -0
  13. package/cpp/src/android/jni.cpp +167 -0
  14. package/cpp/src/app.cpp +40 -0
  15. package/cpp/src/app.h +21 -0
  16. package/cpp/src/attach.cpp +40 -0
  17. package/cpp/src/darwin/bundle.mm +19 -0
  18. package/cpp/src/darwin/channels.mm +196 -0
  19. package/cpp/src/darwin/navigation.mm +69 -0
  20. package/cpp/src/host_core.cpp +335 -0
  21. package/cpp/src/host_core.h +84 -0
  22. package/cpp/src/ios/bundle.mm +15 -0
  23. package/cpp/src/ios/main.mm +71 -0
  24. package/cpp/src/linux/app.cpp +29 -0
  25. package/cpp/src/linux/channels.cpp +143 -0
  26. package/cpp/src/linux/main_thread.h +16 -0
  27. package/cpp/src/linux/navigation.cpp +55 -0
  28. package/cpp/src/main.cpp +57 -0
  29. package/cpp/src/platform.h +57 -0
  30. package/cpp/src/windows/app.cpp +36 -0
  31. package/cpp/src/windows/app.manifest +8 -0
  32. package/cpp/src/windows/app.rc +3 -0
  33. package/cpp/src/windows/channels.cpp +336 -0
  34. package/cpp/src/windows/com.cpp +92 -0
  35. package/cpp/src/windows/com.h +57 -0
  36. package/cpp/src/windows/navigation.cpp +90 -0
  37. package/js/codec.js +123 -0
  38. package/js/host.js +8 -0
  39. package/js/lean-client.js +147 -0
  40. package/js/lean-host.js +237 -0
  41. package/js/page.js +79 -0
  42. package/js/polyfills.js +32 -0
  43. package/package.json +30 -6
  44. package/third_party/jsi/LICENSE +21 -0
  45. package/third_party/jsi/README.md +18 -0
  46. package/third_party/jsi/include/jsi/hermes-interfaces.h +189 -0
  47. package/third_party/jsi/include/jsi/instrumentation.h +132 -0
  48. package/third_party/jsi/include/jsi/jsi-inl.h +434 -0
  49. package/third_party/jsi/include/jsi/jsi.h +2266 -0
  50. package/third_party/jsi/jsi.cpp +1110 -0
  51. package/third_party/webview/LICENSE +22 -0
  52. package/third_party/webview/README.md +24 -0
  53. package/third_party/webview/core/include/webview/api.h +251 -0
  54. package/third_party/webview/core/include/webview/backends.hh +40 -0
  55. package/third_party/webview/core/include/webview/c_api_impl.hh +258 -0
  56. package/third_party/webview/core/include/webview/detail/backends/cocoa_webkit.hh +633 -0
  57. package/third_party/webview/core/include/webview/detail/backends/gtk_webkitgtk.hh +355 -0
  58. package/third_party/webview/core/include/webview/detail/backends/win32_edge.hh +915 -0
  59. package/third_party/webview/core/include/webview/detail/basic_result.hh +119 -0
  60. package/third_party/webview/core/include/webview/detail/engine_base.hh +382 -0
  61. package/third_party/webview/core/include/webview/detail/exceptions.hh +42 -0
  62. package/third_party/webview/core/include/webview/detail/json.hh +337 -0
  63. package/third_party/webview/core/include/webview/detail/native_library.hh +176 -0
  64. package/third_party/webview/core/include/webview/detail/optional.hh +118 -0
  65. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSApplication.hh +107 -0
  66. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +56 -0
  67. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSEvent.hh +71 -0
  68. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +66 -0
  69. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +52 -0
  70. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +51 -0
  71. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +53 -0
  72. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +51 -0
  73. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSOpenPanel.hh +71 -0
  74. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +53 -0
  75. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +53 -0
  76. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSSavePanel.hh +52 -0
  77. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +53 -0
  78. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +87 -0
  79. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +63 -0
  80. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +52 -0
  81. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +56 -0
  82. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSView.hh +91 -0
  83. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/NSWindow.hh +137 -0
  84. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/cocoa.hh +59 -0
  85. package/third_party/webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +49 -0
  86. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/Class.hh +55 -0
  87. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +70 -0
  88. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +75 -0
  89. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/memory.hh +57 -0
  90. package/third_party/webview/core/include/webview/detail/platform/darwin/objc/objc.hh +47 -0
  91. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +57 -0
  92. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +51 -0
  93. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +61 -0
  94. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +71 -0
  95. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +108 -0
  96. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +60 -0
  97. package/third_party/webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +46 -0
  98. package/third_party/webview/core/include/webview/detail/platform/linux/gtk/compat.hh +137 -0
  99. package/third_party/webview/core/include/webview/detail/platform/linux/webkitgtk/compat.hh +142 -0
  100. package/third_party/webview/core/include/webview/detail/platform/linux/webkitgtk/dmabuf.hh +167 -0
  101. package/third_party/webview/core/include/webview/detail/platform/windows/com_init_wrapper.hh +126 -0
  102. package/third_party/webview/core/include/webview/detail/platform/windows/dpi.hh +161 -0
  103. package/third_party/webview/core/include/webview/detail/platform/windows/dwmapi.hh +67 -0
  104. package/third_party/webview/core/include/webview/detail/platform/windows/iid.hh +78 -0
  105. package/third_party/webview/core/include/webview/detail/platform/windows/ntdll.hh +58 -0
  106. package/third_party/webview/core/include/webview/detail/platform/windows/reg_key.hh +135 -0
  107. package/third_party/webview/core/include/webview/detail/platform/windows/shcore.hh +59 -0
  108. package/third_party/webview/core/include/webview/detail/platform/windows/theme.hh +76 -0
  109. package/third_party/webview/core/include/webview/detail/platform/windows/user32.hh +90 -0
  110. package/third_party/webview/core/include/webview/detail/platform/windows/version.hh +151 -0
  111. package/third_party/webview/core/include/webview/detail/platform/windows/webview2/loader.hh +391 -0
  112. package/third_party/webview/core/include/webview/detail/user_script.hh +76 -0
  113. package/third_party/webview/core/include/webview/detail/utility/string.hh +99 -0
  114. package/third_party/webview/core/include/webview/errors.h +67 -0
  115. package/third_party/webview/core/include/webview/errors.hh +85 -0
  116. package/third_party/webview/core/include/webview/json_deprecated.hh +61 -0
  117. package/third_party/webview/core/include/webview/macros.h +123 -0
  118. package/third_party/webview/core/include/webview/types.h +81 -0
  119. package/third_party/webview/core/include/webview/types.hh +48 -0
  120. package/third_party/webview/core/include/webview/version.h +67 -0
  121. package/third_party/webview/core/include/webview/webview.h +32 -0
  122. package/third_party/webview/core/include/webview.h +15 -0
  123. package/third_party/webview2/LICENSE +27 -0
  124. package/third_party/webview2/README.md +13 -0
package/README.md CHANGED
@@ -1,6 +1,58 @@
1
1
  # @crossbind/core-app-host
2
+ **crossbind app host**
3
+ Runs the crossbind embind runtime in Hermes inside a desktop app whose page is the system webview, so the bindings
4
+ that serve WebAssembly on the web run natively in an app, behind the same page code.
2
5
 
3
- This version, 0.0.0-bootstrap.0, holds no code. npm lets a trusted publisher be configured only for a
4
- package that exists, so it reserves the name for the crossbind release workflow.
6
+ <a href="https://www.npmjs.com/package/@crossbind/core-app-host">
7
+ <img alt="NPM version" src="https://img.shields.io/npm/v/@crossbind/core-app-host/beta?style=for-the-badge" />
8
+ </a>
9
+ <a href="https://github.com/crossbind/crossbind/blob/main/LICENSE">
10
+ <img alt="License" src="https://img.shields.io/github/license/crossbind/crossbind?style=for-the-badge" />
11
+ </a>
5
12
 
6
- Install the real package: `npm install @crossbind/core-app-host@beta`. Source: https://github.com/crossbind/crossbind
13
+ ## Use
14
+
15
+ Add it next to `crossbind` and the Vite plugin, and mark the project as an app in `crossbind.config.js`
16
+ (`target: { runtimeEnv: 'app' }`):
17
+
18
+ ```bash
19
+ pnpm add -D crossbind@beta @crossbind/plugin-vite@beta @crossbind/core-app-host@beta
20
+ pnpm crossbind app dev # the app on the dev server
21
+ pnpm crossbind app build # dist/<name>.app on macOS, dist/<name>-linux-<arch>/ on Linux
22
+ pnpm crossbind app build -p linux # dist/<name>-linux-x64/ and -arm64/, on a Mac as well
23
+ pnpm crossbind app build -p windows # dist/<name>-win32-x64/ and -arm64/, on a Mac or Linux as well
24
+ pnpm crossbind app build -p ios # dist/<name>-ios/, an Xcode project
25
+ pnpm crossbind app build -p android # dist/<name>-android.apk, and its Gradle project
26
+ ```
27
+
28
+ crossbind resolves this package from your project; you never import it yourself. See the
29
+ [app reference](https://github.com/crossbind/crossbind/blob/main/docs/api/app.md) for the page's contract, security
30
+ defaults and current limits (macOS, Linux, Windows, iOS and Android for now).
31
+
32
+ ## Contents
33
+
34
+ - `cpp/` — the host's CMake project: Hermes on a thread of its own, the page's messages and `crossbind://` requests
35
+ (`src/host_core.cpp`), what every app does around its webview (`src/app.cpp`), and the window, channels and
36
+ navigation rule of macOS (`src/darwin/`), iOS (`src/ios/` with the WKWebView channels of `src/darwin/`, built as a
37
+ static library for the app's Xcode project), Linux (`src/linux/`, WebKitGTK) and Windows (`src/windows/`, WebView2).
38
+ An Android host (`src/android/jni.cpp`) is a shared library behind JNI.
39
+ - `android/` — the Gradle project an Android app is written into: its one activity (`CrossbindActivity.java`, the
40
+ WebView, its message port and navigation rule), the manifest, and Gradle 9.8.1's wrapper (Apache-2.0), as
41
+ `gradle wrapper` writes it; `gradle-wrapper.jar` matches the sha256 Gradle publishes for the release.
42
+ - `js/host.js` — what runs in Hermes: the embind runtime and the side of the protocol that answers the page.
43
+ - `js/page.js` — what the page loads as `crossbind.js`: `initNative()` over the webview's messages, or on Android its
44
+ message port.
45
+ - `third_party/webview/` — webview/webview (MIT), vendored at a pinned commit; provenance and the local patch are in
46
+ its README.
47
+ - `third_party/jsi/` — JSI's `jsi.cpp` (MIT) from the Hermes release the host links, and on Android its headers.
48
+ - `third_party/webview2/` — the license of the WebView2 SDK (BSD-3-Clause), whose headers crossbind's windows image
49
+ carries for a Windows host.
50
+
51
+ Hermes itself (MIT) is React Native's published build: on macOS and iOS downloaded once into `~/.crossbind/hermes/`
52
+ and checked against the sha256 crossbind pins, and on Android carried by crossbind's android-app image. Linux and
53
+ Windows have no published build, so crossbind's linux and windows images carry the same release built from its pinned
54
+ source.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ <application android:label="${appName}" android:networkSecurityConfig="@xml/network_security_config">
5
+ <!-- The page keeps its WebView, and its state, when the screen turns or the keyboard comes and goes. -->
6
+ <activity android:name="dev.crossbind.app.CrossbindActivity" android:exported="true"
7
+ android:theme="@android:style/Theme.DeviceDefault.DayNight"
8
+ android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density">
9
+ <intent-filter>
10
+ <action android:name="android.intent.action.MAIN" />
11
+ <category android:name="android.intent.category.LAUNCHER" />
12
+ </intent-filter>
13
+ </activity>
14
+ </application>
15
+ </manifest>
@@ -0,0 +1,366 @@
1
+ package dev.crossbind.app;
2
+
3
+ import android.app.Activity;
4
+ import android.content.ActivityNotFoundException;
5
+ import android.content.Intent;
6
+ import android.content.pm.ApplicationInfo;
7
+ import android.content.pm.PackageInfo;
8
+ import android.content.pm.PackageManager;
9
+ import android.content.res.Configuration;
10
+ import android.graphics.Insets;
11
+ import android.net.Uri;
12
+ import android.os.Bundle;
13
+ import android.os.Handler;
14
+ import android.os.HandlerThread;
15
+ import android.util.Log;
16
+ import android.view.Window;
17
+ import android.view.WindowInsets;
18
+ import android.view.WindowInsetsController;
19
+ import android.webkit.ConsoleMessage;
20
+ import android.webkit.WebChromeClient;
21
+ import android.webkit.WebResourceRequest;
22
+ import android.webkit.WebResourceResponse;
23
+ import android.webkit.WebView;
24
+ import android.webkit.WebViewClient;
25
+ import android.widget.FrameLayout;
26
+ import android.widget.TextView;
27
+
28
+ import androidx.webkit.WebMessageCompat;
29
+ import androidx.webkit.WebMessagePortCompat;
30
+ import androidx.webkit.WebViewCompat;
31
+ import androidx.webkit.WebViewFeature;
32
+
33
+ import java.io.ByteArrayInputStream;
34
+ import java.io.File;
35
+ import java.io.IOException;
36
+ import java.io.InputStream;
37
+ import java.nio.charset.StandardCharsets;
38
+ import java.nio.file.Files;
39
+ import java.util.HashMap;
40
+ import java.util.HashSet;
41
+ import java.util.Map;
42
+ import java.util.Set;
43
+ import java.util.concurrent.ConcurrentHashMap;
44
+ import java.util.concurrent.atomic.AtomicInteger;
45
+
46
+ // An app's one activity: its page in a WebView, its module in Hermes behind JNI (libcrossbind_app.so). The page comes
47
+ // from an https origin the activity serves from the app's files, or in a debug app from a dev server; calls and their
48
+ // bytes travel over a message port that the page's top frame alone gets.
49
+ public class CrossbindActivity extends Activity {
50
+ static {
51
+ System.loadLibrary("crossbind_app");
52
+ }
53
+
54
+ private static final String TAG = "Crossbind";
55
+ private static final String HOST = "appassets.androidplatform.net";
56
+ private static final String APP_ORIGIN = "https://" + HOST;
57
+ // The object a page of the app's origins asks for its port with.
58
+ private static final String READY = "__crossbind_ready";
59
+
60
+ // A call of a page, answered on the port of the page that made it: a page that went away gets no answer.
61
+ private static final class Call {
62
+ final WebMessagePortCompat port;
63
+ final int id;
64
+
65
+ Call(WebMessagePortCompat port, int id) {
66
+ this.port = port;
67
+ this.id = id;
68
+ }
69
+ }
70
+
71
+ private static final Map<Integer, Call> calls = new ConcurrentHashMap<>();
72
+ private static final AtomicInteger nextCall = new AtomicInteger();
73
+
74
+ private WebView webView;
75
+ private Handler ipc;
76
+ private WebMessagePortCompat port;
77
+ private String token;
78
+ private final Set<String> origins = new HashSet<>();
79
+
80
+ // Starts the module, once per process, and returns the page's token and the dev server's page, or "".
81
+ private static native String[] nativeStart(String files, String devUrl);
82
+
83
+ private static native void nativeMessage(int call, byte[] text);
84
+
85
+ private static native void nativeFrame(int call, String query, byte[] frame, int bodyOffset);
86
+
87
+ private static native void nativeResult(int call, String query);
88
+
89
+ // A file of the page, its path in UTF-8: "<status> <mime>\n" and its bytes.
90
+ private static native byte[] nativeRequest(byte[] path, String query);
91
+
92
+ private static native void nativeNewPage();
93
+
94
+ // The module's answer to a call, from whichever thread gives it.
95
+ static void onReply(int call, boolean ok, byte[] text) {
96
+ Call to = calls.remove(call);
97
+ if (to != null) post(to.port, new WebMessageCompat(to.id + "|" + (ok ? "" : "!") + new String(text, StandardCharsets.UTF_8)));
98
+ }
99
+
100
+ // A large result: its bytes after four that take the call's id, little-endian.
101
+ static void onReplyBytes(int call, byte[] framed) {
102
+ Call to = calls.remove(call);
103
+ if (to == null) return;
104
+ for (int i = 0; i < 4; i += 1) framed[i] = (byte) (to.id >> (8 * i));
105
+ post(to.port, new WebMessageCompat(framed));
106
+ }
107
+
108
+ // Answers come from the module's thread and the port closes on the UI thread, never at once.
109
+ private static void post(WebMessagePortCompat port, WebMessageCompat message) {
110
+ synchronized (port) {
111
+ try {
112
+ port.postMessage(message);
113
+ } catch (IllegalStateException closed) {
114
+ // The page went away, and its port with it.
115
+ }
116
+ }
117
+ }
118
+
119
+ private static void close(WebMessagePortCompat port) {
120
+ synchronized (port) {
121
+ port.close();
122
+ }
123
+ }
124
+
125
+ @Override
126
+ protected void onCreate(Bundle state) {
127
+ super.onCreate(state);
128
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
129
+ // The page sends byte arrays as ArrayBuffer messages, which Android System WebView passes from 116 on.
130
+ if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)
131
+ || !WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
132
+ || !WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
133
+ PackageInfo installed = WebViewCompat.getCurrentWebViewPackage(this);
134
+ String text = "This app needs Android System WebView 116 or newer. This device has "
135
+ + (installed == null ? "none" : installed.versionName) + ".";
136
+ Log.e(TAG, text);
137
+ TextView message = new TextView(this);
138
+ message.setText(text);
139
+ setContentView(message);
140
+ return;
141
+ }
142
+ // Any app can start this activity with extras, so a debug app opens a dev server on this device alone, which adb
143
+ // reverse forwards, and a release app shows its console only where it is debuggable.
144
+ String url = getIntent().getStringExtra("CROSSBIND_APP_URL");
145
+ if (url != null && !Set.of("localhost", "127.0.0.1").contains(Uri.parse(url).getHost())) {
146
+ Log.w(TAG, "a dev server off this device is not opened: " + url);
147
+ url = null;
148
+ }
149
+ boolean isDebuggable = (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
150
+ String[] started = nativeStart(installFiles().getPath(), url);
151
+ token = started[0];
152
+ String devUrl = started[1];
153
+ origins.add(APP_ORIGIN);
154
+ if (!devUrl.isEmpty()) origins.add(originOf(Uri.parse(devUrl)));
155
+
156
+ HandlerThread thread = new HandlerThread("crossbind-ipc");
157
+ thread.start();
158
+ ipc = new Handler(thread.getLooper());
159
+ webView = new WebView(this);
160
+ webView.getSettings().setJavaScriptEnabled(true);
161
+ webView.getSettings().setDomStorageEnabled(true);
162
+ WebViewCompat.addDocumentStartJavaScript(webView, topFrameScript(token), origins);
163
+ WebViewCompat.addWebMessageListener(webView, READY, origins, (view, message, origin, isMainFrame, reply) -> {
164
+ if (isMainFrame) handOverPort(origin, message.getData());
165
+ });
166
+ webView.setWebViewClient(new PageClient());
167
+ if (!devUrl.isEmpty() || (isDebuggable && getIntent().hasExtra("CROSSBIND_APP_CONSOLE"))) webView.setWebChromeClient(new ConsoleClient());
168
+ // The page stays clear of the system bars, the display cutout and the keyboard.
169
+ FrameLayout frame = new FrameLayout(this);
170
+ frame.addView(webView);
171
+ frame.setOnApplyWindowInsetsListener((view, insets) -> {
172
+ Insets bars = insets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout() | WindowInsets.Type.ime());
173
+ view.setPadding(bars.left, bars.top, bars.right, bars.bottom);
174
+ return WindowInsets.CONSUMED;
175
+ });
176
+ setContentView(frame);
177
+ matchSystemBars();
178
+ webView.loadUrl(devUrl.isEmpty() ? APP_ORIGIN + "/" : devUrl);
179
+ }
180
+
181
+ @Override
182
+ public void onConfigurationChanged(Configuration config) {
183
+ super.onConfigurationChanged(config);
184
+ matchSystemBars();
185
+ }
186
+
187
+ // The system bars lie over the window, so their icons are dark in the light theme and light in the dark one.
188
+ private void matchSystemBars() {
189
+ boolean isDark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
190
+ int light = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
191
+ getWindow().getInsetsController().setSystemBarsAppearance(isDark ? 0 : light, light);
192
+ }
193
+
194
+ @Override
195
+ protected void onDestroy() {
196
+ if (port != null) close(port);
197
+ if (webView != null) webView.destroy();
198
+ if (ipc != null) ipc.getLooper().quitSafely();
199
+ super.onDestroy();
200
+ }
201
+
202
+ // Runs before the page's own scripts in every frame of the app's origins, and in the top frame alone gives the page
203
+ // its token and the port its crossbind.js waits for: the one that comes with the token, which no other frame knows,
204
+ // and with the ask of this page, which a port handed over for the page before cannot carry. The page asks for the
205
+ // port once it waits for it, as a port handed over before would find no one.
206
+ private static String topFrameScript(String token) {
207
+ return "if (window.top === window) {"
208
+ + " const token = '" + token + "';"
209
+ + " const ask = crypto.randomUUID();"
210
+ + " Object.defineProperty(globalThis, '__crossbind_token', { value: token });"
211
+ + " Object.defineProperty(globalThis, '__crossbind_port', { value: new Promise((resolve) => {"
212
+ + " addEventListener('message', function handshake(event) {"
213
+ + " if (event.data !== 'crossbind:' + token + ':' + ask || !event.ports[0]) return;"
214
+ + " removeEventListener('message', handshake);"
215
+ + " resolve(event.ports[0]);"
216
+ + " });"
217
+ + " " + READY + ".postMessage(ask);"
218
+ + " }) });"
219
+ + "}";
220
+ }
221
+
222
+ private static String originOf(Uri url) {
223
+ return url.getScheme() + "://" + url.getHost() + (url.getPort() == -1 ? "" : ":" + url.getPort());
224
+ }
225
+
226
+ private boolean allows(Uri url) {
227
+ if ("about".equals(url.getScheme())) return true;
228
+ // A blob URL carries the origin of the page that made it.
229
+ if ("blob".equals(url.getScheme())) return allows(Uri.parse(url.getSchemeSpecificPart()));
230
+ return origins.contains(originOf(url));
231
+ }
232
+
233
+ // "<id>|m|<message>" or "<id>|r|<query>", or as an ArrayBuffer "<header length><id>|<query>" and the bytes.
234
+ private static void receive(WebMessagePortCompat from, WebMessageCompat message) {
235
+ try {
236
+ if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
237
+ byte[] frame = message.getArrayBuffer();
238
+ int headerLength = (frame[0] & 255) | (frame[1] & 255) << 8 | (frame[2] & 255) << 16 | (frame[3] & 255) << 24;
239
+ String header = new String(frame, 4, headerLength, StandardCharsets.UTF_8);
240
+ int bar = header.indexOf('|');
241
+ nativeFrame(callOf(from, header.substring(0, bar)), header.substring(bar + 1), frame, 4 + headerLength);
242
+ return;
243
+ }
244
+ String data = message.getData();
245
+ int bar = data.indexOf('|');
246
+ int call = callOf(from, data.substring(0, bar));
247
+ String rest = data.substring(bar + 3);
248
+ if (data.charAt(bar + 1) == 'r') nativeResult(call, rest);
249
+ else nativeMessage(call, rest.getBytes(StandardCharsets.UTF_8));
250
+ } catch (RuntimeException malformed) {
251
+ // Only the page's own code could send one in another form, which is no reason to end the app.
252
+ Log.e(TAG, "a message on crossbind's port is in no form it takes", malformed);
253
+ }
254
+ }
255
+
256
+ private static int callOf(WebMessagePortCompat port, String id) {
257
+ int call = nextCall.getAndIncrement();
258
+ calls.put(call, new Call(port, Integer.parseInt(id)));
259
+ return call;
260
+ }
261
+
262
+ private final class PageClient extends WebViewClient {
263
+ // Only the app's origins load; a link the user follows to another opens in the browser.
264
+ @Override
265
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
266
+ Uri url = request.getUrl();
267
+ if (allows(url)) return false;
268
+ if (request.hasGesture() && Set.of("http", "https", "mailto").contains(url.getScheme())) {
269
+ try {
270
+ startActivity(new Intent(Intent.ACTION_VIEW, url));
271
+ } catch (ActivityNotFoundException none) {
272
+ Log.w(TAG, "nothing opens " + url);
273
+ }
274
+ }
275
+ return true;
276
+ }
277
+
278
+ // The page's files. Calls and their bytes travel over the port alone, which no other frame holds.
279
+ @Override
280
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
281
+ Uri url = request.getUrl();
282
+ if (!"https".equals(url.getScheme()) || !HOST.equals(url.getHost())) return null;
283
+ String path = url.getPath() == null ? "/" : url.getPath();
284
+ if (path.startsWith("/b/")) return response("404 text/plain\n".getBytes(StandardCharsets.US_ASCII));
285
+ String query = url.getEncodedQuery();
286
+ return response(nativeRequest(path.getBytes(StandardCharsets.UTF_8), query == null ? "" : query));
287
+ }
288
+
289
+ }
290
+
291
+ // A new page replaced the one whose objects the module holds, and gets a port of its own: the old page's port is
292
+ // closed, so an answer to a call it made goes nowhere.
293
+ private void handOverPort(Uri origin, String ask) {
294
+ if (port != null) close(port);
295
+ nativeNewPage();
296
+ WebMessagePortCompat[] channel = WebViewCompat.createWebMessageChannel(webView);
297
+ port = channel[0];
298
+ port.setWebMessageCallback(ipc, new WebMessagePortCompat.WebMessageCallbackCompat() {
299
+ @Override
300
+ public void onMessage(WebMessagePortCompat from, WebMessageCompat message) {
301
+ receive(from, message);
302
+ }
303
+ });
304
+ WebViewCompat.postWebMessage(webView, new WebMessageCompat("crossbind:" + token + ":" + ask, new WebMessagePortCompat[] {channel[1]}),
305
+ origin);
306
+ }
307
+
308
+ private static final class ConsoleClient extends WebChromeClient {
309
+ @Override
310
+ public boolean onConsoleMessage(ConsoleMessage message) {
311
+ int priority = switch (message.messageLevel()) {
312
+ case ERROR -> Log.ERROR;
313
+ case WARNING -> Log.WARN;
314
+ default -> Log.INFO;
315
+ };
316
+ Log.println(priority, TAG, message.message());
317
+ return true;
318
+ }
319
+ }
320
+
321
+ private static WebResourceResponse response(byte[] answer) {
322
+ int newline = 0;
323
+ while (answer[newline] != '\n') newline += 1;
324
+ String[] head = new String(answer, 0, newline, StandardCharsets.US_ASCII).split(" ", 2);
325
+ int status = Integer.parseInt(head[0]);
326
+ String mime = head[1].split(";", 2)[0];
327
+ InputStream body = new ByteArrayInputStream(answer, newline + 1, answer.length - newline - 1);
328
+ return new WebResourceResponse(mime, "utf-8", status, status == 200 ? "OK" : "Not Found", new HashMap<>(), body);
329
+ }
330
+
331
+ // The module reads the app's files from disk, so they are copied out of the APK once per install, where no backup
332
+ // takes them.
333
+ private File installFiles() {
334
+ File dir = new File(getNoBackupFilesDir(), "app");
335
+ File stamp = new File(dir, ".installed");
336
+ try {
337
+ String install = String.valueOf(getPackageManager()
338
+ .getPackageInfo(getPackageName(), PackageManager.PackageInfoFlags.of(0)).lastUpdateTime);
339
+ if (stamp.exists() && install.equals(new String(Files.readAllBytes(stamp.toPath()), StandardCharsets.UTF_8))) return dir;
340
+ delete(dir);
341
+ copyAssets("app", dir);
342
+ Files.write(stamp.toPath(), install.getBytes(StandardCharsets.UTF_8));
343
+ return dir;
344
+ } catch (IOException | PackageManager.NameNotFoundException error) {
345
+ throw new IllegalStateException("crossbind: cannot install the app's files: " + error.getMessage(), error);
346
+ }
347
+ }
348
+
349
+ private void copyAssets(String path, File to) throws IOException {
350
+ String[] names = getAssets().list(path);
351
+ if (names == null || names.length == 0) {
352
+ to.getParentFile().mkdirs();
353
+ try (InputStream in = getAssets().open(path)) {
354
+ Files.copy(in, to.toPath());
355
+ }
356
+ return;
357
+ }
358
+ for (String name : names) copyAssets(path + "/" + name, new File(to, name));
359
+ }
360
+
361
+ private static void delete(File file) {
362
+ File[] children = file.listFiles();
363
+ if (children != null) for (File child : children) delete(child);
364
+ file.delete();
365
+ }
366
+ }
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- A debug app loads its page from a dev server on the machine, which adb reverse forwards; nothing else goes cleartext. -->
3
+ <network-security-config>
4
+ <domain-config cleartextTrafficPermitted="true">
5
+ <domain includeSubdomains="false">localhost</domain>
6
+ <domain includeSubdomains="false">127.0.0.1</domain>
7
+ </domain-config>
8
+ </network-security-config>
@@ -0,0 +1,14 @@
1
+ // The Android Gradle plugin's own dependencies with known vulnerabilities, at their fixed releases.
2
+ buildscript {
3
+ configurations.classpath {
4
+ resolutionStrategy.eachDependency {
5
+ if (requested.group == 'org.bouncycastle') useVersion '1.85'
6
+ if (requested.group == 'org.bitbucket.b_c' && requested.name == 'jose4j') useVersion '0.9.6'
7
+ if (requested.group == 'org.jdom' && requested.name == 'jdom2') useVersion '2.0.6.1'
8
+ }
9
+ }
10
+ }
11
+
12
+ plugins {
13
+ id 'com.android.application' version '9.4.1' apply false
14
+ }
@@ -0,0 +1,10 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
4
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
5
+ networkTimeout=10000
6
+ retries=0
7
+ retryBackOffMs=500
8
+ validateDistributionUrl=true
9
+ zipStoreBase=GRADLE_USER_HOME
10
+ zipStorePath=wrapper/dists
@@ -0,0 +1,2 @@
1
+ android.useAndroidX=true
2
+ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8