# AppMint hybrid HTML API

Every native method a generated app exposes to your page, extracted from the template runtime
itself (`WebViewActivity.kt`), so this list is always exactly what ships.

**Read the "Writing HTML for your app" guide first** — it covers feature detection, the async
callback convention, and the rules that a method list cannot express.

Total: **138 methods** across 2 namespace(s).

## `window.AndroidNFC`

### `startScan`

```js
window.AndroidNFC.startScan(): Boolean
```

*(Web NFC)* Legacy availability probe, present only when the creator enabled NFC. Real NFC work goes through the standard NDEFReader polyfill (window.WebToApk.__nfc*), which is what a page should use; this only reports whether the adapter is on.

## `window.WebToApk`

### `__nfcCancelWrite`

```js
window.WebToApk.__nfcCancelWrite()
```

Cancel any pending NFC write/lock operation.

### `__nfcMakeReadOnly`

```js
window.WebToApk.__nfcMakeReadOnly()
```

Called from the JS NDEFReader.makeReadOnly() polyfill.

### `__nfcWrite`

```js
window.WebToApk.__nfcWrite(messageJson: String)
```

*(NFC Write Bridge)* Called from the JS NDEFReader.write() polyfill. Stores the message JSON and waits for the next NFC tag tap.

### `__setBackInterest`

```js
window.WebToApk.__setBackInterest(interested: Boolean)
```

*(Back button)* Internal for AppMint.setBackHandler — tells the shell whether the page registered a back handler. When true, a hardware/gesture back is first offered to the page (cancelable `appmint:back` event + the registered handler); only an unconsumed press falls through to history-back / exit.

### `addContact`

```js
window.WebToApk.addContact(requestId: String, contactJson: String)
```

*(Contacts: pick and add)* The system picker (no permission needed) and writing a new contact (WRITE_CONTACTS). The read side — listing and searching — is under the Contacts group above.

### `authenticateBiometric`

```js
window.WebToApk.authenticateBiometric(callbackId: String, title: String, subtitle: String)
```

*(Fingerprint / biometric bridge)* WebToApk.isBiometricAvailable()            -> "available" | "no_hardware"                                                | "not_enrolled" | "unavailable"   WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses:   window.__webToApkAuth = window.__webToApkAuth || {};   window.__webToApkAuth['unlock'] = function (json) {       var r = JSON.parse(json);      // {ok:true} | {ok:false,error:"..."}       if (r.ok) showApp();   };   WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');

### `authenticateBiometricEx`

```js
window.WebToApk.authenticateBiometricEx(callbackId: String, optionsJson: String)
```

The full form. [optionsJson] accepts: title, subtitle, description, negativeButtonText, allowDeviceCredential (PIN/pattern/password fallback), strong (require BIOMETRIC_STRONG — use for anything payment-shaped). Errors are distinct codes, not one boolean: no_hardware | none_enrolled | hw_unavailable | lockout | lockout_permanent | user_cancel | permission_missing.

### `bleConnect`

```js
window.WebToApk.bleConnect(address: String)
```

*(BLE fitness sensors (heart rate / speed / cadence / power / battery))* Runtime for the existing Bluetooth permission toggle (permBluetooth) — the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

### `bleDisconnect`

```js
window.WebToApk.bleDisconnect()
```

*(BLE fitness sensors (heart rate / speed / cadence / power / battery))* Runtime for the existing Bluetooth permission toggle (permBluetooth) — the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

### `bleSetWheelCircumference`

```js
window.WebToApk.bleSetWheelCircumference(mm: Int)
```

*(BLE fitness sensors (heart rate / speed / cadence / power / battery))* Runtime for the existing Bluetooth permission toggle (permBluetooth) — the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

### `bleStartScan`

```js
window.WebToApk.bleStartScan()
```

*(BLE fitness sensors (heart rate / speed / cadence / power / battery))* Runtime for the existing Bluetooth permission toggle (permBluetooth) — the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

### `bleStopScan`

```js
window.WebToApk.bleStopScan()
```

*(BLE fitness sensors (heart rate / speed / cadence / power / battery))* Runtime for the existing Bluetooth permission toggle (permBluetooth) — the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

### `cancelAllNotifications`

```js
window.WebToApk.cancelAllNotifications(): Boolean
```

*(Notification permission, cancelling and listing)* Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

### `cancelNotification`

```js
window.WebToApk.cancelNotification(id: String): Boolean
```

*(Notification permission, cancelling and listing)* Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

### `cancelVibrate`

```js
window.WebToApk.cancelVibrate()
```

*(Vibration)* Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

### `canOpenAppAtTime`

```js
window.WebToApk.canOpenAppAtTime(): String
```

Whether a scheduled notification may put the APP ITSELF on screen (`openApp`). "granted"  — a full-screen alert will show, over the lock screen. "denied"   — Android 14+ withheld it; call requestOpenAppPermission(). There is no third answer where the app silently launches itself in the background: Android has blocked background activity starts since Android 10, and a full-screen intent is the whole of what remains.

### `castMedia`

```js
window.WebToApk.castMedia(mediaUrl: String)
```

Offers [mediaUrl] to whatever on this phone can play it on another screen — Chromecast, a Smart TV app, a local player — through the system chooser. This is a hand-off, not a cast session: the app does not stay in control of playback, and there is no callback. A page that needs a real session should use the Cast SDK in JavaScript instead.

### `clearFolderAccess`

```js
window.WebToApk.clearFolderAccess()
```

*(Native Folder Access (SAF) Bridge)* 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)

