Create Free APK

JavaScript bridge API

Files: a folder the user granted

A directory chosen once, readable and writable afterwards, with permission that survives reboots.

WebToApkFS.clearAccess helper#

WebToApkFS.clearAccess()

Example

Forgets the granted folder and gives the permission back to Android.

Returns: nothing (synchronous). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.

Always offer a way to disconnect next to the folder's name:

document.getElementById('disconnect').addEventListener('click', function () {
  if (!window.WebToApkFS) return;
  window.WebToApkFS.clearAccess();
  console.log('still connected?', window.WebToApkFS.hasAccess());   // false
  document.getElementById('folder-name').textContent = 'No folder chosen';
  document.getElementById('choose-folder').hidden = false;
});

Notes: The files stay where they are; the app simply cannot reach them any more. To use a folder again, call WebToApkFS.requestAccess() from a tap.

WebToApkFS.delete helper#

WebToApkFS.delete(relativePath, options)

Example

Deletes a file or folder inside the granted folder.

Returns: a boolean, synchronously: true when it was deleted. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

Delete one file, after asking:

if (window.WebToApkFS && confirm('Delete notes/old.txt?')) {
  var ok = window.WebToApkFS.delete('notes/old.txt');
  if (!ok) alert('Could not delete the file.');
}

Delete a folder and everything inside it - pass { recursive: true }:

if (window.WebToApkFS && confirm('Delete the whole 2025 folder?')) {
  var gone = window.WebToApkFS.delete('notes/2025', { recursive: true });
  console.log(gone ? 'deleted' : 'not deleted');
}

Notes: Without { recursive: true } a folder is deleted only when it is empty. The granted folder itself ('') can never be deleted. Deletion is immediate and there is no trash, so always confirm with the user first.

WebToApkFS.getAccessUri helper#

WebToApkFS.getAccessUri()

Example

Gives the address of the granted folder, so you can show the user where files go.

Returns: a string, synchronously: a tree URI such as content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, or '' when no folder is granted. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.

Show a readable folder name next to a Disconnect button:

function connectedFolderName() {
  if (!window.WebToApkFS) return '';
  var uri = window.WebToApkFS.getAccessUri();
  if (!uri) return '';
  var tail = uri.split('/tree/')[1] || '';
  try { tail = decodeURIComponent(tail); } catch (e) {}
  return tail.split(':').pop() || tail;        // "Documents/Notes"
}

var name = connectedFolderName();
document.getElementById('folder-name').textContent = name ? 'Folder: ' + name : 'No folder chosen';

Notes: The file methods do not take this URI - they take paths relative to the granted folder ('notes/today.txt'). The URI format belongs to the Files provider the user picked from, so only use it for display.

WebToApkFS.hasAccess helper#

WebToApkFS.hasAccess()

Example

Tells you whether the user has already given the app a folder.

Returns: a boolean, synchronously. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.

Show "Choose folder" or the file list, depending on the answer:

function renderFolderScreen() {
  if (!window.WebToApkFS) {
    document.getElementById('folder-panel').hidden = true;   // not in the app, or not enabled
    return;
  }
  var connected = window.WebToApkFS.hasAccess();
  document.getElementById('choose-folder').hidden = connected;
  document.getElementById('file-list').hidden = !connected;
  if (connected) {
    var entries = window.WebToApkFS.list('');
    document.getElementById('file-list').textContent =
      entries.map(function (e) { return e.name; }).join('\n');
  }
}

Notes: It only says the app remembers a folder. If the user deleted or moved that folder, list('') returns [] - offer "Choose folder" again. window.WebToApkFS appears when the page finishes loading; during load use the raw window.WebToApk.hasFolderAccess() === 'true'.

WebToApkFS.isAvailable helper#

WebToApkFS.isAvailable()

Example

Tells you whether this app can use a user-granted folder at all.

Returns: a boolean, synchronously: true when the build has folder access switched on. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build - without it (and without Secure Keys) window.WebToApkFS is not added to the page; with only Secure Keys it exists and answers false.

window.WebToApkFS is added before your page's first script runs; on WebViews older than about 2021 it arrives only when the page finishes loading. To cover those too, wait for it briefly before you decide:

