JavaScript bridge API
Files: one file at a time
The system picker and chunked binary read/write, for files too large to hold in a string.
AppMint.downloadFile helper#
AppMint.downloadFile(data, fileName, mimeType?)
Example
Saves a file your page made - from a base64 string, a data: URL, a blob: URL or a Blob - with the file name you choose. Android's "Save as…" sheet opens with that name filled in.
Returns: (sync) true once the save was handed to Android (false only for a data: URL it could not parse). Throws TypeError if data is not a Blob or string. The user still picks the folder, and can cancel. Needs: nothing - it is always present in the app.
Base64 string plus a MIME type:
function exportCsv(rows) {
const csv = rows.map(function (r) { return r.join(','); }).join('\n');
const b64 = btoa(unescape(encodeURIComponent(csv))); // UTF-8 safe
if (window.AppMint && typeof AppMint.downloadFile === 'function') {
AppMint.downloadFile(b64, 'Ledger_Q3.csv', 'text/csv');
} else {
const a = document.createElement('a'); // a normal browser
a.href = 'data:text/csv;base64,' + b64;
a.download = 'Ledger_Q3.csv';
a.click();
}
}A data: URL (the MIME type comes from the URL) or a Blob:
const png = document.querySelector('canvas').toDataURL('image/png');
AppMint.downloadFile(png, 'Chart.png');
const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
AppMint.downloadFile(blob, 'Backup.json'); // same as AppMint.saveBlobNotes: Only the last part of fileName is used (no folders). A name without an extension gets one from the MIME type. The third argument is used only for a plain base64 string. If the phone has no "Save as…" screen at all, the file is written straight into Downloads. Your normal <a download> code also works - see downloads.
AppMint.saveBlob helper#
AppMint.saveBlob(blob, fileName)
Example
Saves a Blob (or File) with the name you choose. Android's "Save as…" sheet opens with that name filled in.
Returns: (sync) true once the save was handed to Android. Throws TypeError when the first argument is not a Blob. Needs: nothing - it is always present in the app.
async function saveReport() {
const blob = await makePdfBlob(); // e.g. jsPDF: doc.output('blob')
if (window.AppMint && typeof AppMint.saveBlob === 'function') {
AppMint.saveBlob(blob, 'Site_Report_2026-08.pdf');
return;
}
const url = URL.createObjectURL(blob); // a normal browser
const a = document.createElement('a');
a.href = url;
a.download = 'Site_Report_2026-08.pdf';
a.click();
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
}A File brings its own name, so the name can be left out:
const file = new File(['name,score\n'], 'Scores.csv', { type: 'text/csv' });
AppMint.saveBlob(file); // saved as Scores.csvNotes: The Blob is read into memory and sent to Android in one piece, so very large blobs (hundreds of MB) may run out of memory. The user picks the folder and may cancel; nothing tells the page which one happened.
AppMint.setDownloadName helper#
AppMint.setDownloadName(url, fileName)
Example
Gives a blob: or data: URL the file name it should be saved under, before your code (or a library) triggers the download.
Returns: nothing. Needs: nothing - it is always present in the app.
Useful when a library downloads a URL you built earlier and you cannot pass it a name:
const blob = new Blob([csvText], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
AppMint.setDownloadName(url, 'Invoice_0042.csv');
}
location.href = url; // saved as Invoice_0042.csv, not "download.csv"Notes: The name is remembered even after URL.revokeObjectURL(url), because some libraries revoke before the save finishes. You rarely need this: <a download="name">, URL.createObjectURL(file) (a File has a name) and FileSaver's saveAs(blob, name) are all picked up automatically. The name 'download' is ignored as "no name".
WebToApkFS.maxChunkSize helper#
WebToApkFS.maxChunkSize()
Example
Tells you the most bytes one readBytes or writeBytes call can move (4 MB), so you can size your chunk loop.
Returns: (sync) a number of bytes, or 0 when the bridge method is missing. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build (that is what injects WebToApkFS).
function copyFile(fromUri, toPath) {
if (!window.WebToApkFS) return { ok: false, error: 'not in the app' };
const step = WebToApkFS.maxChunkSize() || 1048576; // fall back to 1 MB pieces
let offset = 0;
for (;;) {
const r = WebToApkFS.readBytes(fromUri, offset, step);
if (!r.ok) return r;
if (r.bytesRead > 0) {
const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
if (!w.ok) return w;
}
offset += r.bytesRead;
if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
}
}Notes: A smaller chunk keeps the page more responsive (each call is synchronous); the cap only says how big one call may be.
WebToApkFS.pickFile helper#
WebToApkFS.pickFile(mimeFilter)
Example
Opens the Android file picker so the user chooses ONE file; your page may then read and write that file's real bytes, even after the app restarts.
Returns: Promise<{ok, uri, name, size, mime, error}>. On success ok is true and uri is a content:// address you pass to stat, readBytes, writeBytes. On failure ok is false and error is 'cancelled', 'disabled', 'no-picker', 'busy' (a picker is already open for an earlier call, which keeps its own answer), 'foreign_origin' or 'unsupported' (no app). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
async function openOneFile() {
if (!window.WebToApkFS) { alert('Open this page in the app to pick a file.'); return; }
const f = await WebToApkFS.pickFile('application/pdf'); // '' = any file, 'image/*' = pictures
if (!f.ok) {
if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
return;
}
console.log(f.name, f.size + ' bytes', f.mime);
localStorage.setItem('lastFile', f.uri); // the grant survives a restart
}Use the saved uri again later, without asking the user a second time:
const uri = localStorage.getItem('lastFile');
if (uri && window.WebToApkFS) {
const info = WebToApkFS.stat(uri);
if (info.ok) console.log('still here:', info.name);
else console.log('file is gone or access was removed:', info.error);
}Notes: The filter is one MIME type pattern ('image/*', 'text/csv'), not a list. The picker asks for read AND write access and keeps it. Android limits how many files an app may keep this way, so call WebToApkFS.releaseFile(uri) when you are done with a file. For a whole folder use WebToApkFS.requestAccess() instead. WebToApkFS is installed when Native Folder Access or Secure Keys is ticked; its calls work only with Native Folder Access (WebToApkFS.isAvailable()).
WebToApkFS.readAll helper#
WebToApkFS.readAll(pathOrUri)
Example
Reads a whole file into one base64 string. It loops over readBytes for you, 4 MB at a time.
Returns: (sync) {ok:true, base64, size}, or the first failing chunk's {ok:false, error}. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
Load a picked image into an <img>:
async function showPicture() {
if (!window.WebToApkFS) return;
const f = await WebToApkFS.pickFile('image/*');
if (!f.ok) return;
const r = WebToApkFS.readAll(f.uri);
if (!r.ok) { alert('Read failed: ' + r.error); return; }
document.getElementById('photo').src = 'data:' + f.mime + ';base64,' + r.base64;
}Read a UTF-8 text file (works for any language, not only English):
function readUtf8(pathOrUri) {
const r = WebToApkFS.readAll(pathOrUri);
if (!r.ok) throw new Error(r.error);
const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
return new TextDecoder('utf-8').decode(bytes);
}Notes: The whole file ends up in memory (as bytes and then as base64), and the call blocks the page while it runs. Use it for files that easily fit in memory; for large files loop WebToApkFS.readBytes and handle each chunk. For plain text inside the granted folder, WebToApkFS.readText(path) is simpler.
WebToApkFS.readBytes helper#
WebToApkFS.readBytes(pathOrUri, offset, length)
Example
Reads the real bytes of a file, starting at a byte position, as base64. Safe for images, PDFs, ZIPs - nothing is changed by text decoding.
Returns: (sync) {ok:true, base64, bytesRead, offset, size, eof} or {ok:false, error} ('not-found', 'disabled', 'cannot-open', 'offset past end of file (N bytes)', 'foreign_origin', 'unsupported'). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
Read the first 64 KB, for example to check a file header:
async function checkPng() {
if (!window.WebToApkFS) return;
const f = await WebToApkFS.pickFile('image/*');
if (!f.ok) return;
const r = WebToApkFS.readBytes(f.uri, 0, 65536);
if (!r.ok) { alert('Read failed: ' + r.error); return; }
const head = atob(r.base64).slice(1, 4);
console.log(head === 'PNG' ? 'a PNG' : 'not a PNG', '- file is', r.size, 'bytes');
}Read a big file piece by piece, so it never has to fit in memory at once:
function eachChunk(uri, onChunk) {
const step = WebToApkFS.maxChunkSize() || 1048576;
let offset = 0;
for (;;) {
const r = WebToApkFS.readBytes(uri, offset, step);
if (!r.ok) return r; // stop on the first error
onChunk(r.base64, offset);
offset += r.bytesRead;
if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
}
}Notes: One call reads at most WebToApkFS.maxChunkSize() bytes (4 MB); length 0 means "as much as one call allows". Loop on bytesRead and stop on eof. The call is synchronous, so keep chunks moderate on the UI thread. For small files WebToApkFS.readAll(uri) does the loop for you.
WebToApkFS.readSealed helper#
WebToApkFS.readSealed(pathOrUri, alias)
Example
Reads a file written by WebToApkFS.writeSealed and decrypts it with the same keystore key.
Returns: (sync) {ok:true, base64} (the original bytes), or {ok:false, error} - 'keystore-disabled', 'corrupt-sealed-file', a keystore code ('no-such-key' after the key was deleted, 'auth-required', 'key-invalidated') or a file error ('not-found' …). Needs: tick Native Folder Access (SAF) and Secure Keys (encryption) in Step 4 (Access) when you build.
function loadSecret() {
if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return null;
const out = WebToApkFS.readSealed('secret.bin', 'vault');
if (out.ok) {
const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
return new TextDecoder().decode(bytes);
}
if (out.error === 'auth-required') {
alert('Unlock your phone, then try again.');
} else if (out.error === 'no-such-key') {
alert('This note was deleted.');
} else {
alert('Could not open the note: ' + out.error);
}
return null;
}Notes: It fails for good once AppMintKeystore.deleteKey(alias) has run - that is the point. The whole file is read and decrypted in memory, synchronously; keep sealed files small (notes, keys, tokens).
WebToApkFS.releaseFile helper#
WebToApkFS.releaseFile(uri)
Example
Gives back the long-term access your app got when the user picked a file with WebToApkFS.pickFile.
Returns: (sync) true when the grant was released, false otherwise (no grant, bad URI, not in the app). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
async function importOnce() {
if (!window.WebToApkFS) return;
const f = await WebToApkFS.pickFile('text/csv');
if (!f.ok) return;
const r = WebToApkFS.readAll(f.uri);
if (r.ok) importCsv(atob(r.base64));
WebToApkFS.releaseFile(f.uri); // done with it: do not keep the grant
}Notes: Android caps how many picked files one app may keep access to. An app that picks many files (an importer, a viewer) should release each one when finished. After release, stat / readBytes / writeBytes on that URI answer not-found. It does not delete the file.
WebToApkFS.stat helper#
WebToApkFS.stat(pathOrUri)
Example
Tells you a file's name, size, type and date without reading its contents.
Returns: (sync) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, or {ok:false, error} with error = 'not-found', 'disabled', 'foreign_origin' or 'unsupported'. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
async function showInfo() {
if (!window.WebToApkFS) return;
const f = await WebToApkFS.pickFile('');
if (!f.ok) return;
const info = WebToApkFS.stat(f.uri);
if (!info.ok) { alert('Cannot read file info: ' + info.error); return; }
console.log(info.name, info.size + ' bytes', info.mime,
new Date(info.lastModified).toLocaleString(),
info.canWrite ? 'writable' : 'read only');
}A path inside the folder the user granted with WebToApkFS.requestAccess() works too:
const info = WebToApkFS.stat('photos/cover.jpg');
if (info.ok && !info.isDirectory) console.log('cover is', info.size, 'bytes');Notes: pathOrUri is a content:// URI from pickFile() or a path relative to the granted folder. A URI your app was never given returns not-found. lastModified is milliseconds since 1970 (0 when the provider does not say).
WebToApkFS.writeAll helper#
WebToApkFS.writeAll(pathOrUri, base64)
Example
Replaces a file's whole contents with your bytes. It splits the data into 4 MB pieces for you: the first piece truncates the file, the rest are patched in after it.
Returns: (sync) {ok:true, bytesWritten}, or the first failing piece's {ok:false, error}. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
Save UTF-8 text (any language) back to a file the user picked:
function bytesToBase64(bytes) {
let s = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(s);
}
async function saveNote(text) {
if (!window.WebToApkFS) return;
const f = await WebToApkFS.pickFile('text/plain');
if (!f.ok) return;
const r = WebToApkFS.writeAll(f.uri, bytesToBase64(new TextEncoder().encode(text)));
if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
else alert('Could not save: ' + r.error);
}Write a canvas image into the granted folder (the file is created if missing):
const b64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
const r = WebToApkFS.writeAll('drawings/sketch.png', b64);
if (!r.ok) alert('Could not save: ' + r.error);Notes: The whole input sits in memory while it is written, and the call blocks the page. If a later piece fails, the file already holds the earlier pieces - check ok and tell the user. To keep a file secret, use WebToApkFS.writeSealed instead.
WebToApkFS.writeBytes helper#
WebToApkFS.writeBytes(pathOrUri, base64, offset, mode)
Example
Writes real bytes into a file and tells you how many landed. It can patch bytes in the middle, replace the whole file, or append to the end.
Returns: (sync) {ok, bytesWritten, offset, size} - size is read back from the file after the data is flushed to disk - or {ok:false, error}. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
Patch - change 4 bytes at position 100, keep the rest of the file:
const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
else alert('Write failed: ' + r.error);Truncate - replace the whole file (offset is ignored):
const csv = 'name,score\nAsha,12\n';
const r = WebToApkFS.writeBytes('exports/scores.csv', btoa(csv), 0, 'truncate');
if (!r.ok) alert('Could not save: ' + r.error);Append - add to the end (offset is ignored):
const line = new Date().toISOString() + ' opened\n';
WebToApkFS.writeBytes('log.txt', btoa(line), 0, 'append');Notes: mode is 'patch' (default), 'truncate' or 'append'. At most 4 MB per call (WebToApkFS.maxChunkSize()); for bigger data use WebToApkFS.writeAll. A path inside the granted folder is created if it does not exist; a picked content:// URI must be one the user chose. btoa only accepts Latin-1 text - for other text encode to bytes first (see WebToApkFS.writeAll). Errors: disabled, foreign_origin (the page on screen is not the app's own), bad mode: x, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.
WebToApkFS.writeSealed helper#
WebToApkFS.writeSealed(pathOrUri, base64, alias)
Example
Writes a file encrypted with a hardware-backed key from AppMintKeystore. Deleting that key later makes the file unreadable for everyone - the only real "secure delete" on a phone.
Returns: (sync) {ok:true, bytesWritten} (encrypted size on disk), or {ok:false, error} - 'keystore-disabled', a keystore code such as 'no-such-key', 'auth-required', 'key-invalidated', or a file error. Needs: tick Native Folder Access (SAF) and Secure Keys (encryption) in Step 4 (Access) when you build.
function saveSecret(text) {
if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) {
alert('Secure storage is not available in this build.');
return false;
}
if (!AppMintKeystore.hasKey('vault')) {
const k = AppMintKeystore.generateKey('vault', {});
if (!k.ok) { alert('Could not create key: ' + k.error); return false; }
}
const bytes = new TextEncoder().encode(text);
let bin = '';
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
const r = WebToApkFS.writeSealed('secret.bin', btoa(bin), 'vault');
if (!r.ok) alert('Could not save: ' + r.error);
return r.ok;
}Delete it for real - destroy the key, then remove the file:
AppMintKeystore.deleteKey('vault'); // secret.bin can never be decrypted again
WebToApkFS.delete('secret.bin');Notes: Do not "overwrite with zeros" to erase: phone flash storage keeps the old blocks. The file format is a list of length-prefixed AES-GCM pieces; read it back only with WebToApkFS.readSealed and the same alias. A key created with {requireAuth:true} answers auth-required until the user has unlocked recently.
downloads web standard#
<a download> and Blob downloads
Example
Your normal web download code works in the app. A WebView ignores blob: and data: downloads on its own; the app catches them and opens Android's "Save as…" sheet with your file name.
Returns: nothing to the page (the user picks a folder or cancels). Needs: nothing to switch on. Web-address downloads (https://…) go to the Downloads folder (a PDF or image first asks "View or Download"); on Android 9 and older the app asks for storage permission first.
<a download> with a Blob - the most common pattern:
<button id="save">Download report</button>
<script>
document.getElementById('save').addEventListener('click', function () {
const blob = new Blob(['Name,Total\nAsha,120\n'], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report_2026-08.csv'; // this is the name the user sees
document.body.appendChild(a);
a.click(); // caught by the app, "Save as…" opens
a.remove();
});
</script>A data: URL, FileSaver.js, and navigator.msSaveBlob also work:
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'Drawing.png';
a.click();
if (typeof saveAs === 'function') saveAs(blob, 'Backup.json'); // FileSaver.js
if (navigator.msSaveBlob) navigator.msSaveBlob(blob, 'Backup.json');window.open of a blob: or data: URL means "show it", not "save it": a PDF opens in the app's PDF viewer, other types in the phone's app for that type:
const pdfUrl = URL.createObjectURL(pdfBlob);
const w = window.open(pdfUrl); // never null in the app, so no "popup blocked" messageNotes: Without a name the file is saved as download.<ext>. A File object brings its own name (URL.createObjectURL(file)). If no app on the phone can show a window.open file, the "Save as…" sheet opens instead. To save without an <a> element, use AppMint.saveBlob(blob, name) or AppMint.downloadFile(base64, name, mime).
input-file web standard#
<input type="file"> — camera, photos and files
Example
A normal file input works in the app like in Chrome: it opens the photo picker, the camera, or the file picker, and your page gets ordinary File objects.
Returns: the input's change event with input.files (empty when the user cancels). Needs: photos and files need nothing. For capture (the camera) tick Camera in Step 4 (Access) when you build; the app asks for the camera permission the first time.
Photos - opens the Android photo picker (no permission prompt; the page gets only what the user chose):
<input type="file" id="pics" accept="image/*" multiple>
<div id="preview"></div>
<script>
document.getElementById('pics').addEventListener('change', function (e) {
const box = document.getElementById('preview');
for (let i = 0; i < e.target.files.length; i++) {
const img = document.createElement('img');
img.src = URL.createObjectURL(e.target.files[i]);
img.style.width = '100px';
box.appendChild(img);
}
});
</script>Camera - capture skips the picker and opens the camera straight away:
<input type="file" id="snap" accept="image/*" capture="environment">
<input type="file" id="clip" accept="video/*" capture="environment">
<script>
document.getElementById('snap').addEventListener('change', function (e) {
const photo = e.target.files[0];
if (!photo) return; // user closed the camera
const form = new FormData();
form.append('photo', photo);
fetch('/api/upload', { method: 'POST', body: form });
});
</script>Any file, or several types (extensions work too):
<input type="file" accept=".pdf,.csv,application/json" multiple>| You write | What opens |
|---|---|
accept="image/*" / "video/*" / "image/*,video/*" | the photo picker (photos, videos, or both) |
add multiple | the user can pick several |
add capture (with Camera ticked) | the camera directly - photo for image/*, video for video/* |
accept="image/*,.pdf" or no accept | the system file picker |
Notes: Without Camera ticked, capture is ignored and the picker opens. When Native Folder Access (SAF) is ticked, the app shows its own "Select Upload Source" menu instead: Take Photo / Record Video (if Camera is ticked), Choose Files, and Upload Folder (sends every file in a folder). With Storage ticked on Android 12 and older, choosing a non-photo file may ask for storage permission first. Phones without the photo picker get the file picker, which shows the same photos.
showOpenFilePicker web standard#
showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access
Example
The standard File System Access API works inside the app, over Android's own document picker: open a file and save back INTO THAT FILE, create a file with a Save-as sheet, or get a whole folder to read, create and delete files in.
Returns: showOpenFilePicker(options) → Promise of an array of FileSystemFileHandle; showSaveFilePicker(options) → one FileSystemFileHandle; showDirectoryPicker(options) → one FileSystemDirectoryHandle. A cancelled picker rejects AbortError; no user gesture rejects SecurityError; a second picker while one is open rejects NotAllowedError. Needs: nothing to switch on and no permission - the user picks.
let handle = null;
document.getElementById('open').addEventListener('click', async () => {
try {
[handle] = await showOpenFilePicker({
types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }]
});
const file = await handle.getFile();
editor.value = await file.text();
title.textContent = file.name;
} catch (e) {
if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
}
});
document.getElementById('save').addEventListener('click', async () => {
if (!handle) handle = await showSaveFilePicker({ suggestedName: 'note.txt' });
const w = await handle.createWritable(); // writes go to a copy…
await w.write(editor.value);
await w.close(); // …that replaces the file only here
});
document.getElementById('export').addEventListener('click', async () => {
const dir = await showDirectoryPicker({ mode: 'readwrite' });
const out = await dir.getFileHandle('export.csv', { create: true });
const w = await out.createWritable();
await w.write(csvText);
await w.close();
for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
});Notes: createWritable() follows the spec's swap-file model - abort() (or the page going away) leaves the original untouched; { keepExistingData: true } starts from the current content; write({type:'seek'|'truncate'|'write', …}), seek() and truncate() work. A file its app offers read-only answers queryPermission({mode:'readwrite'}) with 'denied' and createWritable() rejects NoModificationAllowedError. getFile() streams the document, so large files are fine. Handles last as long as the page - they cannot be stored in IndexedDB and reused after a restart. types become the picker's file filter. Only the top-level page has the pickers. Internal transports: __fsPick (answered with the appmint:fs event), __fsFile, __fsList, __fsChild, __fsRemove, __fsWritableOpen/Write/Truncate/Close/Abort. AI-built apps check can('fileHandles').
__fsChild bridge#
window.WebToApk.__fsChild(uri: String, name: String, kind: String, create: Boolean): String
getFileHandle / getDirectoryHandle (with `create`).
Example
Internal transport behind getFileHandle(name, {create}) and getDirectoryHandle(name, {create}). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok, kind, name, uri, canWrite} or {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError). Needs: a folder from showDirectoryPicker().
Use the public API:
const dir = await showDirectoryPicker();
const photos = await dir.getDirectoryHandle('photos', { create: true });
const file = await photos.getFileHandle('list.json', { create: true });Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsFile bridge#
window.WebToApk.__fsFile(uri: String): String
getFile(): `{ok, name, type, size, lastModified, url}` (the body streams from url).
Example
Internal transport behind FileSystemFileHandle.getFile(). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok, name, type, size, lastModified, url} - the body streams from url (the app's own origin, with ranges) - or {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin). Needs: a handle from one of the pickers.
Use the public API:
const [handle] = await showOpenFilePicker();
const file = await handle.getFile();
console.log(file.name, file.size, await file.text());Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsList bridge#
window.WebToApk.__fsList(uri: String): String
A directory handle's entries: `{ok, entries:[{kind, name, uri, canWrite}]}`.
Example
Internal transport behind iterating a FileSystemDirectoryHandle (entries(), keys(), values(), for await). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok, entries:[{kind, name, uri, canWrite}]} or {ok:false, error}. Needs: a folder from showDirectoryPicker().
Use the public API:
const dir = await showDirectoryPicker();
for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsPick bridge#
window.WebToApk.__fsPick(requestId: String, kind: String, optionsJson: String): String
Opens the [kind] picker (open | save | directory); handles arrive as `appmint:fs`.
Example
Internal transport behind showOpenFilePicker(), showSaveFilePicker() and showDirectoryPicker(). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: {ok:true} once the picker is on screen, or {ok:false, error} (NotAllowedError when one is already open, foreign_origin). The handles arrive as the appmint:fs event {requestId, handles:[{kind, name, uri, canWrite}]} or {requestId, error:'cancelled'}. Needs: nothing to switch on.
Use the public API:
const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': ['.png', '.jpg'] } }] });
const file = await handle.getFile();Notes: Implemented in the app shell (FileSystemAccess.kt). The same page code works unchanged in desktop Chrome.
__fsRemove bridge#
window.WebToApk.__fsRemove(uri: String, name: String, recursive: Boolean): String
removeEntry(name, {recursive}).
Example
Internal transport behind FileSystemDirectoryHandle.removeEntry(name, {recursive}). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok:true} or {ok:false, error} (NotFoundError, InvalidModificationError for a non-empty folder without recursive). Needs: a folder from showDirectoryPicker().
Use the public API:
const dir = await showDirectoryPicker();
await dir.removeEntry('old-drafts', { recursive: true });Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsWritableAbort bridge#
window.WebToApk.__fsWritableAbort(token: String)
abort(): the swap is dropped; the document is untouched.
Example
Internal transport behind FileSystemWritableFileStream.abort() - the swap copy is dropped and the file stays as it was. Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: nothing. Needs: an open writable stream.
Use the public API:
const w = await handle.createWritable();
try { await w.write(await buildExport()); await w.close(); }
catch (e) { await w.abort(); }Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsWritableClose bridge#
window.WebToApk.__fsWritableClose(token: String): String
close(): the swap replaces the document.
Example
Internal transport behind FileSystemWritableFileStream.close() - the swap copy replaces the file. Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok:true} or {ok:false, error} (NoModificationAllowedError, InvalidModificationError when the file's app cannot shorten it). Needs: an open writable stream.
Use the public API:
const w = await handle.createWritable();
await w.write(json);
await w.close(); // nothing changes on disk before thisNotes: Implemented in the app shell (FileSystemAccess.kt).
__fsWritableOpen bridge#
window.WebToApk.__fsWritableOpen(uri: String, keepExisting: Boolean): String
createWritable(): a swap file, copied from the file when [keepExisting].
Example
Internal transport behind FileSystemFileHandle.createWritable({keepExistingData}). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok, token} for a private swap copy, or {ok:false, error} (NoModificationAllowedError for a read-only file). Needs: a handle from one of the pickers.
Use the public API:
const handle = await showSaveFilePicker({ suggestedName: 'report.csv' });
const w = await handle.createWritable({ keepExistingData: false });
await w.write('a,b\n1,2\n');
await w.close();Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsWritableTruncate bridge#
window.WebToApk.__fsWritableTruncate(token: String, size: Double): String
truncate(size) on a writable stream.
Example
Internal transport behind FileSystemWritableFileStream.truncate(size). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok:true} or {ok:false, error}. Needs: an open writable stream.
Use the public API:
const w = await handle.createWritable({ keepExistingData: true });
await w.truncate(0);
await w.write('fresh start');
await w.close();Notes: Implemented in the app shell (FileSystemAccess.kt).
__fsWritableWrite bridge#
window.WebToApk.__fsWritableWrite(token: String, position: Double, base64: String): String
One chunk of a writable stream's write(), at [position].
Example
Internal transport behind FileSystemWritableFileStream.write() - one chunk at a position in the swap copy. Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {ok:true} or {ok:false, error}. Needs: an open writable stream.
Use the public API:
const w = await handle.createWritable({ keepExistingData: true });
await w.write({ type: 'write', position: 0, data: new Blob(['HEADER\n']) });
await w.close();Notes: Implemented in the app shell (FileSystemAccess.kt). Large data is sent in chunks.
getMaxIoChunkSize bridge#
window.WebToApk.getMaxIoChunkSize(): Long
The per-call byte cap for [readFileBase64] / [writeFileBase64], for chunk maths.
Example
The per-call byte limit of readFileBase64 / writeFileBase64 (4 MB). Pages normally use WebToApkFS.maxChunkSize().
Returns: (sync) a number (4194304). Needs: nothing to call it; the read/write methods it describes need Native Folder Access (SAF) ticked in Step 4 (Access).
The public way:
const step = window.WebToApkFS ? WebToApkFS.maxChunkSize() : 0;The raw call:
let step = 1048576;
if (window.WebToApk && typeof WebToApk.getMaxIoChunkSize === 'function') {
step = Number(WebToApk.getMaxIoChunkSize()) || step;
}
console.log('read/write up to', step, 'bytes per call');Notes: A write larger than this is refused with chunk too large (max N bytes); a larger read is cut to this size.
pickFile bridge#
window.WebToApk.pickFile(requestId: String, mimeFilter: String)
Opens the system single-file picker (ACTION_OPEN_DOCUMENT). Unlike [requestFolderAccess] this grants access to exactly one file, which is what an editor or a shredder should ask for; the read/write grant is persisted so the URI keeps working after the process is killed. The answer arrives on `WebToApkOnFilePickResult(success, fileJson, requestId)`; a failure's `error` is "cancelled", "disabled", "no-picker", "foreign_origin", or "busy" (a picker is already open for an earlier request, which keeps its own answer). @param mimeFilter e.g. "image/\*", "application/pdf", or "" for any file. (The star is backslash-escaped only because Kotlin block comments nest.)
Example
Opens the system picker for ONE file. Pages normally use the Promise helper WebToApkFS.pickFile(mimeFilter); this raw method answers through a callback.
Returns: nothing now. Later the shell calls window.WebToApkOnFilePickResult(success, fileJson, requestId), where fileJson is a JSON string {ok, uri, name, size, mime} on success or {ok:false, uri:'', error} with error = 'cancelled', 'disabled', 'no-picker', 'busy' or 'foreign_origin'. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
The public way (recommended):
const f = await WebToApkFS.pickFile('image/*');
if (f.ok) console.log('picked', f.name, f.uri);The raw call, matching on your own requestId:
function pickRaw(mime) {
return new Promise(function (resolve) {
if (!window.WebToApk || typeof WebToApk.pickFile !== 'function') { resolve(null); return; }
const myId = 'pick_' + Date.now();
window.WebToApkOnFilePickResult = function (success, fileJson, requestId) {
if (requestId !== myId) return;
const r = JSON.parse(fileJson); // fileJson is a string
resolve(success && r.ok ? r : null); // null = cancelled or not allowed
};
WebToApk.pickFile(myId, mime || ''); // '' = any file
});
}Notes: Only one pick can be open at a time: a call made while one is open is answered at once with error:'busy', and the open one keeps its own answer. The grant is kept (read and write) until you call releaseFileAccess(uri). The file is not read here - use readFileBase64 / WebToApkFS.readBytes with the uri.
readFileBase64 bridge#
window.WebToApk.readFileBase64(pathOrUri: String, offset: Long, length: Long): String
Reads up to [length] raw bytes starting at [offset] and returns them base64. @param pathOrUri a `content://` URI from [pickFile], or a path relative to the folder granted by [requestFolderAccess]. @param length bytes to read; clamped to 4 MB per call. 0 or negative means "as much as one call allows". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` tells a chunking loop when to stop without having to compare against `size` itself.
Example
Reads raw bytes from a file at a byte offset and returns them as base64. Pages normally use WebToApkFS.readBytes(pathOrUri, offset, length) (same result, already parsed) or WebToApkFS.readAll(pathOrUri).
Returns: (sync) a JSON string {ok:true, base64, bytesRead, offset, size, eof} or {ok:false, error}. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
The public way:
const r = WebToApkFS.readBytes(fileUri, 0, 65536);
if (r.ok) console.log(r.bytesRead, 'bytes read, end of file:', r.eof);The raw call - note the result is a string you must JSON.parse, not bare base64:
if (window.WebToApk && typeof WebToApk.readFileBase64 === 'function') {
const r = JSON.parse(WebToApk.readFileBase64(fileUri, 0, 0)); // length 0 = up to 4 MB
if (r.ok) {
const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
console.log('first byte', bytes[0], 'of', r.size);
} else {
console.log('read failed:', r.error);
}
}Notes: pathOrUri is a content:// URI from pickFile or a path relative to the folder from requestFolderAccess. At most 4 MB per call (getMaxIoChunkSize()); loop on bytesRead until eof. Errors: not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (a page from a site other than the app's own).
releaseFileAccess bridge#
window.WebToApk.releaseFileAccess(uriString: String): String
Releases a persisted single-file grant taken by [pickFile]. Android caps how many URI grants an app may persist, so an app that picks many files should release the ones it is done with.
Example
Releases the kept read/write grant for a file chosen with pickFile. Pages normally use WebToApkFS.releaseFile(uri), which returns a real boolean.
Returns: (sync) the string "true" or "false". Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build (always "false" otherwise).
The public way:
if (window.WebToApkFS) WebToApkFS.releaseFile(fileUri);The raw call:
if (window.WebToApk && typeof WebToApk.releaseFileAccess === 'function') {
const released = WebToApk.releaseFileAccess(fileUri) === 'true'; // a string, not a boolean
console.log(released ? 'access released' : 'nothing to release');
}Notes: Android limits how many file grants an app may keep, so release files you no longer need. The file itself is not touched.
statFile bridge#
window.WebToApk.statFile(pathOrUri: String): String
File or folder metadata without reading any of it. A relative path names a file or a sub-folder of the granted folder; a folder answers `isDirectory: true` with `size` 0 and `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, or `{ok:false, error}` with `not-found`, `disabled` or `foreign_origin`.
Example
Reads a file's metadata (name, size, type) without reading its bytes. Pages normally use WebToApkFS.stat(pathOrUri), which parses the JSON for you.
Returns: (sync) a JSON string {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, or {ok:false, error} ('not-found', 'disabled', 'foreign_origin'). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
The public way:
const info = WebToApkFS.stat(fileUri);
if (info.ok) console.log(info.name, info.size);The raw call:
if (window.WebToApk && typeof WebToApk.statFile === 'function') {
const info = JSON.parse(WebToApk.statFile(fileUri)); // or 'notes/today.txt' in the granted folder
if (info.ok) console.log(info.name, info.size + ' bytes', info.mime, info.canWrite);
else console.log('stat failed:', info.error);
}Notes: Accepts a content:// URI from pickFile or a path relative to the folder granted with requestFolderAccess. It only answers the app's own pages or its configured website; a page from any other site gets foreign_origin.
writeFileBase64 bridge#
window.WebToApk.writeFileBase64(pathOrUri: String, base64: String, offset: Long, mode: String): String
Writes raw bytes and confirms how many landed. @param mode `"patch"` writes at [offset] leaving the rest of the file intact (what a multi-pass overwrite needs), `"truncate"` replaces the file, `"append"` ignores [offset] and writes at the end. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` is re-read from the file after an fsync, so a caller can verify the write for real rather than trust a boolean.
Example
Writes base64 bytes into a file at an offset (patch), over the whole file (truncate), or at the end (append). Pages normally use WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) or WebToApkFS.writeAll(pathOrUri, base64).
Returns: (sync) a JSON string {ok, bytesWritten, offset, size} (size re-read after an fsync) or {ok:false, error}. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.
The public way:
const r = WebToApkFS.writeBytes(fileUri, btoa('hello'), 0, 'truncate');
if (r.ok) console.log('saved', r.bytesWritten, 'bytes');The raw call:
if (window.WebToApk && typeof WebToApk.writeFileBase64 === 'function') {
const r = JSON.parse(WebToApk.writeFileBase64('notes/today.txt', btoa('Buy milk\n'), 0, 'append'));
if (!r.ok) console.log('write failed:', r.error);
else console.log('file is now', r.size, 'bytes');
}Notes: Mode is 'patch' (default when empty), 'truncate' or 'append'. One call takes at most 4 MB of decoded bytes (getMaxIoChunkSize()). A relative path is created inside the granted folder when missing. Errors: disabled, foreign_origin (the page on screen is not the app's own), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.