# Device and app information - JavaScript bridge API

> What phone this is, which build, a stable per-install identifier, and the standard location and camera APIs.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/device

### `getUserMedia`

```js
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 your code calls `getUserMedia({ video: … })` or `{ audio: … }` (or uses `<input capture>`, `BarcodeDetector`, `SpeechRecognition`), the build finds the call and turns the matching switch on for you; `{ video: false }` does not turn Camera on.

**Show the back camera in a video element.**

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

```js
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);
}
```

**Take a photo from the camera stream** and release the camera.

```js
async function snapshot() {
  if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) return null;
  let stream;
  try {
    stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
  } catch (err) {
    showMessage(err.name === 'NotFoundError' ? 'No camera on this device.' : 'Camera permission was refused.');
    return null;
  }
  const video = document.createElement('video');
  video.srcObject = stream;
  await video.play();
  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth; canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video, 0, 0);
  stream.getTracks().forEach(function (t) { t.stop(); });
  return canvas.toDataURL('image/jpeg', 0.9);
}
```

**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`

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

```js
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();
});
```

Resume watching on the next launch without a new tap, once the user has already said yes:

```js
async function watchAwayIfAllowed() {
  if (!('IdleDetector' in window)) return;
  const perm = await navigator.permissions.query({ name: 'idle-detection' });
  if (perm.state !== 'granted') return;          // 'prompt' until requestPermission() ran from a tap
  const idle = new IdleDetector();
  idle.onchange = () => {
    if (idle.screenState === 'locked') pauseLiveSession();
    else if (idle.userState === 'active') resumeLiveSession();
  };
  await idle.start({ threshold: 60000 });         // 1 minute is the smallest threshold
}
watchAwayIfAllowed();
```

Let the user change the auto-lock delay: stop the running detector, start a new one, and report the real errors:

```js
let vaultWatch = null;
async function setAutoLock(minutes) {
  if (vaultWatch) vaultWatch.abort();             // aborting stops the old detector
  vaultWatch = new AbortController();
  const idle = new IdleDetector();
  idle.addEventListener('change', () => {
    if (idle.userState === 'idle' || idle.screenState === 'locked') lockVault();
  });
  try {
    await idle.start({ threshold: minutes * 60000, signal: vaultWatch.signal });
  } catch (err) {
    if (err.name === 'NotAllowedError') showMessage('Tap "Enable auto-lock" first.');
    else if (err instanceof TypeError) showMessage('Pick at least 1 minute.');
    else throw err;
  }
}
```

**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`

```js
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 your code calls `navigator.geolocation`, `getCurrentPosition` or `watchPosition`, the build finds that call and turns location on for you; you can also tick it yourself.

**Get the position once.**

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

```js
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; }
}
```

**Check the permission first, then ask for a position only when it can be answered:**

```js
async function locateIfAllowed() {
  if (!('geolocation' in navigator)) return;
  if (navigator.permissions) {
    const s = await navigator.permissions.query({ name: 'geolocation' });
    if (s.state === 'denied') { showMessage('Location is off for this app. Turn it on in Settings.'); return; }
  }
  navigator.geolocation.getCurrentPosition(
    (pos) => centerMap(pos.coords.latitude, pos.coords.longitude),
    (err) => showMessage('Location error ' + err.code + ': ' + err.message),
    { timeout: 10000 }
  );
}
```

**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`

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

```js
// 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;
```

Open the companion app when it is installed (the entry's `url` comes from your manifest), otherwise send the user to its store page:

```js
// manifest: { "platform": "play", "id": "com.example.scanner", "url": "https://example.com/scan" }
document.getElementById('open-scanner').addEventListener('click', async () => {
  const apps = typeof navigator.getInstalledRelatedApps === 'function'
    ? await navigator.getInstalledRelatedApps() : [];
  const scanner = apps.find((a) => a.id === 'com.example.scanner');
  location.href = scanner && scanner.url
    ? scanner.url
    : 'https://play.google.com/store/apps/details?id=com.example.scanner';
});
```

List which of your apps are already on the phone on a "More from us" screen:

```js
async function renderMoreApps(catalog) {         // [{ id, name }] matching your manifest entries
  if (typeof navigator.getInstalledRelatedApps !== 'function') return; // e.g. inside an iframe
  const installed = new Set((await navigator.getInstalledRelatedApps()).map((a) => a.id));
  document.getElementById('more-apps').innerHTML = catalog.map((app) =>
    `<li>${app.name} — ${installed.has(app.id) ? 'Installed' : '<a href="https://play.google.com/store/apps/details?id=' + app.id + '">Get it</a>'}</li>`
  ).join('');
}
```

**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`

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

```js
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));
}
```

**Ask only when the page can still ask, and send the user to Settings otherwise:**

