Criar APK grátis

API da ponte JavaScript

Informações do dispositivo e do app

Que telefone é este, qual compilação, um identificador estável por instalação e as APIs padrão de localização e câmera.

getUserMedia padrão web#

navigator.mediaDevices.getUserMedia — camera and microphone

Exemplo

A API web padrão de câmera e microfone funciona dentro do app. O app mostra o pedido de permissão do próprio Android e entrega o stream à página.

Retorna: uma Promise de um MediaStream, ou uma rejeição (NotAllowedError quando o usuário recusou ou a chave estava desativada na compilação, NotFoundError quando não há esse dispositivo). Requer: Câmera e/ou Microfone na Etapa 4 (Acesso). Quando você compila a partir de um ZIP ou de uma página HTML e seu código chama getUserMedia({ video: … }) ou { audio: … } (ou usa <input capture>, BarcodeDetector, SpeechRecognition), a compilação encontra a chamada e adiciona a permissão correspondente para você; { video: false } não adiciona Câmera. Em uma compilação de site (URL), marque você mesmo.

Mostre a câmera traseira em um elemento de vídeo.

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

Grave uma nota de voz só com o microfone.

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

Tire uma foto a partir do stream da câmera e libere a câmera.

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

Observações: Quando você pede câmera e microfone juntos, o app pede os dois ao usuário de uma vez. Se a chave estava desativada quando você compilou, o pedido é recusado sem nenhum aviso — compile de novo com ela ativada. Sempre pare as tracks quando terminar, ou a luz da câmera/microfone continua acesa. Vídeo com DRM (Widevine) é permitido sem nenhuma chave. Câmera e microfone são sensíveis: declare-os no seu formulário de Segurança dos dados do Play.

IdleDetector padrão web#

new IdleDetector() — Idle Detection

Exemplo

A API padrão Idle Detection: saiba quando o usuário para de usar o telefone (sem entrada por um tempo) ou bloqueia a tela, e quando ele volta — para um status "ausente", pausar uma sessão ao vivo ou bloquear um cofre.

Retorna: IdleDetector.requestPermission() → 'granted' ou 'denied' (precisa de um toque); detector.start({threshold, signal}) → Promise (rejeita NotAllowedError sem a permissão, TypeError abaixo de 60000 ms); depois userState ('active'/'idle'), screenState ('locked'/'unlocked') e um evento change. Requer: nada para ativar e nenhuma permissão do 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();
});

Retome a observação na próxima abertura sem um novo toque, depois que o usuário já disse sim:

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

Deixe o usuário alterar o tempo do bloqueio automático: pare o detector em execução, inicie um novo e informe os erros reais:

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

Observações: no app, "entrada" é a entrada do usuário NESTE app enquanto ele está em primeiro plano; enquanto outro app está em primeiro plano com a tela ligada, o usuário conta como ativo (um app não vê a entrada de outros apps — o Chrome no Android responde do mesmo jeito); com a tela desligada ou bloqueada, o tempo ocioso é contado a partir do último toque. A permissão é lembrada, e navigator.permissions.query({name:'idle-detection'}) a informa. Transportes internos: __idleState, __idlePermission; mudanças de bloqueio/desbloqueio chegam como appmint:idle. Apps criados com IA usam watchIdle().

navigator.geolocation padrão web#

navigator.geolocation

Exemplo

A API web padrão de localização funciona dentro do app. O app mostra o pedido de localização do próprio Android e passa a resposta à página.

Retorna: os callbacks normais do W3C: um GeolocationPosition (coords.latitude, coords.longitude, coords.accuracy, …) ou um GeolocationPositionError (code 1 = permissão negada, 2 = indisponível, 3 = tempo esgotado). Requer: GPS (preciso) e/ou Localização (aproximada) na Etapa 4 (Acesso). Quando você compila a partir de um ZIP ou de uma página HTML e seu código chama navigator.geolocation, getCurrentPosition ou watchPosition, a compilação encontra essa chamada e adiciona a localização para você. Em uma compilação de site (URL), marque você mesmo.

Obtenha a posição uma vez.

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

Acompanhe o usuário enquanto ele se move (um rastreador de corrida, um mapa de entregas). Pare de acompanhar quando a tela fechar.

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

Verifique a permissão primeiro e só peça uma posição quando houver chance de resposta:

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

Observações: Com GPS ativado, o app pede localização precisa e aproximada juntas; no Android 12+ o usuário pode escolher "Aproximada", que ainda funciona (menor precisão). Com apenas Localização ativada, só a localização aproximada é pedida. Se nenhuma estiver ativada, toda chamada falha com código 1 e sem aviso. Depois que o usuário permite uma vez, as chamadas seguintes são respondidas sem aviso. A chave de localização do próprio telefone também precisa estar ativada. Localização é dado pessoal: declare-a no seu formulário de Segurança dos dados do Play; localização em segundo plano não é oferecida.

