Criar APK grátis

API da ponte JavaScript

Execução em segundo plano

Um serviço em primeiro plano que a página inicia e para, localização que continua chegando com o app em segundo plano, geofences que disparam com o app fechado e o seu próprio código executado periodicamente após uma reinicialização. Cada um precisa da sua opção no build e de uma declaração no Play Console.

AppMint.background helper#

AppMint.background.register(name, options) / unregister / list / runNow / done / isBackgroundRun / currentRun / onRun

Exemplo

Executa periodicamente o código da SUA PRÓPRIA página com o app fechado - atualizar dados, verificar um preço, publicar uma notificação. Cada execução carrega sua página de forma invisível (mesma origem, mesmo localStorage/IndexedDB, sem tela, sem anúncios) com ?appmint_background=1&appmint_task=<name>&appmint_run=<runId>, dispara appmint:background { name, runId } assim que carregou e espera por done(runId, { ok }). Uma execução é interrompida após cerca de 9 minutos.

Retorna: todo método retorna uma Promise. register → { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name, interval_below_15_minutes, invalid_interval, schedule_failed). unregister → true / false. list → [{ name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult }] (lastResult: ok, retry, failed, timeout, unsupported_mode, page_load_failed, …). runNow → { ok } (uma execução assim que as condições permitirem - para testes). done → true quando a execução foi encerrada. isBackgroundRun() e currentRun() são síncronos. Requer: marque Executar código da página com o app fechado (etapa Acesso → Segundo plano). Apps de site e offline/ZIP; apps de documento e mídia não têm código de página para executar.

Uma página, dois papéis:

function startApp() {
  const bg = window.AppMint && AppMint.background;
  if (bg && bg.isBackgroundRun()) return;          // a background run: no UI work
  renderUi();
  if (bg) bg.register('prices', { intervalMinutes: 60, requiresNetwork: true }).then(function (r) {
    if (!r.ok) console.warn('background task not scheduled:', r.reason);
  });
}

// Runs only in the background copy of the page.
window.addEventListener('appmint:background', async function (e) {
  const run = e.detail;                              // { name: 'prices', runId: '…' }
  try {
    const price = await fetch('https://api.example.com/price').then(function (r) { return r.json(); });
    const last = Number(localStorage.getItem('lastPrice') || 0);
    if (price.value < last) {
      WebToApk.notify(JSON.stringify({ title: 'Price dropped', body: 'Now ' + price.value, tag: 'price' }));
    }
    localStorage.setItem('lastPrice', String(price.value));
    AppMint.background.done(run.runId, { ok: true });
  } catch (err) {
    AppMint.background.done(run.runId, { ok: false, retry: true });   // retried with backoff
  }
});
startApp();

Páginas montadas tarde (React, uma rota lazy) usam onRun, que também dispara se a execução já começou, ou o hook da página:

if (window.AppMint && AppMint.background) {
  AppMint.background.onRun(function (run) { syncInbox().then(function () { AppMint.background.done(run.runId, { ok: true }); }); });
}
window.onAppMintBackground = function (run) { console.log('background run', run.name, run.runId, AppMint.background.currentRun()); };

Gerenciar:

AppMint.background.list().then(function (tasks) { console.table(tasks); });
AppMint.background.runNow('prices');
AppMint.background.unregister('prices');

Observações: o Android decide o momento exato; 15 minutos é o menor intervalo e a economia de bateria pode atrasar execuções. Com Iniciar após reiniciar o telefone cada tarefa também roda uma vez após uma reinicialização. O WebToApk de uma execução tem apenas notify, showNotification, o buffer de localização, geofences, workEnqueueOnline, isBackgroundServiceRunning e estes métodos de tarefa; qualquer outra coisa está ausente (detecte o recurso).

addGeofence ponte#

window.WebToApk.addGeofence(json: String): String

Adiciona uma geofence {id, lat, lng, radiusM, events, dwellMs?, notify?} → JSON {ok, status:'adding'} ou {ok:false, reason}; o resultado é appmint:geofence-added.

Exemplo

Um círculo que dispara enter, exit e/ou dwell com o app fechado, opcionalmente publicando uma notificação. As geofences sobrevivem a uma reinicialização do telefone (são adicionadas de volta automaticamente).