### `clearMediaNotification`

```js
window.WebToApk.clearMediaNotification()
```

*(Media Notification Bridge)* Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

### `clearNotificationSounds`

```js
window.WebToApk.clearNotificationSounds(): Int
```

Drops the copied sound files. They are re-created on next use.

### `clearPushInbox`

```js
window.WebToApk.clearPushInbox(): Boolean
```

Empties the push inbox (the tray and server history are untouched).

### `clearSharedText`

```js
window.WebToApk.clearSharedText()
```

Clears the kept share once the page has consumed it.

### `closeWindow`

```js
window.WebToApk.closeWindow()
```

*(Share, clipboard and window)* The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

### `composeSms`

```js
window.WebToApk.composeSms(phoneNumber: String, message: String): Boolean
```

Opens the user's SMS app with the message prefilled. Needs no permission and cannot be rejected by Play — the right default for anything non-automated.

### `copyToClipboard`

```js
window.WebToApk.copyToClipboard(text: String): Boolean
```

*(Share, clipboard and window)* The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

### `decryptWithKey`

```js
window.WebToApk.decryptWithKey(alias: String, base64Payload: String): String
```

Reverses [encryptWithKey]. Fails with `error:"no-such-key"` once the key has been deleted — the payload is then unrecoverable by design. @return JSON `{ok, base64, error}`.

### `deleteEntry`

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

*(Native Folder Access (SAF) Bridge)* 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)

### `deleteKey`

```js
window.WebToApk.deleteKey(alias: String): String
```

Destroys [alias] permanently. Everything encrypted with it becomes undecryptable — including by this app, and by anyone holding the device.

### `dismissPushMessage`

```js
window.WebToApk.dismissPushMessage(id: String): Boolean
```

Dismisses ONE message from the inbox — the per-row swipe-away an inbox screen needs (clearPushInbox is all-or-nothing). REVERSIBLE by design: the entry is hidden, not destroyed, so the UI can offer "Undo" for a few seconds after the swipe. Local only: the tray notification and the creator's history are untouched.

### `encryptWithKey`

```js
window.WebToApk.encryptWithKey(alias: String, base64Plaintext: String): String
```

Encrypts base64 [base64Plaintext] with [alias]. Each call generates its own IV, so a large file is encrypted chunk by chunk — chunks are independent and may be decrypted in any order. @return JSON `{ok, base64, error}` where `base64` is `IV || ciphertext || tag`.

### `enterPip`

```js
window.WebToApk.enterPip(): Boolean
```

Shrinks the whole app into a floating picture-in-picture window right now — a video or a call the user wants to keep watching while they leave. Android 8+ and only when the build enabled PiP (the manifest flag it needs); returns whether the window opened. Leaving the app with PiP enabled does this by itself; this is the explicit "minimise" button.

### `generateKey`

```js
window.WebToApk.generateKey(alias: String, optionsJson: String): String
```