navigator.getInstalledRelatedApps padrão web#

navigator.getInstalledRelatedApps()

Exemplo

A API padrão de apps relacionados: informa à página quais dos seus próprios apps Android (listados no manifest do app web) estão instalados — para ocultar um banner "Instale nosso outro app" ou abrir um link direto nele.

Retorna: Promise de um array das entradas related_applications do manifest que estão instaladas: { platform: 'play', id, url?, version? }. Requer: um <link rel="manifest"> cujo manifest liste os apps com "platform": "play" e "id": "<package name>", incluído no app para que a compilação possa declará-los (o Android só deixa um app ver os pacotes que ele nomeia).

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

Abra o app complementar quando ele estiver instalado (o url da entrada vem do seu manifest); caso contrário, leve o usuário à página dele na loja:

// 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';
});

Liste quais dos seus apps já estão no telefone em uma tela "Mais de nós":

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

Observações: a compilação lê o manifest do seu pacote e declara cada pacote listado; um app que não está listado ali não pode ser visto e nunca é informado. Apps em modo site mantêm o manifest no site, então nada é declarado e a lista fica vazia. Transporte interno: __relatedAppsInstalled.

navigator.permissions padrão web#

navigator.permissions.query({ name })

Exemplo

A API padrão de Permissões, respondida a partir do estado real de permissões do Android no telefone — assim a página pode mostrar "Permitir câmera" só quando ainda é possível pedir, e "Ative nas Configurações" quando não é.

Retorna: uma Promise de um PermissionStatus {name, state, onchange}; state é 'granted', 'denied' ou 'prompt', e change dispara logo depois que um diálogo de permissão fecha ou quando o usuário volta das Configurações. Requer: nada para ativar.

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

Só peça quando a página ainda puder pedir e, caso contrário, leve o usuário às Configurações:

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

Reaja quando o usuário alterar uma permissão nas Configurações e combine isso com um pedido real:

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

Observações: denied também cobre um recurso com o qual o app foi compilado sem (a chave estava desativada na compilação, então o Android nunca pode concedê-lo) e uma recusa com "Não perguntar novamente". screen-wake-lock, clipboard-read e clipboard-write são 'granted' (o app os fornece sem aviso). Outros nomes (midi, accelerometer, …) são respondidos pela WebView como antes. Evento por trás: appmint:permissions-changed.

TextDetector padrão web#

new TextDetector().detect(image) — text recognition (OCR)

Exemplo

Lê o texto de uma imagem no próprio telefone — o total de um recibo, um número de série, um cartão de visita — com o reconhecedor ML Kit do Google. O formato é o TextDetector da Shape Detection API, o que o Chrome oferece atrás de uma flag.

Retorna: Promise de um array, uma entrada por linha: { rawValue, boundingBox, cornerPoints } nos pixels da imagem. Rejeita NotSupportedError sem os serviços do Google Play e OperationError quando o reconhecimento falha. Requer: nada para ativar e nenhuma permissão (para ler da câmera, tire a foto com <input type="file" accept="image/*" capture> ou um stream da câmera).

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

Leia um número de série a partir da prévia da câmera ao vivo (um <video> em reprodução é passado como está e seu quadro atual é lido):

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.');
});

Contorne cada linha reconhecida sobre a foto, usando boundingBox (pixels da imagem):

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

Oculte o botão de escanear onde não há reconhecimento de texto (sem serviços do Google Play) e extraia dados de contato de um cartão de visita:

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],
  });
});

Observações: Apenas escritas latinas (inglês, espanhol, francês, alemão, indonésio, vietnamita, turco e outros); chinês, devanágari, japonês, coreano e árabe não são reconhecidos. O modelo vem dos serviços do Google Play: a compilação o solicita na instalação quando seu código o usa; caso contrário, a primeira chamada em um telefone novo espera alguns segundos enquanto ele é baixado. Imagens de outro site precisam permitir CORS (um canvas contaminado rejeita SecurityError). Transportes internos: __ocrAvailable, __ocrRecognize (respondido como appmint:ocr). Apps criados com IA usam recognizeText().

__idlePermission ponte#

window.WebToApk.__idlePermission(request: Boolean): String

IdleDetector.requestPermission() ([request] true, chamado sob um gesto do usuário) registra a concessão; false apenas a lê. "granted" | "prompt".

Exemplo

Transporte interno por trás de IdleDetector.requestPermission() e do estado de permissão 'idle-detection'. As páginas usam a API padrão; nunca chame isto diretamente — os métodos __ podem mudar sem aviso.

Retorna: 'granted' ou 'prompt'. Requer: um gesto do usuário para o pedido.

Use a API pública:

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