Retorna: addGeofence → uma string JSON { ok: true, status: 'adding', id } ou { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. A resposta do Play Services chega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Uma transição chega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true quando aconteceu com o app fechado e é entregue na próxima inicialização. removeGeofence → true se existia; getGeofences → um array JSON do que você adicionou. Requer: marque Geofences no build e "Permitir o tempo todo" (requestBackgroundLocation()).

function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');

Detecte o recurso e reporte cada recusa:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

Liste o que está registrado e remova um:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

backgroundDone ponte#

window.WebToApk.backgroundDone(runId: String, resultJson: String): Boolean

AppMint.background.done(runId, result): encerra uma execução em segundo plano; sempre false no próprio app.

Exemplo

Documentado junto com WebToApk.backgroundRegister - o exemplo de lá mostra este também.

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

backgroundList ponte#

window.WebToApk.backgroundList(): String

AppMint.background.list(): array JSON {name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult}.

Exemplo

Documentado junto com WebToApk.backgroundRegister - o exemplo de lá mostra este também.

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

backgroundRegister ponte#

window.WebToApk.backgroundRegister(name: String, optionsJson: String): String

AppMint.background.register(name, {intervalMinutes >= 15, requiresNetwork, requiresCharging}) → JSON {ok, name, intervalMinutes} ou {ok:false, reason}.

Exemplo

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

backgroundRunNow ponte#

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

AppMint.background.runNow(name): uma execução em segundo plano assim que as condições permitirem.

Exemplo

Documentado junto com WebToApk.backgroundRegister - o exemplo de lá mostra este também.

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

backgroundUnregister ponte#

window.WebToApk.backgroundUnregister(name: String): Boolean

AppMint.background.unregister(name); false quando nenhuma tarefa tinha esse nome.

Exemplo

Documentado junto com WebToApk.backgroundRegister - o exemplo de lá mostra este também.

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

clearBufferedLocations ponte#

window.WebToApk.clearBufferedLocations(): Boolean

Esvazia o buffer que getBufferedLocations lê (chame depois de salvar os pontos).

Exemplo

Documentado junto com WebToApk.getBufferedLocations - o exemplo de lá mostra este também.

As posições gravadas enquanto sua página estava ausente (app em segundo plano ou fechado), da mais antiga para a mais recente, até 10.000.

Retorna: getBufferedLocations() → uma string JSON com um array de { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' quando a localização em segundo plano está desativada. clearBufferedLocations() → true quando esvaziado. Requer: Localização em segundo plano no build.

function syncTrack() {
  if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (!points.length) return;
  points.forEach(function (p) { drawPoint(p.lat, p.lng); });
  saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });

Inicie o rastreamento e depois trate uma recusa. A chamada de início responde com uma string JSON.

function startTracking() {
  if (!(window.WebToApk && WebToApk.startLocationUpdates)) return;
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({
    intervalMs: 15000, accuracy: 'balanced', background: true
  })));
  if (!r.ok) showMessage('Tracking not started: ' + r.reason);   // not_enabled, location_permission_required, ...
}

As posições ao vivo enquanto a página está aberta chegam como appmint:location; o buffer guarda o resto.

window.addEventListener('appmint:location', function (e) {
  drawPoint(e.detail.lat, e.detail.lng);
});
document.getElementById('stop').addEventListener('click', function () {
  if (window.WebToApk && WebToApk.stopLocationUpdates) WebToApk.stopLocationUpdates();
  if (WebToApk.getBufferedLocations) {
    const left = JSON.parse(WebToApk.getBufferedLocations());
    console.log(left.length + ' fixes still buffered');
  }
});

clearGeofenceEvents ponte#

window.WebToApk.clearGeofenceEvents(): Boolean

Esvazia o histórico de transições que getGeofenceEvents lê.

Exemplo

Documentado junto com WebToApk.getGeofenceEvents - o exemplo de lá mostra este também.

As últimas 100 transições de geofence, da mais antiga para a mais recente - incluindo as que aconteceram com o app fechado. Leia ao iniciar se sua página for montada tarde e puder perder os eventos appmint:geofence enfileirados.

Retorna: getGeofenceEvents() → uma string JSON com um array de { id, event, time, lat, lng }; clearGeofenceEvents() → true. Requer: Geofences no build.

if (window.WebToApk && WebToApk.getGeofenceEvents) {
  JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
    logVisit(ev.id, ev.event, ev.time);
  });
  WebToApk.clearGeofenceEvents();
}

Adicione uma geofence e depois observe as transições enquanto a página está aberta.

if (window.WebToApk && WebToApk.addGeofence) {
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'shop-1', lat: 12.9716, lng: 77.5946, radiusM: 150, events: ['enter', 'exit']
  })));
  if (!r.ok) showMessage('Geofence refused: ' + r.reason);   // not_enabled, invalid_radius, ...
}
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

Liste o que está registrado e remova um quando o usuário sair de um recurso.

