JavaScript bridge API
Информация об устройстве и приложении
Что это за телефон, какая сборка, стабильный идентификатор установки и стандартные API геолокации и камеры.
getUserMedia веб-стандарт#
navigator.mediaDevices.getUserMedia — camera and microphone
Пример
Стандартный веб-API камеры и микрофона работает внутри приложения. Приложение показывает собственный запрос разрешения Android и передаёт поток странице.
Возвращает: Promise с MediaStream или отказ (NotAllowedError, когда пользователь отказал или при сборке переключатель был выключен, NotFoundError, когда такого устройства нет). Требуется: Камера и/или Микрофон на шаге 4 (Доступ). Если вы собираете из ZIP или HTML-страницы и ваш код вызывает getUserMedia({ video: … }) или { audio: … } (или использует <input capture>, BarcodeDetector, SpeechRecognition), сборка находит вызов и сама добавляет нужное разрешение; { video: false } не добавляет «Камеру». Для сборки сайта (URL) отметьте их сами.
Покажите заднюю камеру в элементе video.
<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>Запишите голосовую заметку, используя только микрофон.
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);
}Сделайте фото из потока камеры и освободите камеру.
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);
}Примечания: Когда вы запрашиваете камеру и микрофон вместе, приложение запрашивает у пользователя оба разрешения сразу. Если при сборке переключатель был выключен, запрос отклоняется вообще без диалога — соберите заново с включённым переключателем. Всегда останавливайте треки, когда закончили, иначе индикатор камеры/микрофона останется включённым. DRM-видео (Widevine) разрешено без какого-либо переключателя. Камера и микрофон — чувствительные разрешения: укажите их в форме Data safety в Play.
IdleDetector веб-стандарт#
new IdleDetector() — Idle Detection
Пример
Стандартный Idle Detection API: узнайте, когда пользователь перестал пользоваться телефоном (долго нет ввода) или заблокировал экран и когда он вернулся — для статуса «отошёл», приостановки живого сеанса или блокировки хранилища.
Возвращает: IdleDetector.requestPermission() → 'granted' или 'denied' (нужно нажатие); detector.start({threshold, signal}) → Promise (отклоняется с NotAllowedError без разрешения, TypeError при значении меньше 60000 мс); затем userState ('active'/'idle'), screenState ('locked'/'unlocked') и событие change. Требуется: ничего включать не нужно и разрешения Android не требуется.
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();
});Возобновите слежение при следующем запуске без нового нажатия, если пользователь уже согласился:
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 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;
}
}Примечания: в приложении «ввод» — это ввод пользователя в ЭТО приложение, пока оно на переднем плане; когда на переднем плане другое приложение при включённом экране, пользователь считается активным (приложение не видит ввод в других приложениях — Chrome на Android отвечает так же); когда экран выключен или заблокирован, время простоя отсчитывается от последнего касания. Разрешение запоминается, и ответ на него даёт navigator.permissions.query({name:'idle-detection'}). Внутренние каналы: __idleState, __idlePermission; изменения блокировки/разблокировки приходят как appmint:idle. Приложения, созданные ИИ, используют watchIdle().
TextDetector веб-стандарт#
new TextDetector().detect(image) — text recognition (OCR)
Пример
Читает текст на картинке прямо на телефоне — итог чека, серийный номер, визитку — с помощью распознавателя Google ML Kit. Формат — это TextDetector из Shape Detection API, который Chrome поставляет за флагом.
Возвращает: Promise с массивом, по записи на строку: { rawValue, boundingBox, cornerPoints } в пикселях изображения. Отклоняется с NotSupportedError без сервисов Google Play и с OperationError, когда распознавание не удалось. Требуется: ничего включать не нужно и разрешение не требуется (чтобы читать с камеры, сделайте фото через <input type="file" accept="image/*" capture> или поток камеры).
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 + ')';
}
});Считайте серийный номер с живого превью камеры (проигрываемый <video> передаётся как есть, читается его текущий кадр):
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.');
});Обведите поверх фото каждую распознанную строку, используя boundingBox (пиксели изображения):
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;
}Скройте кнопку сканирования там, где нет распознавания текста (нет сервисов Google Play), и извлеките контактные данные с визитки:
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],
});
});Примечания: Только латинские алфавиты (английский, испанский, французский, немецкий, индонезийский, вьетнамский, турецкий и другие); китайский, деванагари, японский, корейский и арабский не распознаются. Модель поставляется из сервисов Google Play: сборка запрашивает её при установке, если ваш код её использует, а иначе первый вызов на свежем телефоне ждёт несколько секунд, пока она загружается. Изображения с другого сайта должны разрешать CORS (испорченный canvas отклоняет с SecurityError). Внутренние каналы: __ocrAvailable, __ocrRecognize (отвечает как appmint:ocr). Приложения, созданные ИИ, используют recognizeText().
__idlePermission мост#
window.WebToApk.__idlePermission(request: Boolean): String
IdleDetector.requestPermission() ([request] true, вызывается по действию пользователя) фиксирует разрешение; false только читает его. "granted" | "prompt".
Пример
Внутренний канал за IdleDetector.requestPermission() и состоянием разрешения 'idle-detection'. Страницы используют стандартный API; никогда не вызывайте его напрямую — методы __ могут измениться без предупреждения.
Возвращает: 'granted' или 'prompt'. Требуется: действие пользователя для запроса.
Используйте публичный API:
button.onclick = async () => {
const state = await IdleDetector.requestPermission();
console.log(state); // 'granted'
};Проверяйте поддержку возможности и считайте отказ запретом:
async function askIdlePermission() {
if (!('IdleDetector' in window)) return false;
try {
return (await IdleDetector.requestPermission()) === 'granted';
} catch (err) {
return false;
}
}Запросите по нажатию, а затем начинайте обнаружение:
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 });
});Примечания: Реализовано в оболочке приложения (IdleAndRelatedApps.kt).
__idleState мост#
window.WebToApk.__idleState(): String
IdleDetector: `{idleMs, locked}` по собственному вводу приложения и блокировке экрана.
Пример
Внутренний канал за IdleDetector: как давно пользователь вводил что-либо в последний раз и заблокирован ли экран. Страницы используют стандартный API; никогда не вызывайте его напрямую — методы __ могут измениться без предупреждения.
Возвращает: JSON {idleMs, locked}. Изменения экрана и приложения приходят в событии appmint:idle с тем же форматом. Требуется: ничего.
Используйте публичный API:
const idle = new IdleDetector();
idle.onchange = () => console.log(idle.userState, idle.screenState);
await idle.start({ threshold: 60000 });Прекращайте слежение через AbortController, когда экран закрывается:
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();Блокируйте приложение, когда экран блокируется:
if ('IdleDetector' in window && (await IdleDetector.requestPermission()) === 'granted') {
const d = new IdleDetector();
d.onchange = () => { if (d.screenState === 'locked') showPinScreen(); };
await d.start({ threshold: 60000 });
}Примечания: Реализовано в оболочке приложения (IdleAndRelatedApps.kt).
__permissionState мост#
window.WebToApk.__permissionState(name: String): String
Состояние в Permissions API для имени разрешения W3C по настоящим разрешениям Android: "granted" | "denied" | "prompt" или "", когда оболочка не отвечает на это имя (тогда запрос страницы идёт в собственную реализацию WebView).
Пример
Внутренний транспорт для navigator.permissions.query({ name }). Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут измениться без предупреждения.
Возвращает: 'granted', 'denied', 'prompt' или '' для имени, на которое приложение не отвечает (тогда запрос уходит в собственный обработчик WebView). Требуется: ничего включать не нужно.
Используйте публичный API:
const status = await navigator.permissions.query({ name: 'camera' });
showCameraHint(status.state); // 'granted' | 'denied' | 'prompt'
status.addEventListener('change', () => showCameraHint(status.state));Проверьте несколько разрешений сразу:
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]));Спрашивайте, только когда состояние не denied, а когда оно такое, подскажите, что делать:
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));
}Примечания: Отвечает на camera, microphone, geolocation, notifications, screen-wake-lock, clipboard-read, clipboard-write. Возможность, которую сборка оставила выключенной (или чьего разрешения нет в манифесте), — denied. Текущие состояния перечитываются по appmint:permissions-changed (срабатывает после каждого диалога разрешения) и когда страница снова становится видимой.
getAppName мост#
window.WebToApk.getAppName(): String
Название приложения, которое создатель выбрал в мастере, — то, что показывает лаунчер.
Пример
Выдаёт название приложения, которое вы выбрали в мастере сборки, — название под значком в лаунчере.
Возвращает: строку сразу. Требуется: ничего.
Используйте настоящее название приложения в заголовке и в тексте для отправки.
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 + '!' });
});Поставьте название приложения в заголовок страницы и в заголовок на экране:
if (window.WebToApk && WebToApk.getAppName) {
const name = WebToApk.getAppName();
document.title = name;
document.querySelector('h1').textContent = name;
}Составьте письмо в поддержку, где указаны приложение и его версия:
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:[email protected]?subject=' + subject;
}
document.getElementById('contact').href = supportMailto();Примечания: Это название из мастера, а не <title> страницы. Оно не меняется во время работы. Для версии используйте getAppVersion().
getAppVersion мост#
window.WebToApk.getAppVersion(): String
versionName установленного пакета, например «1.4.2» — строка, которую создатель задаёт при сборке, а не versionCode. Если пакет прочитать не удаётся, возвращается «1.0», так что страница может вывести её без проверки на null.
Пример
Выдаёт имя версии установленного приложения, например "1.4.2".
Возвращает: строку сразу. Требуется: ничего.
Покажите версию на экране «О приложении».
var version = (window.WebToApk && window.WebToApk.getAppVersion)
? WebToApk.getAppVersion()
: 'web';
document.getElementById('about').textContent = 'Version ' + version;Попросите обновиться, когда установленная версия устарела:
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;
}Добавьте в отчёт об ошибке версию и код версии из getDeviceInfo():
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();Примечания: Это ИМЯ версии, заданное при сборке, а не код версии. Если пакет прочитать не удаётся, возвращается "1.0", так что результат никогда не пуст. Код версии и даты установки находятся в getDeviceInfo() под app.
getDeviceInfo мост#
window.WebToApk.getDeviceInfo(): String
WebToApk.getDeviceInfo() -> строка JSON, разрешение НЕ требуется WebToApk.getPhoneIdentity() -> оператор/SIM, нужно разрешение Phone WebToApk.getInstallId() -> стабильный UUID установки
Описывается в своей группе, Информация об устройстве / приложении, а не отдельно.
Пример
Выдаёт за один вызов сведения о телефоне, Android, экране, вашем приложении и текущем состоянии (батарея, память, сеть).
Возвращает: JSON-строку сразу — используйте JSON.parse. Ключи верхнего уровня: android, hardware, screen, app, runtime, installId. Требуется: ничего — разрешение не нужно.
Покажите информацию о телефоне.
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;
}Все ключи:
{
"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-…"
}Предупредите, если на телефоне мало места или памяти, перед большой загрузкой.
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;
}Примечания: socManufacturer / socModel есть только в Android 12+. networkType — это wifi, cellular, ethernet, other, none или unknown. installer равно com.android.vending для установок из Play и пусто для установок вручную. Значения читаются в момент вызова — вызовите снова, чтобы обновить батарею или сеть. IMEI и серийного номера нет: Android блокирует их для всех приложений; используйте getInstallId().
getInstallId мост#
window.WebToApk.getInstallId(): String
WebToApk.getDeviceInfo() -> строка JSON, разрешение НЕ требуется WebToApk.getPhoneIdentity() -> оператор/SIM, нужно разрешение Phone WebToApk.getInstallId() -> стабильный UUID установки
Описывается в своей группе, Информация об устройстве / приложении, а не отдельно.
Пример
Выдаёт случайный идентификатор, который остаётся неизменным для этой установки вашего приложения.
Возвращает: строку (UUID, например "3f2b…-…") сразу. Требуется: ничего.
Используйте его как ключ устройства для вашего сервера или для проверки «одна бесплатная пробная версия на устройство».
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() })
});Отправляйте идентификатор на ваш сервер в заголовке и не держите его в URL.
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();
}Прикрепляйте его к отчётам о сбоях, чтобы можно было найти проблемы конкретной установки.
if (window.WebToApk && WebToApk.setCrashUserId && WebToApk.getInstallId) {
// returns false when Crash reports are off or the id has other characters
const ok = WebToApk.setCrashUserId(WebToApk.getInstallId());
if (!ok) console.log('crash user id not set');
}Примечания: Идентификатор создаётся при первом использовании и хранится в хранилище приложения. После обновлений приложения он не меняется. Он меняется, если пользователь удалит приложение или очистит его данные. Это не аппаратный идентификатор — Android не выдаёт приложениям ни IMEI, ни серийный номер. То же значение есть и в getDeviceInfo() как installId.
getPhoneIdentity мост#
window.WebToApk.getPhoneIdentity(): String
WebToApk.getDeviceInfo() -> строка JSON, разрешение НЕ требуется WebToApk.getPhoneIdentity() -> оператор/SIM, нужно разрешение Phone WebToApk.getInstallId() -> стабильный UUID установки
Описывается в своей группе, Информация об устройстве / приложении, а не отдельно.
Пример
Выдаёт данные о мобильном операторе и SIM-карте: название оператора, страна SIM, код оператора, тип телефона и состояние SIM.
Возвращает: JSON-строку сразу — используйте JSON.parse: { carrierName, simCountryIso, networkCountryIso, simOperator, phoneType, simState, hardwareIdAvailable: false } или { error }. Требуется: при сборке включите Телефон на шаге 4 (Доступ); иначе возвращается { "error": "not_enabled" }.
Покажите оператора связи и выберите код страны.
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"
}Выберите телефонный код по стране SIM-карты для формы регистрации.
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();Отличите планшет только с Wi-Fi от телефона с помощью phoneType.
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
}
}Примечания: phoneType — это gsm, cdma, sip или none (планшеты только с Wi-Fi). simOperator — это код MCC+MNC (например, "40445"). IMEI, MEID и серийного номера нет — hardwareIdAvailable всегда false, потому что Android скрывает их от всех приложений начиная с Android 10; для стабильного идентификатора используйте getInstallId(). Переключатель «Телефон» добавляет в вашу карточку Play чувствительное разрешение; укажите, для чего вы его используете, в форме Data safety.
isTv мост#
window.WebToApk.isTv(): Boolean
True, когда приложение работает на телевизоре (Android TV, Google TV, Fire TV).
Пример
Сообщает странице, работает ли приложение на телевизоре (Android TV, Google TV, Fire TV), чтобы она могла показать макет, удобный для пульта.
Возвращает: true или false (синхронно). Требуется: ничего. Если в мастере включена поддержка Android TV, приложение также получает баннер ТВ в лаунчере ТВ, а на ТВ страница получает чёткую рамку фокуса, и ссылки, кнопки, поля и элементы с onclick или ролью кнопки становятся доступны стрелками пульта; OK нажимает на элемент в фокусе, а «Назад» работает как на телефоне.
var onTv = !!(window.WebToApk && typeof window.WebToApk.isTv === 'function' && window.WebToApk.isTv());
if (onTv) {
document.documentElement.classList.add('tv'); /* bigger text, no hover-only menus */
document.querySelector('#play').focus(); /* start the remote on the main action */
}html.tv body { font-size: 22px; }
html.tv :focus-visible { transform: scale(1.05); }Выберите другой главный экран для ТВ и оставьте вариант для телефона по умолчанию.
function isTv() {
return !!(window.WebToApk && typeof WebToApk.isTv === 'function' && WebToApk.isTv());
}
history.replaceState({}, '', isTv() ? '/tv-home' : '/home');
if (isTv()) document.querySelectorAll('.touch-only').forEach(function (el) { el.hidden = true; });Примечания: Проверяйте в эмуляторе Android TV с его D-pad. Публикация для ТВ в Google Play — отдельный шаг: в Play Console подключите приложение к форм-фактору Android TV (Release → Advanced settings → Form factors) и добавьте скриншот для ТВ.