Creates (or replaces) an AES-256-GCM key called [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` binds the key to the device lock; `strongBox` fails outright on devices without a secure element rather than handing back a weaker key than was asked for. @return JSON `{ok, alias, hardwareBacked, error}`.

### `getAlarmPrecision`

```js
window.WebToApk.getAlarmPrecision(): String
```

"exact" — reminders fire on the minute. "inexact" — the user (or the OEM) turned exact alarms off for this app, so Doze may batch them by minutes. Ask [requestExactAlarms] to fix it. A page that must be on time should check this instead of assuming.

### `getAppName`

```js
window.WebToApk.getAppName(): String
```

The app name the creator chose in the wizard — what the launcher shows.

### `getAppVersion`

```js
window.WebToApk.getAppVersion(): String
```

The versionName from the installed package, e.g. "1.4.2" — the string a creator sets when building, not the versionCode. Falls back to "1.0" if the package cannot be read, so a page can print it without a null check.

### `getContact`

```js
window.WebToApk.getContact(requestId: String, contactId: String)
```

*(Contacts)* All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread — an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works — the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

### `getDeviceInfo`

```js
window.WebToApk.getDeviceInfo(): String
```

*(Device / app information)* WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

### `getFolderAccessUri`

```js
window.WebToApk.getFolderAccessUri(): String
```

*(Native Folder Access (SAF) Bridge)* 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)

### `getInstallId`

```js
window.WebToApk.getInstallId(): String
```

*(Device / app information)* WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

### `getLaunchUrl`

```js
window.WebToApk.getLaunchUrl(): String
```

The deep link that launched or resumed this app, or "" if it was opened normally. Read it whenever your app mounts and route on it yourself: const u = window.WebToApk?.getLaunchUrl?.(); if (u) router.navigate(new URL(u).pathname); The value is KEPT, not consumed, so a late-mounting app still sees it; a later link arriving while the app runs replaces it and also fires 'appmint:deep-link'.

### `getMaxIoChunkSize`

```js
window.WebToApk.getMaxIoChunkSize(): Long
```

The per-call byte cap for [readFileBase64] / [writeFileBase64], for chunk maths.

### `getNotificationPermission`

```js
window.WebToApk.getNotificationPermission(): String
```

The CURRENT notification permission, read live from the OS. The polyfill used to hardcode `Notification.permission = 'default'` on every page load, so the overwhelmingly common page pattern if (Notification.permission === 'granted') new Notification(...) never fired — not even for a user who had already granted it, and not on Android 12 and below where notifications need no runtime grant at all.

### `getOwnedProducts`

```js
window.WebToApk.getOwnedProducts(): String
```

JSON array of every owned product ID — for gating whole screens at startup.

### `getPhoneIdentity`

```js
window.WebToApk.getPhoneIdentity(): String
```

*(Device / app information)* WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

### `getProducts`

```js
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
```

Asks Play for live details of the given product IDs (JSON array of strings). Result arrives as 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. IDs Play doesn't know are absent from the result — the page hides those items.

### `getPushInbox`

```js
window.WebToApk.getPushInbox(): String
```

Appwright Push inbox (creator-sent messages, newest first, max 50): JSON array of {id, title, body, imageUrl, link, receivedAt, read}. Always the full current truth — an app that mounts late loses nothing by having missed the live `appmint:push` events.

### `getScheduledNotifications`

```js
window.WebToApk.getScheduledNotifications(): String
```

*(Notification permission, cancelling and listing)* Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

### `getSharedText`

```js
window.WebToApk.getSharedText(): String
```

Text or a link another app shared INTO this one ("Share to <app>"), as JSON `{text, subject, url, receivedAt}` or "" when nothing was shared. Kept until read, so an app that mounts after the share still sees it; a share arriving while the app runs also fires `appmint:shared`. Requires the build's "Open with this app" option, which registers the share-sheet entry.

### `hasFolderAccess`

```js
window.WebToApk.hasFolderAccess(): String
```

*(Native Folder Access (SAF) Bridge)* 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)

### `hasKey`

```js
window.WebToApk.hasKey(alias: String): String
```

True when a key called [alias] exists in this app's keystore.

### `isBiometricAvailable`

```js
window.WebToApk.isBiometricAvailable(): String
```

*(Fingerprint / biometric bridge)* WebToApk.isBiometricAvailable()            -> "available" | "no_hardware"                                                | "not_enrolled" | "unavailable"   WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses:   window.__webToApkAuth = window.__webToApkAuth || {};   window.__webToApkAuth['unlock'] = function (json) {       var r = JSON.parse(json);      // {ok:true} | {ok:false,error:"..."}       if (r.ok) showApp();   };   WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');

### `isFolderAccessEnabled`

```js
window.WebToApk.isFolderAccessEnabled(): String
```

*(Native Folder Access (SAF) Bridge)* 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)

### `isFullscreen`

```js
window.WebToApk.isFullscreen(): Boolean
```

*(Fullscreen bridge)* Lets the page hide the status bar (clock/battery) and navigation bar itself — e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk — there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

### `isInterstitialAdReady`

```js
window.WebToApk.isInterstitialAdReady(): Boolean
```

True when an interstitial is loaded AND past its cooldown (matches exactly what showInterstitialAd() will accept — no "ready" that then refuses to show).

### `isKeyHardwareBacked`

```js
window.WebToApk.isKeyHardwareBacked(alias: String): String
```

True when [alias] lives in the TEE or a secure element rather than in software. Worth checking before promising a user that destroying a key is irreversible.

### `isKeystoreEnabled`

```js
window.WebToApk.isKeystoreEnabled(): String
```

Whether the creator enabled the keystore bridge for this app.

### `isOwned`

```js
window.WebToApk.isOwned(productId: String): Boolean
```

Instant on-device ownership check (one-time product owned OR sub active).

### `isPremium`

```js
window.WebToApk.isPremium(): Boolean
```

True when the user owns "Remove Ads" (Play Billing entitlement, auto-restored on every launch). Matches the reason 'ad_free' in 'appmint:ad-unavailable'.

### `isRewardedAdReady`

```js
window.WebToApk.isRewardedAdReady(): Boolean
```

True when a rewarded ad is loaded and can be shown right now. Let the page enable/disable its "Watch ad" button off this (poll or call before showing).

### `isSecureScreen`

```js
window.WebToApk.isSecureScreen(): Boolean
```

*(Screen-capture protection)* WebToApk.setSecureScreen(true)   // block screenshots on THIS page   WebToApk.setSecureScreen(false)  // release it early   WebToApk.isSecureScreen()        // -> boolean  Protection belongs to the page that asked for it. It is released the moment the app navigates anywhere else — a new document, a pushState route, a #hash route — so a protected screen cannot leave the rest of the app locked down, and a page that wants protection back after a reload simply asks again.  What it does: screenshots, screen recording, casting and the recents-screen thumbnail all stop while it is on. What it cannot do: stop a camera pointed at the screen. Nothing on Android can, and no bridge here will pretend otherwise.

### `listCallLog`

```js
window.WebToApk.listCallLog(requestId: String, limit: Int, offset: Int, sinceMillis: String)
```

*(Call log)* WebToApk.listCallLog(id, limit, offset, sinceMillis) Delivered as `appmint:calllog` / window.onAppMintCallLog.

### `listContacts`

```js
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
```

*(Contacts)* All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread — an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works — the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

### `listFolderEntries`

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

*(Native Folder Access (SAF) Bridge)* 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)

### `listKeys`

```js
window.WebToApk.listKeys(): String
```

JSON array of every key alias this page has created.

### `listSms`

```js
window.WebToApk.listSms(requestId: String, box: String, limit: Int, offset: Int)
```

*(SMS)* WebToApk.composeSms(number, message)   <- NO permission, opens the SMS app   WebToApk.sendSms(id, number, message, wantDelivery)   WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms   {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.

### `makePhoneCall`

```js
window.WebToApk.makePhoneCall(phoneNumber: String)
```

Places a call directly when the creator enabled the Phone permission, otherwise opens the dialer with the number prefilled (which needs no permission). A plain `tel:` link in your HTML also works and is often enough.

### `markPushRead`

```js
window.WebToApk.markPushRead(id: String): Boolean
```

Marks one push message read; pass "" to mark every message read.

### `mkdir`

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

*(Native Folder Access (SAF) Bridge)* 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)

### `navSettled`

```js
window.WebToApk.navSettled()
```

Companion to [navStarted]: the swapped-in view has painted.

### `navStarted`

```js
window.WebToApk.navStarted()
```

Internal (called by the injected navigation observer, not creator code): the page is changing view without changing its URL. This is the ONLY signal for it — a single-HTML app that swaps DOM nodes fires no onPageStarted, no onPageFinished, no doUpdateVisitedHistory, and leaves onProgressChanged pinned at 100, so from native the tap and the new screen are completely invisible. Ignored while a real document load is running: that load owns the bar, and its own callbacks decide when it ends.

### `notify`

```js
window.WebToApk.notify(optionsJson: String): Boolean
```

Rich notification. See NotificationBridge.show for the full option list: channel (urgent|default|quiet|ongoing), image, largeIcon, bigText, ongoing, actions, progress, sound, silent, group, color. Image fetching and decoding happen off the main thread, so a remote image can never block the page.

### `notifyUserAction`

```js
window.WebToApk.notifyUserAction()
```

Internal (called by the injected tap counter, not creator code): one meaningful user interaction happened in the page. Counts toward the "interstitial every N actions" cadence ONLY while no URL-based navigation has ever been observed — single-HTML apps that switch screens via DOM show/hide never change the URL, so without this their navigation counter stayed at 0 forever and configured ads never showed (TicTrek complaint class, 2026-07-26). URL/SPA apps are unaffected: their first real navigation permanently switches counting back to URL signals.

### `openedFileChunk`

```js
window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String
```

*("Open with this app" file collection)* The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices — nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

### `openedFileCollected`

```js
window.WebToApk.openedFileCollected(token: String)
```

*("Open with this app" file collection)* The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices — nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

### `openedFileMaxChunk`

```js
window.WebToApk.openedFileMaxChunk(): Int
```

*("Open with this app" file collection)* The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices — nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

### `openedFileRelease`

```js
window.WebToApk.openedFileRelease(token: String)
```

*("Open with this app" file collection)* The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices — nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

### `openedFilesPending`

```js
window.WebToApk.openedFilesPending(): String
```

*("Open with this app" file collection)* The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices — nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

### `pickContact`

```js
window.WebToApk.pickContact(requestId: String)
```

System contact picker — needs NO permission, and is the right choice whenever the page just wants the user to choose one person. Picks from the PHONE table, not the contact table: the picker grants read access only to the row it returns, so picking a phone row is what makes the number readable without READ_CONTACTS. Picking a bare contact would return a row with a name and no number, and reading the number from it would need the permission this method exists to avoid.

### `pickFile`

```js
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. @param mimeFilter e.g. "image/\*", "application/pdf", or "" for any file. (The star is backslash-escaped only because Kotlin block comments nest.)

### `playClick`

```js
window.WebToApk.playClick()
```

Plays the app's tap sound on demand, so a page can fire it on its OWN semantic events (a button press) instead of on every stray tap — which is what a native app actually does, and the reason clickSoundMode can be "off" while this still works. Honours the creator's mode; no-op when tap sound is off.

### `prepareNotificationSound`

```js
window.WebToApk.prepareNotificationSound(spec: String): Boolean
```

Copies a bundled audio file into place ahead of time and reports whether it is usable as a notification sound. Optional — `notify()` and `scheduleNotificationEx()` do it themselves — but it lets a settings screen verify a sound the moment the user picks it.

### `print`

```js
window.WebToApk.print()
```

*(Printing)* Hands the page as it currently stands to Android's print dialogue, which can print on paper or save a PDF. Nothing to configure — the WebView renders the document.

### `purchase`

```js
window.WebToApk.purchase(productId: String)
```

Opens Google's purchase sheet for [productId] (one-time product or subscription — resolved automatically). Grant ONLY on 'appmint:purchase' {productId, owned:true}; failures fire 'appmint:purchase-failed' {reason}. Already-owned re-fires the purchase event so the page can idempotently unlock.

### `rateApp`

```js
window.WebToApk.rateApp()
```

Opens the Play listing's rating flow. `rateApp()` already existed but only the native drawer entry could reach it, so an app that draws its own UI — every AI build, where the drawer is off — had no way to ask for a rating. It is the one store action a page genuinely cannot perform for itself: `market://` is an intent, not a navigable URL. Gated on the creator's own "Rate App Button" switch, exactly like the drawer entry and the bottom-nav Share item: turning Rate off must mean no rating surface anywhere, or the toggle looks broken.

### `readClipboard`

```js
window.WebToApk.readClipboard(): String
```

*(Share, clipboard and window)* The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

### `readFileBase64`

```js
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.

### `readTextFile`

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

*(Native Folder Access (SAF) Bridge)* 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)

### `releaseFileAccess`

```js
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.

### `renameEntry`

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

*(Native Folder Access (SAF) Bridge)* 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)