function dropGeofence(id) {
  if (!(window.WebToApk && WebToApk.removeGeofence)) return false;
  const known = JSON.parse(WebToApk.getGeofences()).length;
  const removed = WebToApk.removeGeofence(id);
  console.log(removed ? 'removed' : 'not found', 'of', known, 'registered');
  return removed;
}

getBufferedLocations ponte#

window.WebToApk.getBufferedLocations(): String

Array JSON das posições gravadas enquanto a página estava ausente, da mais antiga para a mais recente (até 10.000).

Exemplo

As posições gravadas enquanto sua página estava ausente (app em segundo plano ou fechado), da mais antiga para a mais recente, até 10.000.

Retorna: getBufferedLocations() → uma string JSON com um array de { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' quando a localização em segundo plano está desativada. clearBufferedLocations() → true quando esvaziado. Requer: Localização em segundo plano no build.

function syncTrack() {
  if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (!points.length) return;
  points.forEach(function (p) { drawPoint(p.lat, p.lng); });
  saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });

Inicie o rastreamento e depois trate uma recusa. A chamada de início responde com uma string JSON.

function startTracking() {
  if (!(window.WebToApk && WebToApk.startLocationUpdates)) return;
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({
    intervalMs: 15000, accuracy: 'balanced', background: true
  })));
  if (!r.ok) showMessage('Tracking not started: ' + r.reason);   // not_enabled, location_permission_required, ...
}

As posições ao vivo enquanto a página está aberta chegam como appmint:location; o buffer guarda o resto.

window.addEventListener('appmint:location', function (e) {
  drawPoint(e.detail.lat, e.detail.lng);
});
document.getElementById('stop').addEventListener('click', function () {
  if (window.WebToApk && WebToApk.stopLocationUpdates) WebToApk.stopLocationUpdates();
  if (WebToApk.getBufferedLocations) {
    const left = JSON.parse(WebToApk.getBufferedLocations());
    console.log(left.length + ' fixes still buffered');
  }
});

getGeofenceEvents ponte#

window.WebToApk.getGeofenceEvents(): String

Array JSON das últimas 100 transições de geofence {id, event, time, lat, lng}, da mais antiga para a mais recente.

Exemplo

As últimas 100 transições de geofence, da mais antiga para a mais recente - incluindo as que aconteceram com o app fechado. Leia ao iniciar se sua página for montada tarde e puder perder os eventos appmint:geofence enfileirados.

Retorna: getGeofenceEvents() → uma string JSON com um array de { id, event, time, lat, lng }; clearGeofenceEvents() → true. Requer: Geofences no build.

if (window.WebToApk && WebToApk.getGeofenceEvents) {
  JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
    logVisit(ev.id, ev.event, ev.time);
  });
  WebToApk.clearGeofenceEvents();
}

Adicione uma geofence e depois observe as transições enquanto a página está aberta.

if (window.WebToApk && WebToApk.addGeofence) {
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'shop-1', lat: 12.9716, lng: 77.5946, radiusM: 150, events: ['enter', 'exit']
  })));
  if (!r.ok) showMessage('Geofence refused: ' + r.reason);   // not_enabled, invalid_radius, ...
}
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

Liste o que está registrado e remova um quando o usuário sair de um recurso.

function dropGeofence(id) {
  if (!(window.WebToApk && WebToApk.removeGeofence)) return false;
  const known = JSON.parse(WebToApk.getGeofences()).length;
  const removed = WebToApk.removeGeofence(id);
  console.log(removed ? 'removed' : 'not found', 'of', known, 'registered');
  return removed;
}

getGeofences ponte#

window.WebToApk.getGeofences(): String

Array JSON das geofences que este app adicionou (elas sobrevivem a reinicializações).

Exemplo

Documentado junto com WebToApk.addGeofence - o exemplo de lá mostra este também.

Um círculo que dispara enter, exit e/ou dwell com o app fechado, opcionalmente publicando uma notificação. As geofences sobrevivem a uma reinicialização do telefone (são adicionadas de volta automaticamente).

