JavaScript bridge API
Device and app information
What phone this is, which build, a stable per-install identifier, and the standard location and camera APIs.
getUserMedia web standard#
navigator.mediaDevices.getUserMedia — camera and microphone
Example
The standard web camera and microphone API works inside the app. The app shows Android's own permission prompt and gives the stream to the page.
Returns: a Promise of a MediaStream, or a rejection (NotAllowedError when the user refused or the switch was off at build time, NotFoundError when there is no such device). Needs: Camera and/or Mic in Step 4 (Access). When you build from a ZIP or an HTML page and your code calls getUserMedia({ video: … }) or { audio: … } (or uses <input capture>, BarcodeDetector, SpeechRecognition), the build finds the call and adds the matching permission for you; { video: false } does not add Camera. For a website (URL) build, tick them yourself.
Show the back camera in a video element.
<video id="preview" autoplay playsinline muted></video>
<button id="startCam">Start camera</button>
<script>
document.getElementById('startCam').addEventListener('click', async function () {
if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
showMessage('Camera is not available here.');
return;
}
try {
var stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }, // 'user' = front camera
audio: false
});
document.getElementById('preview').srcObject = stream;
} catch (err) {
if (err.name === 'NotAllowedError') showMessage('Camera permission was refused.');
else showMessage('Camera error: ' + err.name);
}
});
</script>Record a voice note with the microphone only.
async function recordVoice(seconds) {
var stream;
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
} catch (err) {
showMessage('Microphone permission was refused.');
return;
}
var chunks = [];
var rec = new MediaRecorder(stream);
rec.ondataavailable = function (e) { chunks.push(e.data); };
rec.onstop = function () {
stream.getTracks().forEach(function (t) { t.stop(); }); // turn the mic off
var blob = new Blob(chunks, { type: rec.mimeType });
document.getElementById('player').src = URL.createObjectURL(blob);
};
rec.start();
setTimeout(function () { rec.stop(); }, seconds * 1000);
}Notes: When you ask for camera and mic together, the app asks the user for both at once. If the switch was off when you built, the request is refused with no prompt at all - build again with it on. Always stop the tracks when you are done, or the camera/mic light stays on. DRM video (Widevine) is allowed without any switch. Camera and microphone are sensitive: declare them in your Play Data safety form.
IdleDetector web standard#
new IdleDetector() — Idle Detection
Example
The standard Idle Detection API: learn when the user stops using the phone (no input for a while) or locks the screen, and when they come back - for an "away" status, pausing a live session or locking a vault.
Returns: IdleDetector.requestPermission() → 'granted' or 'denied' (needs a tap); detector.start({threshold, signal}) → Promise (rejects NotAllowedError without the permission, TypeError under 60000 ms); then userState ('active'/'idle'), screenState ('locked'/'unlocked') and a change event. Needs: nothing to switch on and no Android permission.
document.getElementById('away').addEventListener('click', async () => {
if (!('IdleDetector' in window)) return;
if ((await IdleDetector.requestPermission()) !== 'granted') return;
const ac = new AbortController();
const idle = new IdleDetector();
idle.addEventListener('change', () => {
setStatus(idle.userState === 'idle' || idle.screenState === 'locked' ? 'Away' : 'Online');
});
await idle.start({ threshold: 5 * 60000, signal: ac.signal });
stopButton.onclick = () => ac.abort();
});Notes: in the app, "input" is the user's input to THIS app while it is in front; while another app is in front with the screen on, the user counts as active (an app cannot see other apps' input - Chrome on Android answers the same way); once the screen is off or locked, the idle time runs from the last touch. The permission is remembered, and navigator.permissions.query({name:'idle-detection'}) answers it. Internal transports: __idleState, __idlePermission; lock/unlock changes arrive as appmint:idle. AI-built apps use watchIdle().
TextDetector web standard#
new TextDetector().detect(image) — text recognition (OCR)
Example
Reads the text in a picture on the phone itself - a receipt total, a serial number, a business card - with Google's ML Kit recognizer. The shape is the Shape Detection API's TextDetector, the one Chrome ships behind a flag.
Returns: Promise of an array, one entry per line: { rawValue, boundingBox, cornerPoints } in the image's pixels. Rejects NotSupportedError without Google Play services and OperationError when recognition fails. Needs: nothing to switch on and no permission (to read from the camera, take the photo with <input type="file" accept="image/*" capture> or a camera stream).
document.getElementById('photo').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file || !('TextDetector' in window)) return;
status.textContent = 'Reading…';
try {
const lines = await new TextDetector().detect(file); // a Blob, <img>, <canvas>, <video> or ImageBitmap
const text = lines.map((l) => l.rawValue).join('\n');
const total = /total\s*[:$€£]?\s*([\d.,]+)/i.exec(text);
status.textContent = total ? 'Total: ' + total[1] : text;
} catch (err) {
status.textContent = 'Could not read the text (' + err.name + ')';
}
});Notes: Latin scripts only (English, Spanish, French, German, Indonesian, Vietnamese, Turkish and others); Chinese, Devanagari, Japanese, Korean and Arabic are not recognised. The model comes from Google Play services: the build asks for it at install when your code uses it, otherwise the first call on a fresh phone waits a few seconds while it downloads. Images from another site must allow CORS (a tainted canvas rejects SecurityError). Internal transports: __ocrAvailable, __ocrRecognize (answered as appmint:ocr). AI-built apps use recognizeText().
__idlePermission bridge#
window.WebToApk.__idlePermission(request: Boolean): String
IdleDetector.requestPermission() ([request] true, called under a user gesture) records the grant; false only reads it. "granted" | "prompt".
Example
Internal transport behind IdleDetector.requestPermission() and the 'idle-detection' permission state. Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: 'granted' or 'prompt'. Needs: a user gesture for the request.
Use the public API:
button.onclick = async () => {
const state = await IdleDetector.requestPermission();
console.log(state); // 'granted'
};Notes: Implemented in the app shell (IdleAndRelatedApps.kt).
__idleState bridge#
window.WebToApk.__idleState(): String
IdleDetector: `{idleMs, locked}` from the app's own input and the keyguard.
Example
Internal transport behind IdleDetector: how long since the user last gave input, and whether the screen is locked. Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: JSON {idleMs, locked}. Screen and app changes are pushed as the appmint:idle event with the same shape. Needs: nothing.
Use the public API:
const idle = new IdleDetector();
idle.onchange = () => console.log(idle.userState, idle.screenState);
await idle.start({ threshold: 60000 });Notes: Implemented in the app shell (IdleAndRelatedApps.kt).
__permissionState bridge#
window.WebToApk.__permissionState(name: String): String
Permissions API state of a W3C permission name, from Android's real grants: "granted" | "denied" | "prompt", or "" when the shell does not answer that name (the page's query then goes to WebView's own implementation).
Example
Internal transport behind navigator.permissions.query({ name }). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: 'granted', 'denied', 'prompt', or '' for a name the app does not answer (the query then goes to the WebView's own). Needs: nothing to switch on.
Use the public API:
const status = await navigator.permissions.query({ name: 'camera' });
showCameraHint(status.state); // 'granted' | 'denied' | 'prompt'
status.addEventListener('change', () => showCameraHint(status.state));Notes: Answers camera, microphone, geolocation, notifications, screen-wake-lock, clipboard-read, clipboard-write. A capability the build left off (or whose permission is not in the manifest) is denied. Live statuses re-read on appmint:permissions-changed (fired after every permission dialog) and when the page becomes visible again.
getAppName bridge#
window.WebToApk.getAppName(): String
The app name the creator chose in the wizard - what the launcher shows.
Example
Gives the app name you chose in the build wizard - the name under the launcher icon.
Returns: a string right away. Needs: nothing.
Use the real app name in a title and a share text.
var appName = (window.WebToApk && window.WebToApk.getAppName)
? WebToApk.getAppName()
: document.title; // in a browser: the page title
document.getElementById('welcome').textContent = 'Welcome to ' + appName;
document.getElementById('shareBtn').addEventListener('click', function () {
if (navigator.share) navigator.share({ text: 'Try ' + appName + '!' });
});Notes: This is the name from the wizard, not the page's <title>. It does not change at run time. For the version, use getAppVersion().
getAppVersion bridge#
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.
Example
Gives the installed app's version name, for example "1.4.2".
Returns: a string right away. Needs: nothing.
Show the version in an About screen.
var version = (window.WebToApk && window.WebToApk.getAppVersion)
? WebToApk.getAppVersion()
: 'web';
document.getElementById('about').textContent = 'Version ' + version;Notes: This is the version NAME you set when building, not the version code. If the package cannot be read it returns "1.0", so it is never empty. The version code and install dates are in getDeviceInfo() under app.
getDeviceInfo bridge#
window.WebToApk.getDeviceInfo(): String
WebToApk.getDeviceInfo() -> JSON string, NO permission required WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission WebToApk.getInstallId() -> stable per-install UUID
Described by its group, Device / app information, rather than on its own.
Example
Gives facts about the phone, Android, the screen, your app and the current state (battery, memory, network) in one call.
Returns: a JSON string right away - use JSON.parse. Top-level keys: android, hardware, screen, app, runtime, installId. Needs: nothing - no permission.
Show phone information.
function showPhoneInfo() {
if (!(window.WebToApk && window.WebToApk.getDeviceInfo)) {
document.getElementById('out').textContent = navigator.userAgent; // browser: only the user agent
return;
}
var info = JSON.parse(WebToApk.getDeviceInfo());
document.getElementById('out').innerHTML =
'Android: ' + info.android.release + ' (SDK ' + info.android.sdkInt + ')<br>' +
'Version ID: ' + info.android.buildId + '<br>' +
'Build number: ' + info.android.incremental + '<br>' +
'Phone: ' + info.hardware.manufacturer + ' ' + info.hardware.model + '<br>' +
'Screen: ' + info.screen.widthPx + 'x' + info.screen.heightPx + ' @' + info.screen.refreshRate + 'Hz<br>' +
'Battery: ' + info.runtime.batteryLevel + '%' + (info.runtime.charging ? ' (charging)' : '') + '<br>' +
'Network: ' + info.runtime.networkType + '<br>' +
'App: ' + info.app.versionName + ' (' + info.app.versionCode + ')<br>' +
'WebView: ' + info.app.webViewPackage;
}All the keys:
{
"android": { "release": "14", "sdkInt": 34, "codename": "REL", "buildId": "UP1A.231005.007", "incremental": "R.1a2b3c", "display": "…", "fingerprint": "…", "securityPatch": "2026-08-05" },
"hardware": { "manufacturer": "OPPO", "brand": "OPPO", "model": "CPH2269", "device": "…", "product": "…", "board": "…", "hardware": "…", "abis": ["arm64-v8a"], "socManufacturer": "…", "socModel": "…", "isEmulator": false },
"screen": { "widthPx": 720, "heightPx": 1600, "density": 2, "densityDpi": 320, "refreshRate": 90, "nightMode": true },
"app": { "packageName": "com.example.app", "versionName": "1.0", "versionCode": 1, "targetSdk": 35, "firstInstallTime": 1737000000000, "lastUpdateTime": 1737000000000, "installer": "com.android.vending", "webViewPackage": "com.google.android.webview 131.0" },
"runtime": { "totalMemoryBytes": 4000000000, "availableMemoryBytes": 1500000000, "lowMemory": false, "storageFreeBytes": 20000000000, "storageTotalBytes": 64000000000, "batteryLevel": 80, "charging": false, "locale": "en-IN", "timezone": "Asia/Kolkata", "networkType": "wifi" },
"installId": "3f2b8c1e-…"
}Notes: socManufacturer / socModel exist only on Android 12+. networkType is wifi, cellular, ethernet, other, none or unknown. installer is com.android.vending for Play installs and empty for side-loads. Values are read at the moment you call - call again to refresh battery or network. There is no IMEI or serial number: Android blocks them for all apps; use getInstallId().
getInstallId bridge#
window.WebToApk.getInstallId(): String
WebToApk.getDeviceInfo() -> JSON string, NO permission required WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission WebToApk.getInstallId() -> stable per-install UUID
Described by its group, Device / app information, rather than on its own.
Example
Gives a random ID that stays the same for this install of your app.
Returns: a string (a UUID such as "3f2b…-…") right away. Needs: nothing.
Use it as a device key for your server or for a "one free trial per device" check.
function deviceKey() {
if (window.WebToApk && window.WebToApk.getInstallId) {
return WebToApk.getInstallId();
}
// In a browser: keep your own random ID in localStorage (per browser, cleared with site data).
var k = localStorage.getItem('installId');
if (!k) {
k = 'w-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
localStorage.setItem('installId', k);
}
return k;
}
fetch('https://api.example.com/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device: deviceKey() })
});Notes: The ID is made on first use and kept in the app's storage. It stays the same after app updates. It changes when the user uninstalls the app or clears its data. It is not a hardware ID - Android gives no IMEI or serial to apps. The same value is also in getDeviceInfo() as installId.
getPhoneIdentity bridge#
window.WebToApk.getPhoneIdentity(): String
WebToApk.getDeviceInfo() -> JSON string, NO permission required WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission WebToApk.getInstallId() -> stable per-install UUID
Described by its group, Device / app information, rather than on its own.
Example
Gives the mobile carrier and SIM facts: carrier name, SIM country, operator code, phone type and SIM state.
Returns: a JSON string right away - use JSON.parse: { carrierName, simCountryIso, networkCountryIso, simOperator, phoneType, simState, hardwareIdAvailable: false }, or { error }. Needs: turn on Phone in Step 4 (Access) when you build; otherwise it returns { "error": "not_enabled" }.
Show the carrier and pick the country code.
function showCarrier() {
if (!(window.WebToApk && window.WebToApk.getPhoneIdentity)) return;
var p = JSON.parse(WebToApk.getPhoneIdentity());
if (p.error === 'not_enabled') { showMessage('Turn on Phone when you build the app'); return; }
if (p.error) { showMessage('Could not read SIM info: ' + p.error); return; }
if (p.simState !== 'ready') {
showMessage('No SIM ready (' + p.simState + ')'); // absent | pin_required | network_locked | unknown
return;
}
document.getElementById('carrier').textContent = p.carrierName; // e.g. "Jio 4G"
document.getElementById('country').value = p.simCountryIso.toUpperCase(); // e.g. "IN"
}Notes: phoneType is gsm, cdma, sip or none (Wi-Fi-only tablets). simOperator is the MCC+MNC code (e.g. "40445"). There is no IMEI, MEID or serial - hardwareIdAvailable is always false, because Android hides them from every app since Android 10; use getInstallId() for a stable id. The Phone switch adds a sensitive permission to your Play listing; declare what you use it for in the Data safety form.