Detecte o recurso e trate uma recusa como "não permitido":

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

Peça a partir de um toque e depois comece a detectar:

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

Observações: Implementado no shell do app (IdleAndRelatedApps.kt).

__idleState ponte#

window.WebToApk.__idleState(): String

IdleDetector: `{idleMs, locked}` a partir da entrada do próprio app e do keyguard.

Exemplo

Transporte interno por trás de IdleDetector: há quanto tempo o usuário deu a última entrada e se a tela está bloqueada. As páginas usam a API padrão; nunca chame isto diretamente — os métodos __ podem mudar sem aviso.

Retorna: JSON {idleMs, locked}. Mudanças de tela e de app são enviadas como o evento appmint:idle com o mesmo formato. Requer: nada.

Use a API pública:

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

Pare de acompanhar com um AbortController quando a tela fechar:

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

Bloqueie o app quando a tela bloquear:

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

Observações: Implementado no shell do app (IdleAndRelatedApps.kt).

__permissionState ponte#

window.WebToApk.__permissionState(name: String): String

Estado, na API de Permissões, de um nome de permissão do W3C, a partir das concessões reais do Android: "granted" | "denied" | "prompt", ou "" quando o shell não responde esse nome (a consulta da página então vai para a implementação própria da WebView).

Exemplo

Transporte interno por trás de navigator.permissions.query({ name }). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: 'granted', 'denied', 'prompt', ou '' para um nome que o app não responde (a consulta então vai para a própria WebView). Requer: nada para ativar.

Use a API pública:

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

Verifique várias permissões de uma vez:

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]));

Só peça quando o estado não for denied e diga o que fazer quando for:

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

Observações: Responde camera, microphone, geolocation, notifications, screen-wake-lock, clipboard-read, clipboard-write. Um recurso que a compilação deixou desativado (ou cuja permissão não está no manifest) é denied. Os estados ao vivo são relidos em appmint:permissions-changed (disparado após cada diálogo de permissão) e quando a página volta a ficar visível.

__relatedAppsInstalled ponte#

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

navigator.getInstalledRelatedApps(): quais de [idsJson] (nomes de pacote do manifest da página) estão instalados e visíveis para este app.

Exemplo

Transporte interno por trás de navigator.getInstalledRelatedApps(). As páginas usam a API padrão; nunca chame isso diretamente - os métodos __ podem mudar sem aviso.

Retorna: array JSON dos nomes de pacote informados que estão instalados e visíveis para este app. Requer: os pacotes declarados na compilação (do manifest do app web incluído).

Use a API pública:

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

Mostre o botão "Baixar o app complementar" só quando ele não estiver instalado:

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

Os pacotes vêm do manifest do app web que a página referencia:

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

Observações: Implementado no shell do app (IdleAndRelatedApps.kt).

getAppName ponte#

window.WebToApk.getAppName(): String

O nome do app que o criador escolheu no assistente — o que o launcher mostra.

Exemplo

Fornece o nome do app que você escolheu no assistente de compilação — o nome sob o ícone do launcher.

Retorna: uma string imediatamente. Requer: nada.

Use o nome real do app em um título e em um texto de compartilhamento.

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

Coloque o nome do app no título da página e em um cabeçalho:

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

Monte um e-mail de suporte que cita o app e sua versão:

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

Observações: Este é o nome do assistente, não o <title> da página. Não muda durante a execução. Para a versão, use getAppVersion().

getAppVersion ponte#

window.WebToApk.getAppVersion(): String

O versionName do pacote instalado, ex.: "1.4.2" — a string que o criador define ao compilar, não o versionCode. Volta para "1.0" se o pacote não puder ser lido, para que a página possa imprimi-lo sem checar null.

Exemplo

Fornece o nome da versão do app instalado, por exemplo "1.4.2".

Retorna: uma string imediatamente. Requer: nada.

Mostre a versão em uma tela Sobre.

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

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

Peça uma atualização quando a versão instalada for antiga:

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

Adicione a versão e o código da versão de getDeviceInfo() a um relatório de erro:

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

Observações: Este é o NOME da versão que você definiu ao compilar, não o código da versão. Se o pacote não puder ser lido, retorna "1.0", então nunca é vazio. O código da versão e as datas de instalação estão em getDeviceInfo() em app.

getDeviceInfo ponte#

window.WebToApk.getDeviceInfo(): String

WebToApk.getDeviceInfo() -> string JSON, NENHUMA permissão necessária WebToApk.getPhoneIdentity() -> operadora/SIM, precisa da permissão Telefone WebToApk.getInstallId() -> UUID estável por instalação

Descrito pelo seu grupo, Informações do dispositivo / app, e não separadamente.

Exemplo

Fornece dados sobre o telefone, o Android, a tela, seu app e o estado atual (bateria, memória, rede) em uma única chamada.