Retorna: addGeofence → uma string JSON { ok: true, status: 'adding', id } ou { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. A resposta do Play Services chega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Uma transição chega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true quando aconteceu com o app fechado e é entregue na próxima inicialização. removeGeofence → true se existia; getGeofences → um array JSON do que você adicionou. Requer: marque Geofences no build e "Permitir o tempo todo" (requestBackgroundLocation()).

function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');

Detecte o recurso e reporte cada recusa:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

Liste o que está registrado e remova um:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

getLocationPermissionState ponte#

window.WebToApk.getLocationPermissionState(): String

JSON {foreground:'granted'|'denied', background:'granted'|'denied', precise: boolean}.

Exemplo

Documentado junto com WebToApk.requestBackgroundLocation - o exemplo de lá mostra este também.

Pede a localização e depois "Permitir o tempo todo" (o Android 10+ pede separadamente; no Android 11+ o segundo passo abre a página de configurações de localização do app). As geofences precisam disso; reiniciar o rastreamento de localização após uma reinicialização do telefone também.

Retorna: nada diretamente; a resposta chega como appmint:location-permission (e window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → o mesmo objeto como string JSON, sem perguntar. Requer: Localização em segundo plano ou Geofences no build.

function askAlways() {
  const b = window.WebToApk;
  if (!(b && b.requestBackgroundLocation)) return;
  const now = JSON.parse(b.getLocationPermissionState());
  if (now.background === 'granted') { enableGeofences(); return; }
  // Explain first: Play requires an in-app disclosure before this request.
  if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
  b.requestBackgroundLocation();
}

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.background === 'granted') enableGeofences();
  else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});

Verifique primeiro o estado e explique o que o usuário deve tocar:

const b = window.WebToApk;
if (b && b.getLocationPermissionState) {
  const s = JSON.parse(b.getLocationPermissionState());
  if (s.foreground !== 'granted') showMessage('Location is off for this app.');
  else if (s.background !== 'granted') showMessage('Tap "Allow all the time" to enable arrival reminders.');
  else if (!s.precise) showMessage('Precise location is off; arrival alerts may be late.');
}

Peça a permissão e depois adicione uma geofence quando ela for concedida:

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.error === 'not_enabled') return showMessage('This build has no background location.');
  if (e.detail.background !== 'granted') return;
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'home', lat: 51.5033, lng: -0.1196, radiusM: 120, events: ['enter']
  })));
  if (!r.ok) showMessage('Could not add the place: ' + r.reason);
}, { once: true });
WebToApk.requestBackgroundLocation();

isBackgroundRun ponte#

window.WebToApk.isBackgroundRun(): Boolean

False aqui: a própria página do app nunca é uma execução em segundo plano (uma execução headless responde true).

Exemplo

Documentado junto com WebToApk.backgroundRegister - o exemplo de lá mostra este também.

Os métodos brutos por trás de AppMint.background - use AppMint.background.register(...) (ele retorna promises e trata o evento de execução). Listados aqui para que todo método da ponte tenha um exemplo.

Retorna: backgroundRegister → JSON { ok: true, name, intervalMinutes } ou { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, até 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true se existia. backgroundList → array JSON. backgroundRunNow → JSON { ok, name } ou { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true em uma execução em segundo plano, sempre false no app. isBackgroundRun() → false no app, true em uma execução em segundo plano. Requer: marque Executar código da página com o app fechado.

const b = window.WebToApk;
if (b && b.backgroundRegister) {
  const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
  if (!r.ok) console.warn('not scheduled:', r.reason);
  console.log(JSON.parse(b.backgroundList()));
  console.log(b.isBackgroundRun());               // false here: this is the app on screen
  // b.backgroundRunNow('refresh');  b.backgroundUnregister('refresh');
  // In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}

Detecte o recurso e diferencie a execução do app da execução em segundo plano:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Teste uma tarefa e limpe:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

isBackgroundServiceRunning ponte#

window.WebToApk.isBackgroundServiceRunning(): Boolean

Se o serviço em primeiro plano do app está em execução (seja de manter em execução ou de localização em segundo plano).

Exemplo

Documentado junto com WebToApk.startBackgroundService - o exemplo de lá mostra este também.

Mantém seu app em execução enquanto o usuário está ausente: um serviço em primeiro plano real do Android com uma notificação. Enquanto ele roda a página não é pausada, então seu JavaScript, <audio> e a música de fundo do app continuam (o Android ainda desacelera os timers de uma página que ninguém consegue ver).

Retorna: startBackgroundService → uma string JSON { ok: true, status: 'starting', type, notificationShown } ou { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } ou { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. As mudanças de estado chegam como appmint:background-service (e window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requer: marque Manter em execução em segundo plano (etapa Acesso → Segundo plano) e escolha o tipo de serviço. No Android 13+ a notificação só fica visível depois que o usuário permitiu notificações (notificationShown: false caso contrário - o serviço continua em execução).

function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});

Trate as respostas que significam que o build ou o telefone não consegue fazer isso:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine com um loop de envio: continue em execução apenas enquanto a fila tiver trabalho.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

Observações: o tipo deve ser um que o build declarou (o escolhido no assistente, mais location quando a localização em segundo plano está ativada). Deslizar o app para fora para um serviço de manter em execução, a menos que você passe keepAfterSwipe: true (a própria página já não existe então). O Play Console pede uma declaração de serviço em primeiro plano com um vídeo curto para cada tipo declarado.

isIgnoringBatteryOptimizations ponte#

window.WebToApk.isIgnoringBatteryOptimizations(): Boolean

Se o usuário isentou este app da otimização de bateria (Doze app standby).

Exemplo

Verifica e pede a isenção da otimização de bateria, para que o Doze não adie seu trabalho em segundo plano.

Retorna: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (o diálogo Permitir/Negar do sistema - apenas quando o build marcou Pedir para ignorar a otimização de bateria diretamente), 'settings_opened' (a lista de otimização de bateria; sem permissão, sem declaração no Play) ou 'unavailable'. Requer: nada para a lista de configurações. O diálogo direto declara REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, que o Play só aceita quando é a função principal do app.

document.getElementById('reliable').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.isIgnoringBatteryOptimizations)) return;
  if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});