### `requestExactAlarms`

```js
window.WebToApk.requestExactAlarms(): Boolean
```

Opens the system screen where exact alarms are granted. Android 12+.

### `requestFolderAccess`

```js
window.WebToApk.requestFolderAccess(requestId: String)
```

*(Native Folder Access (SAF) Bridge)* 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)

### `requestNotificationPermission`

```js
window.WebToApk.requestNotificationPermission(): String
```

*(Notification permission, cancelling and listing)* Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

### `requestOpenAppPermission`

```js
window.WebToApk.requestOpenAppPermission(): Boolean
```

Opens the system screen where the full-screen alert is granted. Android 14+.

### `restorePushMessage`

```js
window.WebToApk.restorePushMessage(id: String): Boolean
```

Undo a dismiss: puts the message back in its place in the inbox.

### `scheduleNotification`

```js
window.WebToApk.scheduleNotification(id: String, title: String, body: String, triggerAtMillis: String, repeat: String): Boolean
```

===== Tier-2: SCHEDULED local notifications (reminders) ===== Fire a notification at a future time even when the app is closed — backed by AlarmManager + persistence so it survives reboot. Dormant unless called. JS contract (all args strings; triggerAtMillis = epoch millis): window.WebToApk.scheduleNotification(id, title, body, triggerAtMillis, repeat) repeat ∈ "none" | "minutely" | "hourly" | "daily" | "weekly" window.WebToApk.cancelNotification(id) window.WebToApk.cancelAllNotifications() window.WebToApk.getScheduledNotifications()  -> JSON array string