function whenFolderHelperReady(done) {
  var tries = 0;
  (function check() {
    if (window.WebToApkFS) return done(window.WebToApkFS);
    if (++tries > 30) return done(null);       // in a browser, or the build did not enable it
    setTimeout(check, 100);
  })();
}

whenFolderHelperReady(function (fs) {
  var usable = !!(fs && fs.isAvailable());
  document.getElementById('folder-panel').hidden = !usable;
});

Inside a button tap the page has long finished loading, so a direct check is enough:

if (window.WebToApkFS && window.WebToApkFS.isAvailable()) {
  // show the folder features
}

Notes: This says nothing about whether a folder is chosen yet - use WebToApkFS.hasAccess() for that. Before the page has loaded, the raw window.WebToApk.isFolderAccessEnabled() (string "true"/"false") answers the same question.

WebToApkFS.list helper#

WebToApkFS.list(relativePath)

Example

Lists the files and sub-folders inside a folder of the granted folder.

Returns: an array, synchronously: [{ name, uri, isDirectory, length, lastModified }] (length in bytes, lastModified in ms since 1970). Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

A simple file browser ('' is the granted folder itself):

function renderFolder(path) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) return;
  var ul = document.getElementById('files');
  ul.innerHTML = '';
  window.WebToApkFS.list(path)
    .sort(function (a, b) { return (b.isDirectory - a.isDirectory) || a.name.localeCompare(b.name); })
    .forEach(function (e) {
      var li = document.createElement('li');
      li.textContent = (e.isDirectory ? '[folder] ' : '') + e.name +
        (e.isDirectory ? '' : ' (' + e.length + ' bytes, ' + new Date(e.lastModified).toLocaleDateString() + ')');
      if (e.isDirectory) li.onclick = function () { renderFolder(path ? path + '/' + e.name : e.name); };
      ul.appendChild(li);
    });
}

renderFolder('');

Notes: Empty folder, missing folder, a path that is a file, a path with .., and no granted folder all return []. The list is not sorted. Each uri also works with WebToApkFS.stat() and WebToApkFS.readBytes().

WebToApkFS.mkdir helper#

WebToApkFS.mkdir(relativePath)

Example

Creates a folder inside the granted folder, including any missing parent folders.

Returns: a boolean, synchronously: true when the folder exists afterwards. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  var month = new Date().toISOString().slice(0, 7);          // "2026-06"
  if (window.WebToApkFS.mkdir('journal/' + month)) {
    window.WebToApkFS.writeText('journal/' + month + '/readme.txt', 'One file per day.');
  } else {
    alert('Could not create the folder.');
  }
}

Notes: It returns true if the folder already exists, so calling it before each save is fine. It returns false when a FILE with that name is in the way, the path contains .., or no folder is granted. WebToApkFS.writeText() already creates missing parents, so you only need mkdir for empty folders.

WebToApkFS.readText helper#

WebToApkFS.readText(relativePath)

Example

Reads a text file from the granted folder as UTF-8.

Returns: a string, synchronously: the file's text, or ''. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

'' means "empty file" OR "no such file". Use WebToApkFS.stat() when you must know which:

function loadSettings() {
  if (!window.WebToApkFS) return null;
  var info = window.WebToApkFS.stat('data/settings.json');
  if (!info.ok) return null;                                // not saved yet
  try {
    return JSON.parse(window.WebToApkFS.readText('data/settings.json'));
  } catch (e) {
    alert('settings.json is damaged: ' + e.message);
    return null;
  }
}

Open a note into a text box:

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  document.getElementById('editor').value = window.WebToApkFS.readText('notes/today.txt');
}

Notes: The whole file is loaded at once - for big or binary files use WebToApkFS.readBytes(). Bytes that are not valid UTF-8 come back as the replacement character. A folder, a path with .., or no granted folder also returns ''.

WebToApkFS.rename helper#

WebToApkFS.rename(relativePath, newName)

Example

Renames a file or folder inside the granted folder.

Returns: a boolean, synchronously. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

newName is only the new name, not a path. The entry stays in the same folder:

function renameNote(path, newName) {
  if (!window.WebToApkFS) return false;
  var ok = window.WebToApkFS.rename(path, newName);     // 'notes/a.txt', 'b.txt' -> notes/b.txt
  if (!ok) alert('Could not rename. A file called "' + newName + '" may already exist.');
  return ok;
}

renameNote('notes/today.txt', 'journal.txt');
renameNote('notes/2025', 'archive-2025');               // folders work too

Notes: It returns false when another entry already has newName, when newName is empty, ./.., or contains / or \, when the path does not exist, or when no folder is granted. To move a file between folders, read it, write it in the new place, then delete the old one.

WebToApkFS.requestAccess helper#

WebToApkFS.requestAccess()

Example

Opens the system folder picker so the user can give the app one folder, kept across restarts.

Returns: a Promise that always resolves (never rejects) with { success, folderUri, requestId }, plus error when success is false. success is a boolean; folderUri is the granted content://…/tree/… address, or ''. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.

Ask from a button the user taps, and show which folder is connected:

document.getElementById('choose-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) {
    alert('Folder access is not available in this app.');
    return;
  }
  var r = await window.WebToApkFS.requestAccess();
  if (!r.success) {
    document.getElementById('status').textContent = 'No folder chosen.';
    return;
  }
  var name = decodeURIComponent(r.folderUri.split('/tree/')[1] || '').split(':').pop();
  document.getElementById('status').textContent = 'Saving to: ' + name;
  window.WebToApkFS.writeText('hello.txt', 'Connected at ' + new Date().toISOString());
});

Only ask when no folder is granted yet:

async function ensureFolder() {
  if (!window.WebToApkFS) return false;
  if (window.WebToApkFS.hasAccess()) return true;
  var r = await window.WebToApkFS.requestAccess();
  return r.success;
}

Notes: Call it from a tap, never on page load - the picker covers the app. success: false comes with error: 'cancelled' (the user backed out), 'not_persisted' (Android would not keep the grant), 'disabled', 'busy' (a picker is already open for an earlier request - that one still gets its own answer), 'no_picker' or 'foreign_origin'. Choosing again replaces the earlier folder.

WebToApkFS.writeText helper#

WebToApkFS.writeText(relativePath, content)

Example

Writes a UTF-8 text file into the granted folder, creating it (and missing parent folders) or replacing it.

Returns: a boolean, synchronously: true when the file was written. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build, and a granted folder.

Save from a button:

document.getElementById('save').addEventListener('click', async function () {
  if (!window.WebToApkFS) { alert('Saving to a folder is not available here.'); return; }
  if (!window.WebToApkFS.hasAccess()) {
    var r = await window.WebToApkFS.requestAccess();
    if (!r.success) return;                              // user cancelled
  }
  var ok = window.WebToApkFS.writeText('notes/today.txt', document.getElementById('editor').value);
  document.getElementById('status').textContent = ok ? 'Saved' : 'Could not save';
});

Save an object as JSON:

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  window.WebToApkFS.writeText('data/settings.json', JSON.stringify({ theme: 'dark', size: 16 }, null, 2));
}

Notes: An existing file is replaced completely - there is no append. content is turned into a string (null becomes ''). It returns false when the path is an existing folder, contains .., or no folder is granted. The file type is taken from the extension, so the name on disk is exactly the name you gave.

clearFolderAccess bridge#

window.WebToApk.clearFolderAccess()

Releases the persisted folder grant. Refused (no-op) for a foreign page.

Example

Forgets the granted folder and gives the permission back to Android.

Returns: nothing (synchronous). WebToApkFS.clearAccess() does the same. Needs: Native Folder Access (SAF) ticked in Step 4 (Access); with the switch off it does nothing.

A "Disconnect" button:

document.getElementById('disconnect').addEventListener('click', function () {
  if (window.WebToApkFS) {
    window.WebToApkFS.clearAccess();
  } else if (window.WebToApk && typeof window.WebToApk.clearFolderAccess === 'function') {
    window.WebToApk.clearFolderAccess();
  }
  document.getElementById('folder-name').textContent = 'No folder chosen';
});

