aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/assets/bridge
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-02 18:21:26 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-03 14:24:54 +0200
commitd8f5005ba2c9a249ce5ce8976d9c3754a3a7458e (patch)
tree960ece9a79c58fbe60033b91f6ec685795eff8a1 /packages/meshbay-android/app/src/main/assets/bridge
parentede70b0fcc006d3ced8ef0d1d7115593f6627aef (diff)
downloadmeshbay-d8f5005ba2c9a249ce5ce8976d9c3754a3a7458e.tar.gz
feat(android): downloads to disk and uploads through the system picker
Native save over the Storage Access Framework and MediaStore, chunks sent as binary bridge messages, a chosen folder that has gone asks rather than redirects, unfinished files removed on abort and after a killed process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-android/app/src/main/assets/bridge')
-rw-r--r--packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js58
1 files changed, 55 insertions, 3 deletions
diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
index c6e1f53..5fb5bb6 100644
--- a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
+++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
@@ -15,8 +15,9 @@
* that refuses: `platform.js` decides what to show from whether an object
* exists (`platform.node.available`, `platform.folder.available`, …).
*
- * `HUB_BASE` is prepended by the shell when it injects this file: the
- * interface asks for it while its modules load, before anything can await.
+ * `HUB_BASE` and `BINARY` are prepended by the shell when it injects this
+ * file: the interface asks for the hub while its modules load, before anything
+ * can await, and BINARY says whether the WebView carries ArrayBuffer messages.
*/
(function () {
'use strict';
@@ -43,6 +44,34 @@
port.postMessage(JSON.stringify({ id, ch: channel, args }));
});
+ // A write is a binary message: "MBB1" | id | channel | 0 | handle | bytes,
+ // big-endian — one copy, no JSON, no base64 (spike S-3: 136 MB/s awaited
+ // per 48 KB chunk). Without ArrayBuffer messages, base64 over JSON.
+ const SAVE_WRITE = 1;
+ const bytesOf = (chunk) => (chunk instanceof Uint8Array ? chunk
+ : ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
+ : new Uint8Array(chunk));
+ const writeChunk = (handle, chunk) => {
+ const bytes = bytesOf(chunk);
+ if (!BINARY) {
+ let s = '';
+ for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
+ return call('save:write', handle, btoa(s));
+ }
+ return new Promise((resolve, reject) => {
+ const id = ++seq;
+ pending.set(id, { resolve, reject });
+ const frame = new ArrayBuffer(16 + bytes.length);
+ const head = new DataView(frame);
+ head.setUint32(0, 0x4d424231); // "MBB1"
+ head.setUint32(4, id);
+ head.setUint16(8, SAVE_WRITE);
+ head.setUint32(12, handle);
+ new Uint8Array(frame, 16).set(bytes);
+ port.postMessage(frame);
+ });
+ };
+
const meshbay = {
hubBase: () => HUB_BASE,
setHubBase: (base) => call('hub:set', base),
@@ -50,7 +79,7 @@
capabilities: {
nodeAdmin: false, // no node runs on a phone (§11.3)
localFolders: false,
- nativeSave: false, // phase 2
+ nativeSave: true,
lanCast: false, // phase 3
tray: false,
},
@@ -102,6 +131,29 @@
secrets: {
backend: () => call('secrets:backend'),
},
+
+ // Where downloads go, chosen once. A display name comes back, never a URI.
+ folder: {
+ choose: () => call('folder:choose'),
+ get: () => call('folder:get'),
+ forget: () => call('folder:forget'),
+ },
+
+ // A sink that writes to disk as chunks arrive, never a buffer handed over
+ // at the end. The page holds an id. `open` exists only where the target
+ // says the file may be opened — a type that runs nothing.
+ saveFile: async (suggestedName, opts) => {
+ const handle = await call('save:begin', suggestedName, opts);
+ if (!handle) return null;
+ const sink = {
+ name: handle.name,
+ write: (chunk) => writeChunk(handle.id, chunk),
+ close: () => call('save:end', handle.id),
+ abort: () => call('save:abort', handle.id),
+ };
+ if (handle.openable) sink.open = () => call('save:open', handle.id);
+ return sink;
+ },
};
const freeze = (o) => {