Diga o que aconteceu para cada resposta.

function askBatteryExemption() {
  const b = window.WebToApk;
  if (!(b && b.requestIgnoreBatteryOptimizations)) { showMessage('Only available in the app'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  const text = {
    already_ignoring: 'Already unrestricted',
    dialog_opened: 'Please tap Allow',
    settings_opened: 'Choose "Not optimised" for this app',
    unavailable: 'This phone has no such setting'
  };
  showMessage(text[how] || how);
}

Combine com o rastreamento em segundo plano, já que o Doze o pausaria.

if (window.WebToApk && WebToApk.startLocationUpdates) {
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({ intervalMs: 30000 })));
  if (r.ok && !WebToApk.isIgnoringBatteryOptimizations()) {
    showMessage('For steady tracking, turn off battery optimisation for this app.');
  }
}

removeGeofence ponte#

window.WebToApk.removeGeofence(id: String): Boolean

Remove a geofence com este id; false quando não havia nenhuma.

Exemplo

Documentado junto com WebToApk.addGeofence - o exemplo de lá mostra este também.

Um círculo que dispara enter, exit e/ou dwell com o app fechado, opcionalmente publicando uma notificação. As geofences sobrevivem a uma reinicialização do telefone (são adicionadas de volta automaticamente).

Retorna: addGeofence → uma string JSON { ok: true, status: 'adding', id } ou { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. A resposta do Play Services chega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Uma transição chega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true quando aconteceu com o app fechado e é entregue na próxima inicialização. removeGeofence → true se existia; getGeofences → um array JSON do que você adicionou. Requer: marque Geofences no build e "Permitir o tempo todo" (requestBackgroundLocation()).

function watchOffice() {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) return;
  const r = JSON.parse(b.addGeofence(JSON.stringify({
    id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
    events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
    notify: {
      enter: { title: 'At the office', body: 'Tap to check in' },
      exit: { title: 'Left the office', body: 'Tap to check out' }
    }
  })));
  if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}

window.addEventListener('appmint:geofence-added', function (e) {
  if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');

Detecte o recurso e reporte cada recusa:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

Liste o que está registrado e remova um:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

requestBackgroundLocation ponte#

window.WebToApk.requestBackgroundLocation()

Pede a localização e depois "Permitir o tempo todo"; o resultado chega como appmint:location-permission.

Exemplo

Pede a localização e depois "Permitir o tempo todo" (o Android 10+ pede separadamente; no Android 11+ o segundo passo abre a página de configurações de localização do app). As geofences precisam disso; reiniciar o rastreamento de localização após uma reinicialização do telefone também.

Retorna: nada diretamente; a resposta chega como appmint:location-permission (e window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → o mesmo objeto como string JSON, sem perguntar. Requer: Localização em segundo plano ou Geofences no build.

function askAlways() {
  const b = window.WebToApk;
  if (!(b && b.requestBackgroundLocation)) return;
  const now = JSON.parse(b.getLocationPermissionState());
  if (now.background === 'granted') { enableGeofences(); return; }
  // Explain first: Play requires an in-app disclosure before this request.
  if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
  b.requestBackgroundLocation();
}

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.background === 'granted') enableGeofences();
  else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});

Verifique primeiro o estado e explique o que o usuário deve tocar:

const b = window.WebToApk;
if (b && b.getLocationPermissionState) {
  const s = JSON.parse(b.getLocationPermissionState());
  if (s.foreground !== 'granted') showMessage('Location is off for this app.');
  else if (s.background !== 'granted') showMessage('Tap "Allow all the time" to enable arrival reminders.');
  else if (!s.precise) showMessage('Precise location is off; arrival alerts may be late.');
}