Notes: Files in the folder are NOT deleted; the app just cannot reach them any more. After this, hasFolderAccess() answers "false" and every file method fails until the user chooses a folder again.

deleteEntry bridge#

window.WebToApk.deleteEntry(relativePath: String, recursive: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Deletes a file or folder inside the granted folder.

Returns: the STRING "true" or "false", synchronously. WebToApkFS.delete(path, { recursive }) returns a real boolean. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

With the helper, after asking the user:

function removeEntry(path, isFolder) {
  if (!window.WebToApkFS) return false;
  if (!confirm('Delete ' + path + '? This cannot be undone.')) return false;
  return window.WebToApkFS.delete(path, { recursive: isFolder });
}

removeEntry('notes/old.txt', false);
removeEntry('notes/2025', true);      // the folder and everything in it

The raw call - the second argument is the STRING 'true' or 'false':

if (window.WebToApk && typeof window.WebToApk.deleteEntry === 'function') {
  var gone = window.WebToApk.deleteEntry('cache', 'true') === 'true';
}

Notes: A folder that is not empty is only deleted with recursive 'true'. The granted folder itself ('') can never be deleted. Deleting is real and immediate: there is no trash.

getFolderAccessUri bridge#

window.WebToApk.getFolderAccessUri(): String

The granted folder's `content://` tree URI, or "" (none, disabled, or a foreign page).

Example

Gives the address of the folder the user granted, so you can show which folder is connected.

Returns: a string, synchronously: a tree URI such as content://com.android.externalstorage.documents/tree/primary%3ADocuments%2FNotes, or "" when no folder is granted or the switch is off. WebToApkFS.getAccessUri() returns the same string. Needs: Native Folder Access (SAF) ticked in Step 4 (Access).

Turn the URI into a name the user recognises:

function folderLabel() {
  var uri = '';
  if (window.WebToApkFS) {
    uri = window.WebToApkFS.getAccessUri();
  } else if (window.WebToApk && typeof window.WebToApk.getFolderAccessUri === 'function') {
    uri = window.WebToApk.getFolderAccessUri();
  }
  if (!uri) return '';
  var tail = uri.split('/tree/')[1] || '';
  try { tail = decodeURIComponent(tail); } catch (e) {}
  return tail.split(':').pop() || tail;          // "primary:Documents/Notes" -> "Documents/Notes"
}

var label = folderLabel();
document.getElementById('folder-name').textContent = label ? 'Saving to: ' + label : 'No folder chosen';

Notes: You never pass this URI back to the file methods - they all take paths relative to this folder. Show the name next to a "Disconnect" button so the user knows where files go.

hasFolderAccess bridge#

window.WebToApk.hasFolderAccess(): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Tells you whether the user has already given the app a folder.

Returns: the STRING "true" or "false" (not a boolean), synchronously. WebToApkFS.hasAccess() gives a real boolean. Needs: Native Folder Access (SAF) ticked in Step 4 (Access); without it the answer is always "false".

The raw method exists from the very first line of the page, so it is the right check while the page is still loading (WebToApkFS is added only when loading finishes):

function folderConnected() {
  if (window.WebToApkFS) return window.WebToApkFS.hasAccess();
  return !!(window.WebToApk && typeof window.WebToApk.hasFolderAccess === 'function' &&
    window.WebToApk.hasFolderAccess() === 'true');
}

document.getElementById('choose-folder').hidden = folderConnected();

Notes: Compare with === 'true'; the string "false" is truthy. It only says the app remembers a folder. If that folder was deleted or moved in the Files app, listFolderEntries('') answers [] - ask the user to choose it again.

isFolderAccessEnabled bridge#

window.WebToApk.isFolderAccessEnabled(): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Tells you whether this app was built with folder access switched on at all.

Returns: the STRING "true" or "false", synchronously. Needs: nothing to call it; it answers "true" only when Native Folder Access (SAF) was ticked in Step 4 (Access).

Use it to decide whether to show the folder feature. WebToApkFS.isAvailable() answers the same question as a boolean once the page has loaded:

function folderFeatureShipped() {
  if (window.WebToApkFS) return window.WebToApkFS.isAvailable();
  return !!(window.WebToApk && typeof window.WebToApk.isFolderAccessEnabled === 'function' &&
    window.WebToApk.isFolderAccessEnabled() === 'true');
}

if (!folderFeatureShipped()) {
  document.getElementById('folder-panel').hidden = true;   // in a browser, or the switch was off
}

Notes: This says nothing about whether a folder is chosen yet - use hasFolderAccess() for that. In a browser window.WebToApk does not exist, so the check above answers false without throwing.

listFolderEntries bridge#

window.WebToApk.listFolderEntries(relativePath: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Lists the files and sub-folders inside a folder of the granted folder.

Returns: a JSON STRING, synchronously: an array of { name, uri, isDirectory, length, lastModified } (length in bytes, lastModified in ms since 1970). WebToApkFS.list(path) parses it for you. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

With the helper ('' is the granted folder itself):

function showFolder(path) {
  if (!window.WebToApkFS) return;
  var entries = window.WebToApkFS.list(path);          // already an array
  entries.forEach(function (e) {
    console.log(e.isDirectory ? '[dir] ' : '      ', e.name, e.length + ' bytes');
  });
}
showFolder('');          // the root
showFolder('notes');     // a sub-folder

The raw call, which returns text you must parse:

if (window.WebToApk && typeof window.WebToApk.listFolderEntries === 'function') {
  var entries = JSON.parse(window.WebToApk.listFolderEntries('notes'));
  console.log(entries.length + ' entries');
}

Notes: An empty folder, a missing folder, a path that is a file, a path with .., no granted folder, and a page from a foreign website all answer [] - call hasFolderAccess() first if you need to tell "no folder" apart. Paths are relative, use /, and never go above the granted folder.

mkdir bridge#

window.WebToApk.mkdir(relativePath: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Creates a folder inside the granted folder, including any missing parent folders.

Returns: the STRING "true" or "false", synchronously. WebToApkFS.mkdir(path) returns a real boolean. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

With the helper:

if (window.WebToApkFS && window.WebToApkFS.hasAccess()) {
  var ok = window.WebToApkFS.mkdir('photos/2026/june');   // creates photos, 2026 and june
  if (!ok) console.log('Could not create the folder');
}

The raw call:

if (window.WebToApk && typeof window.WebToApk.mkdir === 'function') {
  var made = window.WebToApk.mkdir('backups') === 'true';
}

Notes: It answers "true" when the folder already exists, so it is safe to call before every save. It answers "false" when a FILE with that name is in the way, the path contains .., or no folder is granted.

readTextFile bridge#

window.WebToApk.readTextFile(relativePath: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Reads a text file from the granted folder as UTF-8.

Returns: a string, synchronously: the file's text, or "". WebToApkFS.readText(path) returns the same. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

"" means "empty file" OR "no such file", so check with WebToApkFS.stat() when the difference matters:

function loadNote(path) {
  if (!window.WebToApkFS) return null;
  var info = window.WebToApkFS.stat(path);    // { ok: true, name, size, ... } or { ok: false, error: 'not-found' }
  if (!info.ok) return null;                  // the file does not exist
  return window.WebToApkFS.readText(path);    // '' here really is an empty file
}

var text = loadNote('notes/today.txt');
document.getElementById('editor').value = text === null ? '' : text;

The raw call:

if (window.WebToApk && typeof window.WebToApk.readTextFile === 'function') {
  var raw = window.WebToApk.readTextFile('notes/today.txt');
  console.log(raw.length + ' characters');
}

Notes: The whole file is read into memory - fine for notes and JSON, not for large files; use WebToApkFS.readBytes for those. Bytes that are not valid UTF-8 come back as the replacement character. A folder path, a path with .., no granted folder, or a page from a foreign website all answer "".

renameEntry bridge#

window.WebToApk.renameEntry(relativePath: String, newName: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Renames a file or folder inside the granted folder.

Returns: the STRING "true" or "false", synchronously. WebToApkFS.rename(path, newName) returns a real boolean. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

newName is a plain name, not a path - the entry stays in the same folder:

if (window.WebToApkFS) {
  var ok = window.WebToApkFS.rename('notes/today.txt', 'journal.txt');   // -> notes/journal.txt
  if (!ok) alert('Rename failed. Is there already a file called journal.txt?');
}

The raw call:

if (window.WebToApk && typeof window.WebToApk.renameEntry === 'function') {
  var renamed = window.WebToApk.renameEntry('old-folder', 'new-folder') === 'true';
}

Notes: It fails when another entry already has newName, when newName is empty or contains / or \, when the path does not exist, or when no folder is granted. To move a file to another folder, write it there and delete the old one.

requestFolderAccess bridge#

window.WebToApk.requestFolderAccess(requestId: String)

Opens the system folder picker and persists read/write access to the chosen folder. The answer arrives on `WebToApkOnFolderAccessResult`; only one picker can be open at a time, so a request made while one is open is answered at once with `error:"busy"` and the open one keeps its own answer.

Example

Opens the system folder picker so the user can give the app one folder. Pages normally use WebToApkFS.requestAccess(), which wraps this call in a Promise.

Returns: nothing. The answer comes back later through an internal callback that WebToApkFS owns; WebToApkFS.requestAccess() resolves with { success, folderUri, requestId }, plus error when success is false. Needs: tick Native Folder Access (SAF) in Step 4 (Access) when you build.

Ask from a button tap, with the helper:

document.getElementById('choose-folder').addEventListener('click', async function () {
  if (!window.WebToApkFS) {
    alert('Folder access is not available in this app.');
    return;
  }
  var r = await window.WebToApkFS.requestAccess();
  if (r.success) {
    console.log('Folder granted:', r.folderUri);   // content://.../tree/...
  } else {
    console.log('No folder chosen');                 // user cancelled, or Android refused the grant
  }
});

Notes: Do not call the raw method yourself: its only answer is the callback the helper listens to. success is false when the user cancels (error:'cancelled'), when Android will not keep the grant ('not_persisted'), and at once when the build did not enable the switch ('disabled') or the page on screen is not the app's own ('foreign_origin'). The grant is kept across app restarts; choosing again replaces the folder. Only one picker can be open at a time - a request made while one is open is answered at once with error:'busy', and the open one keeps its own answer.

writeTextFile bridge#

window.WebToApk.writeTextFile(relativePath: String, content: String): String

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions. JavaScript usage: window.WebToApk.requestFolderAccess('myRequestId') window.WebToApk.hasFolderAccess() // "true" / "false" window.WebToApk.getFolderAccessUri() // "content://..." or "" window.WebToApk.clearFolderAccess() // releases persisted access Callback from Android → JS: window.WebToApkOnFolderAccessResult(success, folderUri, requestId, error) `error` (4th argument, "" on success) says why a request failed: "disabled", "foreign_origin", "busy" (a picker is already open for an earlier request - that one still gets its own answer), "cancelled", "not_persisted", "no_picker".

Described by its group, Native Folder Access (SAF) Bridge, rather than on its own.

Example

Writes a text file (UTF-8) into the granted folder, creating it or replacing its content.

Returns: the STRING "true" or "false", synchronously. WebToApkFS.writeText(path, text) returns a real boolean. Needs: Native Folder Access (SAF) ticked in Step 4 (Access) and a granted folder.

With the helper:

function saveNote(path, text) {
  if (!window.WebToApkFS || !window.WebToApkFS.hasAccess()) {
    alert('Choose a folder first.');
    return false;
  }
  var ok = window.WebToApkFS.writeText(path, text);   // missing parent folders are created
  if (!ok) alert('Could not save ' + path);
  return ok;
}

saveNote('notes/2026/today.txt', document.getElementById('editor').value);

The raw call, whose answer is text:

if (window.WebToApk && typeof window.WebToApk.writeTextFile === 'function') {
  var saved = window.WebToApk.writeTextFile('data/settings.json', JSON.stringify({ theme: 'dark' })) === 'true';
}

Notes: An existing file is replaced completely. It fails when the path is an existing folder, contains .., or no folder is granted. The file type comes from the extension (.json, .txt, .md…), so the name you give is the name on disk.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-09-23.