```js
async function ensureMic() {
  if (!(navigator.permissions && navigator.permissions.query)) return true;   // a browser without it
  const s = await navigator.permissions.query({ name: 'microphone' });
  if (s.state === 'denied') {
    showMessage('Microphone is off for this app. Turn it on in Settings.');
    return false;
  }
  return true;   // 'granted', or 'prompt' (Android asks on first use)
}

document.getElementById('record').onclick = async () => {
  if (await ensureMic()) startRecording();
};
```

**React when the user changes a permission in Settings, and combine it with a real request:**

```js
async function watchLocationSwitch() {
  const status = await navigator.permissions.query({ name: 'geolocation' });
  status.addEventListener('change', () => {
    if (status.state === 'granted') {
      navigator.geolocation.getCurrentPosition((p) => showNearby(p.coords.latitude, p.coords.longitude));
    } else {
      hideNearby();
    }
  });
}
watchLocationSwitch();
```

**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`

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

```js
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 + ')';
  }
});
```

Read a serial number from the live camera preview (a playing `<video>` is passed as it is, its current frame is read):

```js
const video = document.querySelector('video');
async function startPreview() {
  video.srcObject = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
  await video.play();
}

document.getElementById('read-serial').addEventListener('click', async () => {
  if (!('TextDetector' in window)) return;
  const lines = await new TextDetector().detect(video);
  const hit = lines.map((l) => l.rawValue).find((t) => /S\/?N[:\s]*[A-Z0-9-]{6,}/i.test(t));
  serialField.value = hit ? hit.replace(/^.*S\/?N[:\s]*/i, '') : '';
  if (!hit) showMessage('No serial number in view — move closer and try again.');
});
```

Outline every recognised line on top of the photo, using `boundingBox` (image pixels):

```js
async function outlineText(img, canvas) {
  await img.decode();                             // detect() rejects InvalidStateError before it has loaded
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  const g = canvas.getContext('2d');
  g.drawImage(img, 0, 0);
  const lines = await new TextDetector().detect(img);
  g.strokeStyle = '#ff3b30';
  g.lineWidth = 3;
  for (const { boundingBox: b, rawValue } of lines) {
    g.strokeRect(b.x, b.y, b.width, b.height);
    console.log(rawValue);
  }
  return lines.length;
}
```

Hide the scan button where text recognition is absent (no Google Play services), and pull contact details from a business card:

```js
const scanBtn = document.getElementById('scan-card');
scanBtn.hidden = !('TextDetector' in window);
document.getElementById('card-photo').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  const text = (await new TextDetector().detect(file)).map((l) => l.rawValue).join('\n');
  fillContact({
    email: (text.match(/[\w.+-]+@[\w-]+\.[\w.]+/) || [''])[0],
    phone: (text.match(/\+?\d[\d\s().-]{7,}\d/) || [''])[0],
  });
});
```

**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`

```js
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:**

```js
button.onclick = async () => {
  const state = await IdleDetector.requestPermission();
  console.log(state); // 'granted'
};
```

Feature-detect, and treat a refusal as "not allowed":

```js
async function askIdlePermission() {
  if (!('IdleDetector' in window)) return false;
  try {
    return (await IdleDetector.requestPermission()) === 'granted';
  } catch (err) {
    return false;
  }
}
```

Request it from a tap, then start detecting:

```js
startBtn.addEventListener('click', async () => {
  if (!(await askIdlePermission())) { showMessage('Idle detection was not allowed.'); return; }
  const idle = new IdleDetector();
  idle.addEventListener('change', () => console.log(idle.userState));
  await idle.start({ threshold: 60000 });
});
```

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

### `__idleState`

```js
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:**

```js
const idle = new IdleDetector();
idle.onchange = () => console.log(idle.userState, idle.screenState);
await idle.start({ threshold: 60000 });
```

Stop watching with an AbortController when the screen closes:

```js
const ctl = new AbortController();
const idle = new IdleDetector();
idle.addEventListener('change', () => {
  if (idle.userState === 'idle') showAwayBanner();
});
await idle.start({ threshold: 60000, signal: ctl.signal });
// later: ctl.abort();
```

Lock the app when the screen locks:

```js
if ('IdleDetector' in window && (await IdleDetector.requestPermission()) === 'granted') {
  const d = new IdleDetector();
  d.onchange = () => { if (d.screenState === 'locked') showPinScreen(); };
  await d.start({ threshold: 60000 });
}
```

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

### `__permissionState`

```js
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:**

```js
const status = await navigator.permissions.query({ name: 'camera' });
showCameraHint(status.state);                       // 'granted' | 'denied' | 'prompt'
status.addEventListener('change', () => showCameraHint(status.state));
```

Check several permissions at once:

```js
const names = ['camera', 'microphone', 'geolocation', 'notifications'];
const states = await Promise.all(names.map((name) =>
  navigator.permissions.query({ name }).then((s) => s.state, () => 'unknown')));
