diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-02 18:21:26 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-03 14:24:54 +0200 |
| commit | d8f5005ba2c9a249ce5ce8976d9c3754a3a7458e (patch) | |
| tree | 960ece9a79c58fbe60033b91f6ec685795eff8a1 /packages/meshbay-android/app/src/main/assets | |
| parent | ede70b0fcc006d3ced8ef0d1d7115593f6627aef (diff) | |
| download | meshbay-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')
| -rw-r--r-- | packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js | 58 |
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) => { |