Peça a permissão e depois adicione uma geofence quando ela for concedida:

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.error === 'not_enabled') return showMessage('This build has no background location.');
  if (e.detail.background !== 'granted') return;
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'home', lat: 51.5033, lng: -0.1196, radiusM: 120, events: ['enter']
  })));
  if (!r.ok) showMessage('Could not add the place: ' + r.reason);
}, { once: true });
WebToApk.requestBackgroundLocation();

requestIgnoreBatteryOptimizations ponte#

window.WebToApk.requestIgnoreBatteryOptimizations(): String

Pede a isenção de bateria: 'already_ignoring' | 'dialog_opened' | 'settings_opened' | 'unavailable'.

Exemplo

Documentado junto com WebToApk.isIgnoringBatteryOptimizations - o exemplo de lá mostra este também.

Verifica e pede a isenção da otimização de bateria, para que o Doze não adie seu trabalho em segundo plano.

Retorna: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (o diálogo Permitir/Negar do sistema - apenas quando o build marcou Pedir para ignorar a otimização de bateria diretamente), 'settings_opened' (a lista de otimização de bateria; sem permissão, sem declaração no Play) ou 'unavailable'. Requer: nada para a lista de configurações. O diálogo direto declara REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, que o Play só aceita quando é a função principal do app.

document.getElementById('reliable').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.isIgnoringBatteryOptimizations)) return;
  if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});

Diga o que aconteceu para cada resposta.

function askBatteryExemption() {
  const b = window.WebToApk;
  if (!(b && b.requestIgnoreBatteryOptimizations)) { showMessage('Only available in the app'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  const text = {
    already_ignoring: 'Already unrestricted',
    dialog_opened: 'Please tap Allow',
    settings_opened: 'Choose "Not optimised" for this app',
    unavailable: 'This phone has no such setting'
  };
  showMessage(text[how] || how);
}

Combine com o rastreamento em segundo plano, já que o Doze o pausaria.

if (window.WebToApk && WebToApk.startLocationUpdates) {
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({ intervalMs: 30000 })));
  if (r.ok && !WebToApk.isIgnoringBatteryOptimizations()) {
    showMessage('For steady tracking, turn off battery optimisation for this app.');
  }
}

startBackgroundService ponte#

window.WebToApk.startBackgroundService(optionsJson: String): String

Inicia o serviço em primeiro plano do app: {title, text, type?, progress?, stopLabel?, keepAfterSwipe?} → JSON {ok, status, type, notificationShown} ou {ok:false, reason}.

Exemplo

Mantém seu app em execução enquanto o usuário está ausente: um serviço em primeiro plano real do Android com uma notificação. Enquanto ele roda a página não é pausada, então seu JavaScript, <audio> e a música de fundo do app continuam (o Android ainda desacelera os timers de uma página que ninguém consegue ver).

Retorna: startBackgroundService → uma string JSON { ok: true, status: 'starting', type, notificationShown } ou { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } ou { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. As mudanças de estado chegam como appmint:background-service (e window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requer: marque Manter em execução em segundo plano (etapa Acesso → Segundo plano) e escolha o tipo de serviço. No Android 13+ a notificação só fica visível depois que o usuário permitiu notificações (notificationShown: false caso contrário - o serviço continua em execução).

function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});

Trate as respostas que significam que o build ou o telefone não consegue fazer isso:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine com um loop de envio: continue em execução apenas enquanto a fila tiver trabalho.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

Observações: o tipo deve ser um que o build declarou (o escolhido no assistente, mais location quando a localização em segundo plano está ativada). Deslizar o app para fora para um serviço de manter em execução, a menos que você passe keepAfterSwipe: true (a própria página já não existe então). O Play Console pede uma declaração de serviço em primeiro plano com um vídeo curto para cada tipo declarado.

startLocationUpdates ponte#

window.WebToApk.startLocationUpdates(optionsJson: String): String

Atualizações de localização {intervalMs, minDistanceM, accuracy, background, uploadUrl?, headers?} → JSON {ok, background, backgroundPermission, precise} ou {ok:false, reason}; as posições chegam como appmint:location.

Exemplo

Atualizações de localização que continuam chegando enquanto o app está em segundo plano (um rastreador de corrida, um app de entregas). Com background: true (o padrão) elas rodam dentro do serviço em primeiro plano do app. Cada posição vai para a página como appmint:location enquanto a página está presente, para um buffer (getBufferedLocations()) enquanto não está e - quando você informa um uploadUrl - para o seu servidor como um POST JSON a partir do código nativo, então o rastreamento funciona com a página ausente.

