Create Free APK

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().

navigator.geolocation web standard#

navigator.geolocation

Example

The standard web location API works inside the app. The app shows Android's own location prompt and passes the answer to the page.

Returns: the normal W3C callbacks: a GeolocationPosition (coords.latitude, coords.longitude, coords.accuracy, …) or a GeolocationPositionError (code 1 = permission denied, 2 = unavailable, 3 = timeout). Needs: GPS (precise) and/or Location (approximate) in Step 4 (Access). When you build from a ZIP or an HTML page and your code calls navigator.geolocation, getCurrentPosition or watchPosition, the build finds that call and adds location for you. For a website (URL) build, tick it yourself.

Get the position once.

function whereAmI() {
  if (!('geolocation' in navigator)) { showMessage('Location is not available here.'); return; }

  navigator.geolocation.getCurrentPosition(
    function (pos) {
      showMessage('You are at ' + pos.coords.latitude.toFixed(5) + ', ' +
                  pos.coords.longitude.toFixed(5) + ' (±' + Math.round(pos.coords.accuracy) + ' m)');
    },
    function (err) {
      if (err.code === 1) showMessage('Location permission was refused.');
      else if (err.code === 3) showMessage('Location timed out. Try again outside.');
      else showMessage('Location is unavailable. Is location turned on in phone settings?');
    },
    { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
  );
}

Follow the user while they move (a run tracker, a delivery map). Stop watching when the screen closes.

var watchId = null;

function startTracking() {
  if (!('geolocation' in navigator)) return;
  watchId = navigator.geolocation.watchPosition(
    function (pos) { addPointToMap(pos.coords.latitude, pos.coords.longitude); },
    function (err) { showMessage('Tracking stopped: ' + err.message); },
    { enableHighAccuracy: true }
  );
}

function stopTracking() {
  if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
}

Notes: With GPS on, the app asks for precise and approximate location together; on Android 12+ the user may choose "Approximate", which still works (lower accuracy). With only Location on, only approximate location is asked for. If neither is on, every call fails with code 1 and no prompt. After the user allows it once, later calls are answered without a prompt. The phone's own location switch must also be on. Location is personal data: declare it in your Play Data safety form; background location is not provided.

navigator.getInstalledRelatedApps web standard#

navigator.getInstalledRelatedApps()

Example

The standard related-apps API: tells the page which of its own Android apps (listed in its web app manifest) are installed - to hide an "Install our other app" banner, or deep-link into it.

Returns: Promise of an array of the manifest's related_applications entries that are installed: { platform: 'play', id, url?, version? }. Needs: a <link rel="manifest"> whose manifest lists the apps with "platform": "play" and "id": "<package name>", bundled with the app so the build can declare them (Android only lets an app see packages it names).

// manifest.webmanifest: { "related_applications": [{ "platform": "play", "id": "com.example.scanner" }] }
const apps = await navigator.getInstalledRelatedApps();
const hasScanner = apps.some((a) => a.id === 'com.example.scanner');
document.getElementById('get-scanner').hidden = hasScanner;

Notes: the build reads the manifest in your bundle and declares each listed package; an app not listed there cannot be seen and is never reported. Website-mode apps keep their manifest on the site, so nothing is declared and the list is empty. Internal transport: __relatedAppsInstalled.

navigator.permissions web standard#

navigator.permissions.query({ name })

Example

The standard Permissions API, answered from the phone's real Android permission state - so a page can show "Allow camera" only when it can still be asked, and "Turn it on in Settings" when it cannot.

Returns: a Promise of a PermissionStatus {name, state, onchange}; state is 'granted', 'denied' or 'prompt', and change fires right after a permission dialog closes or when the user comes back from Settings. Needs: nothing to switch on.

async function cameraButton() {
  const btn = document.getElementById('scan');
  const status = await navigator.permissions.query({ name: 'camera' });
  function render() {
    if (status.state === 'denied') {
      btn.textContent = 'Camera is off — enable it in Settings';
      btn.disabled = true;
    } else {
      btn.textContent = status.state === 'granted' ? 'Scan' : 'Allow camera and scan';
      btn.disabled = false;
    }
  }
  render();
  status.onchange = render;   // e.g. after the Android dialog, or back from Settings
}
cameraButton();

// The other names the app answers the same way:
for (const name of ['microphone', 'geolocation', 'notifications']) {
  navigator.permissions.query({ name }).then((s) => console.log(name, s.state));
}

Notes: denied also covers a capability the app was built without (its switch was off in the build, so Android can never grant it) and a "Don't ask again" refusal. screen-wake-lock, clipboard-read and clipboard-write are 'granted' (the app provides them without a prompt). Other names (midi, accelerometer, …) are answered by the WebView as before. Event behind it: appmint:permissions-changed.

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.

__relatedAppsInstalled bridge#

window.WebToApk.__relatedAppsInstalled(idsJson: String): String

navigator.getInstalledRelatedApps(): which of [idsJson] (package names from the page's manifest) are installed and visible to this app.

Example

Internal transport behind navigator.getInstalledRelatedApps(). Pages use the standard API; never call this directly - the __ methods may change without notice.

Returns: JSON array of the given package names that are installed and visible to this app. Needs: the packages declared at build time (from the bundled web app manifest).

Use the public API:

const apps = await navigator.getInstalledRelatedApps();
console.log(apps.map((a) => a.id));

Notes: Implemented in the app shell (IdleAndRelatedApps.kt).

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.

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.