names.forEach((name, i) => console.log(name, states[i]));
```

Ask only when the state is not `denied`, and say what to do when it is:

```js
async function ensureLocation() {
  const s = await navigator.permissions.query({ name: 'geolocation' });
  if (s.state === 'denied') { showMessage('Turn location on in the phone settings.'); return null; }
  return new Promise((ok, fail) => navigator.geolocation.getCurrentPosition(ok, fail));
}
```

**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`

```js
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:**

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

Show the "Get the companion app" button only when it is not installed:

```js
async function showCompanionLink() {
  if (!navigator.getInstalledRelatedApps) return;
  const apps = await navigator.getInstalledRelatedApps();
  document.getElementById('get-companion').hidden = apps.length > 0;
}
```

The packages come from the web app manifest the page links:

```html
<link rel="manifest" href="/manifest.webmanifest">
<!-- manifest.webmanifest: "related_applications": [{ "platform": "play", "id": "com.example.companion" }] -->
```

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

### `getAppName`

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

```js
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 + '!' });
});
```

**Put the app name in the page title and a heading:**

```js
if (window.WebToApk && WebToApk.getAppName) {
  const name = WebToApk.getAppName();
  document.title = name;
  document.querySelector('h1').textContent = name;
}
```

**Build a support email that names the app and its version:**

```js
function supportMailto() {
  const b = window.WebToApk;
  const name = b && b.getAppName ? b.getAppName() : 'our app';
  const version = b && b.getAppVersion ? b.getAppVersion() : 'web';
  const subject = encodeURIComponent('Help with ' + name + ' ' + version);
  return 'mailto:support@example.com?subject=' + subject;
}
document.getElementById('contact').href = supportMailto();
```

**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`

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

**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.**

```js
var version = (window.WebToApk && window.WebToApk.getAppVersion)
  ? WebToApk.getAppVersion()
  : 'web';

document.getElementById('about').textContent = 'Version ' + version;
```

**Ask for an update when the installed version is old:**

```js
function isOlder(a, b) {            // "1.4.2" < "1.10.0"
  const x = a.split('.').map(Number), y = b.split('.').map(Number);
  for (let i = 0; i < Math.max(x.length, y.length); i++) {
    if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) < (y[i] || 0);
  }
  return false;
}

if (window.WebToApk && WebToApk.getAppVersion && isOlder(WebToApk.getAppVersion(), '1.5.0')) {
  document.getElementById('update-banner').hidden = false;
}
```

**Add the version and the version code from `getDeviceInfo()` to a bug report:**

```js
function reportInfo() {
  const b = window.WebToApk;
  if (!(b && b.getAppVersion)) return 'web';
  let text = 'v' + b.getAppVersion();
  if (b.getDeviceInfo) {
    const info = JSON.parse(b.getDeviceInfo());
    text += ' (build ' + info.app.versionCode + ')';
  }
  return text;
}
document.getElementById('report').value = reportInfo();
```

**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`

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

```js
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:**

```json
{
  "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-…"
}
```

**Warn on a low-storage or low-memory phone** before a large download.

```js
function canDownload(bytesNeeded) {
  if (!(window.WebToApk && WebToApk.getDeviceInfo)) return true;   // browser: let the download try
  const r = JSON.parse(WebToApk.getDeviceInfo()).runtime;
  if (r.storageFreeBytes < bytesNeeded * 2) { showMessage('Not enough free storage.'); return false; }
  if (r.lowMemory) showMessage('The phone is low on memory; this may be slow.');
  return true;
}
```

**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`

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

```js
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() })
});
```

**Send the id to your server with a header** and keep it out of the URL.

```js
async function apiGet(path) {
  const id = (window.WebToApk && WebToApk.getInstallId) ? WebToApk.getInstallId() : null;
  const res = await fetch('https://api.example.com' + path, {
    headers: id ? { 'X-Install-Id': id } : {}
  });
  return res.json();
}
```

**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`

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

```js
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"
}
```

**Pick a calling code from the SIM country** for a sign-up form.

```js
const DIAL = { IN: '+91', US: '+1', GB: '+44', DE: '+49' };
function defaultDialCode() {
  if (!(window.WebToApk && WebToApk.getPhoneIdentity)) return '+1';
  const p = JSON.parse(WebToApk.getPhoneIdentity());
  if (p.error || p.simState !== 'ready') return '+1';
  return DIAL[p.simCountryIso.toUpperCase()] || '+1';
}
document.getElementById('dial').value = defaultDialCode();
```

**Tell a Wi-Fi-only tablet from a phone** with `phoneType`.

```js
if (window.WebToApk && WebToApk.getPhoneIdentity) {
  const p = JSON.parse(WebToApk.getPhoneIdentity());
  if (!p.error && p.phoneType === 'none') {
    document.getElementById('sms-login').hidden = true;   // no cellular radio: hide SMS sign-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.