### `scheduleNotificationEx`

```js
window.WebToApk.scheduleNotificationEx(id: String, optionsJson: String): Boolean
```

The full form of [scheduleNotification]: everything `notify()` accepts, fired at a future time with the app closed. WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({ at: Date.now() + 3600000,   // epoch millis (alias: triggerAt) repeat: 'daily',            // none|minutely|hourly|daily|weekly title: 'Time for your dose', body: 'Vitamin D — 1 tablet', sound: '/audio/chime.mp3',  // a file in YOUR bundle; 'silent' for none openApp: true               // open the app itself, over the lock screen })); A bundled sound is copied into place during this call, so a wrong file name is reported now (false) rather than turning into the default sound days later.

### `searchContacts`

```js
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
```

*(Contacts)* All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread — an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works — the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

### `sendSms`

```js
window.WebToApk.sendSms(
```

*(SMS)* WebToApk.composeSms(number, message)   <- NO permission, opens the SMS app   WebToApk.sendSms(id, number, message, wantDelivery)   WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms   {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.

### `setAppShortcuts`

```js
window.WebToApk.setAppShortcuts(itemsJson: String)
```

Launcher shortcuts (long-press the app icon). Up to four entries: `[{id, title, route}]`, where `route` is a path in the app ('/new-note'). A tap opens the app with that route as its launch deep link (`appmint-shortcut://<package><route>`), which the page reads through `launchUrl()` and routes on exactly like any other deep link. Pass `[]` to remove them. Android 7.1+; a no-op below that.

### `setFullscreen`

```js
window.WebToApk.setFullscreen(enabled: Boolean)
```

*(Fullscreen bridge)* Lets the page hide the status bar (clock/battery) and navigation bar itself — e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk — there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

### `setMediaMetadata`

```js
window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)
```

*(Media Notification Bridge)* Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

### `setPlaybackState`

```js
window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)
```

*(Media Notification Bridge)* Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

### `setSecureScreen`

```js
window.WebToApk.setSecureScreen(enabled: Boolean): Boolean
```

Sets protection for the CURRENT page and returns the state now in force. The window flag itself is applied on the UI thread, but the state this returns is already committed, so a page can call this and immediately reveal its sensitive content without waiting for a callback.

### `setStatusBarColor`

```js
window.WebToApk.setStatusBarColor(color: String, style: String): Boolean
```

*(System bar colour)* WebToApk.setStatusBarColor('#0B0F1A', 'light')   // light icons on a dark bar   WebToApk.setStatusBarColor('#FFFFFF', 'dark')    // dark icons on a light bar   WebToApk.setStatusBarColor('#0B0F1A', '')        // icons chosen from luminance  Why the shell and not Capacitor's StatusBar plugin: on API 35+ the plugin's setBackgroundColor is a no-op (it writes window.statusBarColor, which enforced edge-to-edge ignores), so appmintNative's setStatusBar() reported 'native' and changed nothing on every Android 15 device. The shell owns the strip — see systemBarCanvas — so it is the only party that can actually paint it there. Returns false for an unparseable colour; hidden bars (fullscreen/kiosk) accept the colour for when they reappear.

### `shareFiles`

```js
window.WebToApk.shareFiles(filesJson: String, title: String, text: String): Boolean
```

TRUE native file share for navigator.share({files:[...]}). Android WebView has no Web Share API, and text/plain ACTION_SEND can't carry a file — so before this a shared image was merely SAVED, not shared. filesJson is [{name, mimeType, base64}, ...]; we write each to cache, expose it via the app's FileProvider, and fire ACTION_SEND / ACTION_SEND_MULTIPLE so the real Android share sheet (WhatsApp, Gmail, …) opens. Returns true if the sheet opened.

### `shareNative`

```js
window.WebToApk.shareNative(title: String, text: String, url: String)
```

*(Share, clipboard and window)* The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

### `showInterstitialAd`

```js
window.WebToApk.showInterstitialAd()
```

Show an interstitial NOW (e.g. between levels / on a deliberate action). Not showable right now → skipped + 'appmint:ad-unavailable' with the reason.

### `showNotification`

```js
window.WebToApk.showNotification(title: String, body: String, icon: String, tag: String): Boolean
```

The ORIGINAL four-argument form, kept verbatim as an adapter so every app already in the wild and every guide snippet keeps working. It now forwards to the rich builder — which means the `icon` argument, silently discarded by the old implementation, finally does something.

### `showRewardedAd`

```js
window.WebToApk.showRewardedAd()
```

Show a rewarded ad NOW (user-initiated — the only policy-compliant way to show rewarded). Pair it with trigger mode "On Demand" in the wizard to get rewarded ads with NO automatic app-open/navigation ads at all. The reward is delivered back to the page via deliverRewardToPage(): window.addEventListener('appmint:reward', e => { /* e.detail = {type,amount} */ }) When the ad closes, 'appmint:ad-closed' fires with {adType:'rewarded', earned} — earned is false if the user backed out early, so grant nothing in that case. Not showable right now → skipped + 'appmint:ad-unavailable' fired with the reason.

### `signInWithGoogle`

```js
window.WebToApk.signInWithGoogle(serverClientId: String, callbackId: String)
```

===== Native Google Sign-In via Credential Manager ===== Bypasses Google's Android WebView OAuth block (disallowed_useragent error) by running the sign-in flow in native Android and bridging just the resulting Google ID token back to JS, where the Firebase Web SDK consumes it via GoogleAuthProvider.credential(idToken) + signInWithCredential(...). JS contract: window.WebToApk.signInWithGoogle(serverClientId, callbackId) -> on completion, window.__webToApkAuth[callbackId](resultJsonString) is called resultJsonString = '{"ok":true,"idToken":"...","email":"...","displayName":"..."}' or '{"ok":false,"error":"<message>"}'

### `signOutGoogle`

```js
window.WebToApk.signOutGoogle(callbackId: String)
```

Clears the saved Credential Manager state so the next signInWithGoogle call shows the account picker again (rather than silently re-selecting the last account). Generated apps should call this on user-initiated sign-out, in addition to firebase.auth().signOut().

### `splashHold`

```js
window.WebToApk.splashHold()
```

Keeps the branded launch splash up until [splashReady] is called. By default the splash hides as soon as the body has laid-out children, which for an app that paints a skeleton and then fetches its first screen means "splash, then a skeleton". An app that would rather reveal a finished first screen calls this at import and [splashReady] once that screen is on. Capped at the same 6s backstop as the paint poll, so a page that never reports ready still opens.

### `splashReady`

```js
window.WebToApk.splashReady()
```

The first screen is on — hide the launch splash now (subject to the creator's minimum splash duration). Idempotent; harmless when nothing was held.

### `startRemoveAdsPurchase`

```js
window.WebToApk.startRemoveAdsPurchase()
```

Launch the Google Play "Remove Ads" purchase flow from the page (user-initiated, e.g. the app's own "Remove ads" button). Without this the ONLY entry point was the native side-menu item — unreachable in apps built without the side menu. Outcome reaches the page via 'appmint:premium' {premium:true}; already-owned re-notifies immediately; flow errors surface as a native toast.

### `statFile`

```js
window.WebToApk.statFile(pathOrUri: String): String
```

File metadata without reading any of it. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`.

### `toggleFullscreen`

```js
window.WebToApk.toggleFullscreen()
```

*(Fullscreen bridge)* Lets the page hide the status bar (clock/battery) and navigation bar itself — e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk — there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

### `ttsCancel`

```js
window.WebToApk.ttsCancel()
```

*(Text-to-speech)* Backs the window.speechSynthesis polyfill, which is installed at document start in every frame — pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

### `ttsGetVoices`

```js
window.WebToApk.ttsGetVoices(): String
```

JSON array of installed voices: [{name, lang, default}]. Empty until the engine finishes init — the page gets 'voiceschanged' then and re-queries, exactly like Chrome's async getVoices() contract.

### `ttsIsSpeaking`

```js
window.WebToApk.ttsIsSpeaking(): Boolean
```

*(Text-to-speech)* Backs the window.speechSynthesis polyfill, which is installed at document start in every frame — pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

### `ttsSpeak`

```js
window.WebToApk.ttsSpeak(text: String, lang: String, rate: Double, pitch: Double, utteranceId: String)
```

*(Text-to-speech)* Backs the window.speechSynthesis polyfill, which is installed at document start in every frame — pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

### `ttsWarmUp`

```js
window.WebToApk.ttsWarmUp()
```

*(Text-to-speech)* Backs the window.speechSynthesis polyfill, which is installed at document start in every frame — pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

### `vibrate`

```js
window.WebToApk.vibrate(durationMs: Long)
```

*(Vibration)* Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

### `vibratePattern`

```js
window.WebToApk.vibratePattern(patternJson: String)
```

Vibrates an on/off pattern, e.g. "[100,50,100]" = buzz 100ms, pause 50, buzz 100. Backs navigator.vibrate(array); prefer the standard API. No-op unless the creator enabled the Vibrate permission.

### `wbConnect`

```js
window.WebToApk.wbConnect(requestId: String, deviceId: String)
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbDisconnect`

```js
window.WebToApk.wbDisconnect(deviceId: String)
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbGetCharacteristics`

```js
window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbIsAvailable`

```js
window.WebToApk.wbIsAvailable(): Boolean
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbNotify`

```js
window.WebToApk.wbNotify(
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbRead`

```js
window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbRequestDevice`

```js
window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `wbWrite`

```js
window.WebToApk.wbWrite(
```

*(Web Bluetooth (navigator.bluetooth))* Raw transport for WebBluetoothPolyfill. A page never calls these directly — it calls navigator.bluetooth, exactly as it would in a browser.

### `workCancel`

```js
window.WebToApk.workCancel(id: String)
```

*(Background jobs (WorkManager): durable auto-save + periodic native sync)* No permission needed; results echo as `appmint:work` / window.onAppMintWork.

### `workEnqueueOnline`

```js
window.WebToApk.workEnqueueOnline(id: String, url: String, method: String, headersJson: String, payload: String)
```

*(Background jobs (WorkManager): durable auto-save + periodic native sync)* No permission needed; results echo as `appmint:work` / window.onAppMintWork.

### `workList`

```js
window.WebToApk.workList(requestId: String)
```

*(Background jobs (WorkManager): durable auto-save + periodic native sync)* No permission needed; results echo as `appmint:work` / window.onAppMintWork.

### `workSchedulePeriodic`

```js
window.WebToApk.workSchedulePeriodic(id: String, url: String, method: String, headersJson: String, payload: String, intervalMinutes: Int)
```

*(Background jobs (WorkManager): durable auto-save + periodic native sync)* No permission needed; results echo as `appmint:work` / window.onAppMintWork.

### `writeFileBase64`

```js
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.

### `writeTextFile`

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

*(Native Folder Access (SAF) Bridge)* 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)