Retorna: uma string JSON { ok: true, background, backgroundPermission, precise } ou { ok: false, reason } - not_enabled, invalid_options, invalid_interval (1 s … 24 h), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true se algo estava em execução. Posições: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, ou { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Requer: marque Localização em segundo plano (etapa Acesso → Segundo plano) e antes a permissão de localização do usuário.

function startTracking(token) {
  const b = window.WebToApk;
  if (!(b && b.startLocationUpdates)) return;
  navigator.geolocation.getCurrentPosition(function () {       // asks for location first
    const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
      intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
      uploadUrl: 'https://api.example.com/track',
      headers: { Authorization: 'Bearer ' + token }
    })));
    if (!r.ok) showMessage('Tracking not started: ' + r.reason);
  });
}

window.addEventListener('appmint:location', function (e) {
  if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
  else showMessage('Location stopped: ' + e.detail.reason);
});

function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }

Reporte uma opção inválida ou uma permissão ausente com o reason:

function tryTrack() {
  const b = window.WebToApk;
  if (!(b && typeof b.startLocationUpdates === 'function')) {
    showMessage('Background tracking needs the installed app.');
    return;
  }
  const r = JSON.parse(b.startLocationUpdates(JSON.stringify({ intervalMs: 500 })));
  if (!r.ok && r.reason === 'invalid_interval') showMessage('Interval must be 1 second or more.');
  if (!r.ok && r.reason === 'location_permission_required') showMessage('Allow location first.');
}

Um par natural com o buffer: pare no fim de um turno e envie o que foi gravado enquanto a página estava ausente.

async function endShift() {
  if (!window.WebToApk) return;
  WebToApk.stopLocationUpdates();
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (points.length) {
    await fetch('/api/shift/points', { method: 'POST', body: JSON.stringify(points) });
    WebToApk.clearBufferedLocations();
  }
}

Observações: um serviço de localização iniciado pelo app continua recebendo posições em segundo plano sem "Permitir o tempo todo"; reiniciá-lo após uma reinicialização do telefone precisa disso (requestBackgroundLocation()). Um POST que falha é entregue ao WorkManager e enviado quando a rede voltar. Telefones sem Google Play Services respondem play_services_unavailable. O Play Console exige uma declaração de localização em segundo plano.

stopBackgroundService ponte#

window.WebToApk.stopBackgroundService(): Boolean

Para o serviço iniciado por startBackgroundService (a localização em segundo plano, se estiver em execução, continua).

Exemplo

Documentado junto com WebToApk.startBackgroundService - o exemplo de lá mostra este também.

Mantém seu app em execução enquanto o usuário está ausente: um serviço em primeiro plano real do Android com uma notificação. Enquanto ele roda a página não é pausada, então seu JavaScript, <audio> e a música de fundo do app continuam (o Android ainda desacelera os timers de uma página que ninguém consegue ver).

Retorna: startBackgroundService → uma string JSON { ok: true, status: 'starting', type, notificationShown } ou { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } ou { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. As mudanças de estado chegam como appmint:background-service (e window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requer: marque Manter em execução em segundo plano (etapa Acesso → Segundo plano) e escolha o tipo de serviço. No Android 13+ a notificação só fica visível depois que o usuário permitiu notificações (notificationShown: false caso contrário - o serviço continua em execução).

function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});

Trate as respostas que significam que o build ou o telefone não consegue fazer isso:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine com um loop de envio: continue em execução apenas enquanto a fila tiver trabalho.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

Observações: o tipo deve ser um que o build declarou (o escolhido no assistente, mais location quando a localização em segundo plano está ativada). Deslizar o app para fora para um serviço de manter em execução, a menos que você passe keepAfterSwipe: true (a própria página já não existe então). O Play Console pede uma declaração de serviço em primeiro plano com um vídeo curto para cada tipo declarado.

stopLocationUpdates ponte#

window.WebToApk.stopLocationUpdates(): Boolean

Para as atualizações de localização iniciadas por startLocationUpdates; false quando nenhuma estava em execução.

Exemplo

Documentado junto com WebToApk.startLocationUpdates - o exemplo de lá mostra este também.

Atualizações de localização que continuam chegando enquanto o app está em segundo plano (um rastreador de corrida, um app de entregas). Com background: true (o padrão) elas rodam dentro do serviço em primeiro plano do app. Cada posição vai para a página como appmint:location enquanto a página está presente, para um buffer (getBufferedLocations()) enquanto não está e - quando você informa um uploadUrl - para o seu servidor como um POST JSON a partir do código nativo, então o rastreamento funciona com a página ausente.