Retorna: uma string JSON imediatamente — use JSON.parse. Chaves de nível superior: android, hardware, screen, app, runtime, installId. Requer: nada — nenhuma permissão.

Mostre as informações do telefone.

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

Todas as chaves:

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

Avise em um telefone com pouco armazenamento ou pouca memória antes de um download grande.

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

Observações: socManufacturer / socModel existem apenas no Android 12+. networkType é wifi, cellular, ethernet, other, none ou unknown. installer é com.android.vending para instalações do Play e vazio para instalações manuais. Os valores são lidos no momento da chamada — chame de novo para atualizar bateria ou rede. Não há IMEI nem número de série: o Android os bloqueia para todos os apps; use getInstallId().

getInstallId ponte#

window.WebToApk.getInstallId(): String

WebToApk.getDeviceInfo() -> string JSON, NENHUMA permissão necessária WebToApk.getPhoneIdentity() -> operadora/SIM, precisa da permissão Telefone WebToApk.getInstallId() -> UUID estável por instalação

Descrito pelo seu grupo, Informações do dispositivo / app, e não separadamente.

Exemplo

Fornece um ID aleatório que permanece o mesmo para esta instalação do seu app.

Retorna: uma string (um UUID como "3f2b…-…") imediatamente. Requer: nada.

Use-o como chave do dispositivo para seu servidor ou para uma verificação de "um teste grátis por dispositivo".

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

Envie o id ao seu servidor em um cabeçalho e mantenha-o fora da 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();
}

Anexe-o a relatórios de falha para que os problemas de uma instalação possam ser encontrados.

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

Observações: O ID é criado no primeiro uso e guardado no armazenamento do app. Ele permanece o mesmo após atualizações do app. Muda quando o usuário desinstala o app ou limpa seus dados. Não é um ID de hardware — o Android não fornece IMEI nem número de série aos apps. O mesmo valor também está em getDeviceInfo() como installId.

getPhoneIdentity ponte#

window.WebToApk.getPhoneIdentity(): String

WebToApk.getDeviceInfo() -> string JSON, NENHUMA permissão necessária WebToApk.getPhoneIdentity() -> operadora/SIM, precisa da permissão Telefone WebToApk.getInstallId() -> UUID estável por instalação

Descrito pelo seu grupo, Informações do dispositivo / app, e não separadamente.

Exemplo

Fornece os dados da operadora móvel e do SIM: nome da operadora, país do SIM, código da operadora, tipo de telefone e estado do SIM.

Retorna: uma string JSON imediatamente — use JSON.parse: { carrierName, simCountryIso, networkCountryIso, simOperator, phoneType, simState, hardwareIdAvailable: false }, ou { error }. Requer: ative Telefone na Etapa 4 (Acesso) ao compilar; caso contrário retorna { "error": "not_enabled" }.

Mostre a operadora e escolha o código do país.

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

Escolha um código de discagem a partir do país do SIM para um formulário de cadastro.

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

Distinga um tablet só com Wi-Fi de um telefone com 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
  }
}

Observações: phoneType é gsm, cdma, sip ou none (tablets só com Wi-Fi). simOperator é o código MCC+MNC (ex.: "40445"). Não há IMEI, MEID nem número de série — hardwareIdAvailable é sempre false, porque o Android os oculta de todos os apps desde o Android 10; use getInstallId() para um id estável. A chave Telefone adiciona uma permissão sensível à sua página no Play; declare para que você a usa no formulário de Segurança dos dados.

isTv ponte#

window.WebToApk.isTv(): Boolean

Verdadeiro quando o app roda em uma televisão (Android TV, Google TV, Fire TV).

Exemplo

Informa à página se o app está rodando em uma televisão (Android TV, Google TV, Fire TV), para que ela possa mostrar um layout adequado ao controle remoto.

Retorna: true ou false (de forma síncrona). Requer: nada. Com Suporte a Android TV ativado no assistente, o app também recebe um banner de TV no launcher da TV e, em uma TV, a página ganha um anel de foco nítido, e links, botões, campos e elementos ligados com onclick ou com papel de botão ficam acessíveis com as setas do controle; OK clica no elemento em foco e Voltar funciona como no telefone.

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

Escolha uma tela inicial diferente para TV e mantenha o layout de telefone como padrão.

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

Observações: Teste com o emulador de Android TV e seu D-pad. Publicar para TVs no Google Play é uma etapa separada: no Play Console, inclua o app no formato Android TV (Versão → Configurações avançadas → Formatos) e adicione uma captura de tela de TV.

Gerado a partir do runtime do app e de seus arquivos de exemplo a cada build da documentação. Leia em Markdown · Todas as famílias.

Conferido com a ponte atual em 22/10/2026.