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 existedbackgroundList 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 existedbackgroundRegister 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 existedbackgroundRunNow 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 existedbackgroundUnregister 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 existedclearBufferedLocations 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 existedisBackgroundServiceRunning 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.