Retorna: uma string JSON { ok: true, background, backgroundPermission, precise } ou { ok: false, reason } - not_enabled, invalid_options, invalid_interval (1 s … 24 h), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true se algo estava em execução. Posições: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, ou { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Requer: marque Localização em segundo plano (etapa Acesso → Segundo plano) e antes a permissão de localização do usuário.

function startTracking(token) {
  const b = window.WebToApk;
  if (!(b && b.startLocationUpdates)) return;
  navigator.geolocation.getCurrentPosition(function () {       // asks for location first
    const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
      intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
      uploadUrl: 'https://api.example.com/track',
      headers: { Authorization: 'Bearer ' + token }
    })));
    if (!r.ok) showMessage('Tracking not started: ' + r.reason);
  });
}

window.addEventListener('appmint:location', function (e) {
  if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
  else showMessage('Location stopped: ' + e.detail.reason);
});

function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }

Reporte uma opção inválida ou uma permissão ausente com o reason:

function tryTrack() {
  const b = window.WebToApk;
  if (!(b && typeof b.startLocationUpdates === 'function')) {
    showMessage('Background tracking needs the installed app.');
    return;
  }
  const r = JSON.parse(b.startLocationUpdates(JSON.stringify({ intervalMs: 500 })));
  if (!r.ok && r.reason === 'invalid_interval') showMessage('Interval must be 1 second or more.');
  if (!r.ok && r.reason === 'location_permission_required') showMessage('Allow location first.');
}

Um par natural com o buffer: pare no fim de um turno e envie o que foi gravado enquanto a página estava ausente.

async function endShift() {
  if (!window.WebToApk) return;
  WebToApk.stopLocationUpdates();
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (points.length) {
    await fetch('/api/shift/points', { method: 'POST', body: JSON.stringify(points) });
    WebToApk.clearBufferedLocations();
  }
}

Observações: um serviço de localização iniciado pelo app continua recebendo posições em segundo plano sem "Permitir o tempo todo"; reiniciá-lo após uma reinicialização do telefone precisa disso (requestBackgroundLocation()). Um POST que falha é entregue ao WorkManager e enviado quando a rede voltar. Telefones sem Google Play Services respondem play_services_unavailable. O Play Console exige uma declaração de localização em segundo plano.

updateBackgroundService ponte#

window.WebToApk.updateBackgroundService(optionsJson: String): String

Altera a notificação do serviço em execução (mesmas opções de startBackgroundService) → JSON {ok} ou {ok:false, reason:'not_running'}.

Exemplo

Documentado junto com WebToApk.startBackgroundService - o exemplo de lá mostra este também.

Mantém seu app em execução enquanto o usuário está ausente: um serviço em primeiro plano real do Android com uma notificação. Enquanto ele roda a página não é pausada, então seu JavaScript, <audio> e a música de fundo do app continuam (o Android ainda desacelera os timers de uma página que ninguém consegue ver).

Retorna: startBackgroundService → uma string JSON { ok: true, status: 'starting', type, notificationShown } ou { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } ou { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. As mudanças de estado chegam como appmint:background-service (e window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requer: marque Manter em execução em segundo plano (etapa Acesso → Segundo plano) e escolha o tipo de serviço. No Android 13+ a notificação só fica visível depois que o usuário permitiu notificações (notificationShown: false caso contrário - o serviço continua em execução).

function startWorkout() {
  const b = window.WebToApk;
  if (!(b && b.startBackgroundService)) return;             // browser
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({
    title: 'Workout in progress',
    text: '00:00 elapsed',
    stopLabel: 'Stop',                  // a Stop button on the notification
    progress: { value: 0, max: 100 }
  })));
  if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}

function tick(seconds) {
  if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
    WebToApk.updateBackgroundService(JSON.stringify({
      title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
      progress: { value: Math.min(100, seconds / 18), max: 100 }
    }));
  }
}

function finishWorkout() {
  if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}

window.addEventListener('appmint:background-service', function (e) {
  // reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
  // 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
  // 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
  if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});

Trate as respostas que significam que o build ou o telefone não consegue fazer isso:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine com um loop de envio: continue em execução apenas enquanto a fila tiver trabalho.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

Observações: o tipo deve ser um que o build declarou (o escolhido no assistente, mais location quando a localização em segundo plano está ativada). Deslizar o app para fora para um serviço de manter em execução, a menos que você passe keepAfterSwipe: true (a própria página já não existe então). O Play Console pede uma declaração de serviço em primeiro plano com um vídeo curto para cada tipo declarado.

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.