Crear APK gratis

API del puente de JavaScript

Ejecución en segundo plano

Un servicio en primer plano que la página inicia y detiene, ubicación que sigue llegando con la app en segundo plano, geovallas que se disparan con la app cerrada, y tu propio código ejecutado periódicamente tras un reinicio. Cada uno necesita su interruptor en la compilación y una declaración en Play Console.

AppMint.background ayudante#

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

Ejemplo

Ejecuta periódicamente el código PROPIO de tu página con la app cerrada: actualizar datos, revisar un precio, publicar una notificación. Cada ejecución carga tu página de forma invisible (mismo origen, mismo localStorage/IndexedDB, sin pantalla, sin anuncios) con ?appmint_background=1&appmint_task=<name>&appmint_run=<runId>, dispara appmint:background { name, runId } una vez cargada, y espera done(runId, { ok }). Una ejecución se corta después de unos 9 minutos.

Devuelve: cada método devuelve una Promesa. register → { ok: true, name, intervalMinutes } o { 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 } (una ejecución en cuanto las condiciones lo permitan, para pruebas). done → true cuando se terminó la ejecución. isBackgroundRun() y currentRun() son síncronos. Requiere: marcar Ejecutar código de la página con la app cerrada (paso Acceso → Segundo plano). Apps de sitio web y sin conexión/ZIP; las apps de documentos y multimedia no tienen código de página que ejecutar.

Una página, dos funciones:

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

Las páginas que se montan tarde (React, una ruta diferida) usan onRun, que también se dispara si la ejecución ya empezó, o el gancho de la 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()); };

Administrar:

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

Notas: Android decide el momento exacto; 15 minutos es el intervalo más corto y el ahorro de batería puede retrasar las ejecuciones. Con Iniciar tras reiniciar el teléfono cada tarea también se ejecuta una vez tras un reinicio. El WebToApk de una ejecución solo tiene notify, showNotification, el búfer de ubicación, las geovallas, workEnqueueOnline, isBackgroundServiceRunning y estos métodos de tareas; cualquier otra cosa está ausente (detecta la función).

addGeofence puente#

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

Añade una geovalla {id, lat, lng, radiusM, events, dwellMs?, notify?} → JSON {ok, status:'adding'} o {ok:false, reason}; el resultado es appmint:geofence-added.

Ejemplo

Un círculo que dispara enter, exit y/o dwell con la app cerrada, y opcionalmente publica una notificación. Las geovallas sobreviven a un reinicio del teléfono (se vuelven a añadir automáticamente).

Devuelve: addGeofence → una cadena JSON { ok: true, status: 'adding', id } o { 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. La respuesta de Play services llega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Una transición llega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true cuando ocurrió con la app cerrada y se entrega en el siguiente inicio. removeGeofence → true si existía; getGeofences → un arreglo JSON de lo que añadiste. Requiere: marcar Geovallas en la compilación, y «Permitir todo el tiempo» (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');

Detecta la función e informa de cada rechazo:

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

Lista lo que está registrado y elimina un elemento:

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

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

AppMint.background.done(runId, result): termina una ejecución en segundo plano; siempre false en la propia app.

Ejemplo

Documentado junto con WebToApk.backgroundRegister: el ejemplo de allí también muestra este.

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

window.WebToApk.backgroundList(): String

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

Ejemplo

Documentado junto con WebToApk.backgroundRegister: el ejemplo de allí también muestra este.

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

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

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

Ejemplo

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

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

AppMint.background.runNow(name): una ejecución en segundo plano en cuanto sus condiciones lo permitan.

Ejemplo

Documentado junto con WebToApk.backgroundRegister: el ejemplo de allí también muestra este.

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

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

AppMint.background.unregister(name); false cuando ninguna tarea tenía ese nombre.

Ejemplo

Documentado junto con WebToApk.backgroundRegister: el ejemplo de allí también muestra este.

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

window.WebToApk.clearBufferedLocations(): Boolean

Vacía el búfer que lee getBufferedLocations (llámalo después de guardar los puntos).

Ejemplo

Documentado junto con WebToApk.getBufferedLocations: el ejemplo de allí también muestra este.

Las posiciones registradas mientras tu página no estaba (app en segundo plano o cerrada), de la más antigua a la más reciente, hasta 10 000.

Devuelve: getBufferedLocations() → una cadena JSON de arreglo de { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' cuando la ubicación en segundo plano está desactivada. clearBufferedLocations() → true cuando se vació. Requiere: Ubicación en segundo plano en la compilación.

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

Inicia el seguimiento y luego gestiona un rechazo. La llamada de inicio responde con una cadena 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, ...
}

Las posiciones en vivo mientras la página está abierta llegan como appmint:location; el búfer guarda el 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 puente#

window.WebToApk.clearGeofenceEvents(): Boolean

Vacía el historial de transiciones que lee getGeofenceEvents.

Ejemplo

Documentado junto con WebToApk.getGeofenceEvents: el ejemplo de allí también muestra este.

Las últimas 100 transiciones de geovalla, de la más antigua a la más reciente, incluidas las que ocurrieron con la app cerrada. Léelas al iniciar si tu página se monta tarde y podría perderse los eventos appmint:geofence en cola.

Devuelve: getGeofenceEvents() → una cadena JSON de arreglo de { id, event, time, lat, lng }; clearGeofenceEvents() → true. Requiere: Geovallas en la compilación.

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

Añade una geovalla y luego vigila las transiciones mientras la página esté abierta.

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

Lista lo que está registrado y elimina un elemento cuando el usuario sale de una función.

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

window.WebToApk.getBufferedLocations(): String

Arreglo JSON de las posiciones registradas mientras la página no estaba, de la más antigua a la más reciente (hasta 10 000).

Ejemplo

Las posiciones registradas mientras tu página no estaba (app en segundo plano o cerrada), de la más antigua a la más reciente, hasta 10 000.

Devuelve: getBufferedLocations() → una cadena JSON de arreglo de { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' cuando la ubicación en segundo plano está desactivada. clearBufferedLocations() → true cuando se vació. Requiere: Ubicación en segundo plano en la compilación.

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

Inicia el seguimiento y luego gestiona un rechazo. La llamada de inicio responde con una cadena 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, ...
}

Las posiciones en vivo mientras la página está abierta llegan como appmint:location; el búfer guarda el 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 puente#

window.WebToApk.getGeofenceEvents(): String

Arreglo JSON de las últimas 100 transiciones de geovalla {id, event, time, lat, lng}, de la más antigua a la más reciente.

Ejemplo

Las últimas 100 transiciones de geovalla, de la más antigua a la más reciente, incluidas las que ocurrieron con la app cerrada. Léelas al iniciar si tu página se monta tarde y podría perderse los eventos appmint:geofence en cola.

Devuelve: getGeofenceEvents() → una cadena JSON de arreglo de { id, event, time, lat, lng }; clearGeofenceEvents() → true. Requiere: Geovallas en la compilación.

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

Añade una geovalla y luego vigila las transiciones mientras la página esté abierta.

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

Lista lo que está registrado y elimina un elemento cuando el usuario sale de una función.

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

window.WebToApk.getGeofences(): String

Arreglo JSON de las geovallas que esta app ha añadido (sobreviven a los reinicios).

Ejemplo

Documentado junto con WebToApk.addGeofence: el ejemplo de allí también muestra este.

Un círculo que dispara enter, exit y/o dwell con la app cerrada, y opcionalmente publica una notificación. Las geovallas sobreviven a un reinicio del teléfono (se vuelven a añadir automáticamente).

Devuelve: addGeofence → una cadena JSON { ok: true, status: 'adding', id } o { 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. La respuesta de Play services llega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Una transición llega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true cuando ocurrió con la app cerrada y se entrega en el siguiente inicio. removeGeofence → true si existía; getGeofences → un arreglo JSON de lo que añadiste. Requiere: marcar Geovallas en la compilación, y «Permitir todo el tiempo» (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');

Detecta la función e informa de cada rechazo:

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

Lista lo que está registrado y elimina un elemento:

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

window.WebToApk.getLocationPermissionState(): String

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

Ejemplo

Documentado junto con WebToApk.requestBackgroundLocation: el ejemplo de allí también muestra este.

Pide la ubicación y luego «Permitir todo el tiempo» (Android 10+ los pide por separado; en Android 11+ el segundo paso abre la página de ajustes de ubicación de la app). Las geovallas lo necesitan; también lo necesita reiniciar el seguimiento de ubicación tras un reinicio del teléfono.

Devuelve: nada directamente; la respuesta llega como appmint:location-permission (y window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → el mismo objeto como cadena JSON, sin preguntar. Requiere: Ubicación en segundo plano o Geovallas en la compilación.

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

Comprueba primero el estado y explica qué debe tocar el usuario:

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

Solicita el permiso y luego añade una geovalla una vez concedido:

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

window.WebToApk.isBackgroundRun(): Boolean

False aquí: la propia página de la app nunca es una ejecución en segundo plano (una ejecución sin interfaz responde true).

Ejemplo

Documentado junto con WebToApk.backgroundRegister: el ejemplo de allí también muestra este.

Los métodos directos detrás de AppMint.background: usa AppMint.background.register(...) (devuelve promesas y gestiona el evento de ejecución). Se listan aquí para que cada método del puente tenga un ejemplo.

Devuelve: backgroundRegister → JSON { ok: true, name, intervalMinutes } o { ok: false, reason } (not_enabled, invalid_name - letras, dígitos, _, -, hasta 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true si existía. backgroundList → arreglo JSON. backgroundRunNow → JSON { ok, name } o { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true en una ejecución en segundo plano, siempre false en la app. isBackgroundRun() → false en la app, true en una ejecución en segundo plano. Requiere: marcar Ejecutar código de la página con la app cerrada.

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

Detecta la función y distingue la ejecución de la app de la ejecución en 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'
}

Prueba una tarea y limpia:

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

window.WebToApk.isBackgroundServiceRunning(): Boolean

Si el servicio en primer plano de la app está en ejecución (ya sea de mantener en ejecución o de ubicación en segundo plano).

Ejemplo

Documentado junto con WebToApk.startBackgroundService: el ejemplo de allí también muestra este.

Mantiene tu app en ejecución mientras el usuario no está: un verdadero servicio en primer plano de Android con una notificación. Mientras se ejecuta, la página no se pausa, así que su JavaScript, <audio> y la música de fondo de la app continúan (Android aún ralentiza los temporizadores de una página que nadie puede ver).

Devuelve: startBackgroundService → una cadena JSON { ok: true, status: 'starting', type, notificationShown } o { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } o { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Los cambios de estado llegan como appmint:background-service (y window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requiere: marcar Mantener en ejecución en segundo plano (paso Acceso → Segundo plano) y elegir el tipo de servicio. En Android 13+ la notificación solo es visible una vez que el usuario permitió las notificaciones (notificationShown: false en caso contrario; el servicio igualmente se ejecuta).

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

Gestiona las respuestas que significan que la compilación o el teléfono no pueden hacerlo:

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

Combínalo con un ciclo de subida: mantén la ejecución solo mientras la cola tenga trabajo.

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

Notas: el tipo debe ser uno que la compilación haya declarado (el elegido en el asistente, más location cuando la ubicación en segundo plano está activada). Deslizar la app para cerrarla detiene un servicio de mantener en ejecución, a menos que pases keepAfterSwipe: true (entonces la página en sí ya no existe). Play Console pide una declaración de servicio en primer plano con un video corto por cada tipo declarado.

isIgnoringBatteryOptimizations puente#

window.WebToApk.isIgnoringBatteryOptimizations(): Boolean

Si el usuario eximió a esta app de la optimización de batería (modo Doze, espera de apps).

Ejemplo

Comprueba y pide la exención de la optimización de batería, para que Doze no posponga tu trabajo en segundo plano.

Devuelve: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (el diálogo Permitir/Denegar del sistema; solo cuando la compilación marcó Pedir directamente omitir la optimización de batería), 'settings_opened' (la lista de optimización de batería; sin permiso, sin declaración en Play) o 'unavailable'. Requiere: nada para la lista de ajustes. El diálogo directo declara REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, que Play acepta solo cuando es la función principal de la 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".');
});

Di qué pasó en cada respuesta.

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

Combínalo con el seguimiento en segundo plano, ya que Doze lo pausaría.

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

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

Elimina la geovalla con este id; false cuando no había ninguna.

Ejemplo

Documentado junto con WebToApk.addGeofence: el ejemplo de allí también muestra este.

Un círculo que dispara enter, exit y/o dwell con la app cerrada, y opcionalmente publica una notificación. Las geovallas sobreviven a un reinicio del teléfono (se vuelven a añadir automáticamente).

Devuelve: addGeofence → una cadena JSON { ok: true, status: 'adding', id } o { 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. La respuesta de Play services llega como appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 por app, play_services_unavailable). Una transición llega como appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true cuando ocurrió con la app cerrada y se entrega en el siguiente inicio. removeGeofence → true si existía; getGeofences → un arreglo JSON de lo que añadiste. Requiere: marcar Geovallas en la compilación, y «Permitir todo el tiempo» (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');

Detecta la función e informa de cada rechazo:

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

Lista lo que está registrado y elimina un elemento:

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

window.WebToApk.requestBackgroundLocation()

Pide la ubicación y luego «Permitir todo el tiempo»; el resultado llega como appmint:location-permission.

Ejemplo

Pide la ubicación y luego «Permitir todo el tiempo» (Android 10+ los pide por separado; en Android 11+ el segundo paso abre la página de ajustes de ubicación de la app). Las geovallas lo necesitan; también lo necesita reiniciar el seguimiento de ubicación tras un reinicio del teléfono.

Devuelve: nada directamente; la respuesta llega como appmint:location-permission (y window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → el mismo objeto como cadena JSON, sin preguntar. Requiere: Ubicación en segundo plano o Geovallas en la compilación.

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

Comprueba primero el estado y explica qué debe tocar el usuario:

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

Solicita el permiso y luego añade una geovalla una vez concedido:

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

window.WebToApk.requestIgnoreBatteryOptimizations(): String

Pide la exención de la batería: 'already_ignoring' | 'dialog_opened' | 'settings_opened' | 'unavailable'.

Ejemplo

Documentado junto con WebToApk.isIgnoringBatteryOptimizations: el ejemplo de allí también muestra este.

Comprueba y pide la exención de la optimización de batería, para que Doze no posponga tu trabajo en segundo plano.

Devuelve: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (el diálogo Permitir/Denegar del sistema; solo cuando la compilación marcó Pedir directamente omitir la optimización de batería), 'settings_opened' (la lista de optimización de batería; sin permiso, sin declaración en Play) o 'unavailable'. Requiere: nada para la lista de ajustes. El diálogo directo declara REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, que Play acepta solo cuando es la función principal de la 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".');
});

Di qué pasó en cada respuesta.

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

Combínalo con el seguimiento en segundo plano, ya que Doze lo pausaría.

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

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

Inicia el servicio en primer plano de la app: {title, text, type?, progress?, stopLabel?, keepAfterSwipe?} → JSON {ok, status, type, notificationShown} o {ok:false, reason}.

Ejemplo

Mantiene tu app en ejecución mientras el usuario no está: un verdadero servicio en primer plano de Android con una notificación. Mientras se ejecuta, la página no se pausa, así que su JavaScript, <audio> y la música de fondo de la app continúan (Android aún ralentiza los temporizadores de una página que nadie puede ver).

Devuelve: startBackgroundService → una cadena JSON { ok: true, status: 'starting', type, notificationShown } o { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } o { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Los cambios de estado llegan como appmint:background-service (y window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requiere: marcar Mantener en ejecución en segundo plano (paso Acceso → Segundo plano) y elegir el tipo de servicio. En Android 13+ la notificación solo es visible una vez que el usuario permitió las notificaciones (notificationShown: false en caso contrario; el servicio igualmente se ejecuta).

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

Gestiona las respuestas que significan que la compilación o el teléfono no pueden hacerlo:

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

Combínalo con un ciclo de subida: mantén la ejecución solo mientras la cola tenga trabajo.

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

Notas: el tipo debe ser uno que la compilación haya declarado (el elegido en el asistente, más location cuando la ubicación en segundo plano está activada). Deslizar la app para cerrarla detiene un servicio de mantener en ejecución, a menos que pases keepAfterSwipe: true (entonces la página en sí ya no existe). Play Console pide una declaración de servicio en primer plano con un video corto por cada tipo declarado.

startLocationUpdates puente#

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

Actualizaciones de ubicación {intervalMs, minDistanceM, accuracy, background, uploadUrl?, headers?} → JSON {ok, background, backgroundPermission, precise} o {ok:false, reason}; las posiciones llegan como appmint:location.

Ejemplo

Actualizaciones de ubicación que siguen llegando mientras la app está en segundo plano (un rastreador de carreras, una app de reparto). Con background: true (el valor por defecto) se ejecutan dentro del servicio en primer plano de la app. Cada posición va a la página como appmint:location mientras la página está presente, a un búfer (getBufferedLocations()) cuando no lo está, y, cuando das una uploadUrl, a tu servidor como un POST JSON desde código nativo, así que el seguimiento funciona aunque la página ya no esté.

Devuelve: una cadena JSON { ok: true, background, backgroundPermission, precise } o { 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 si algo estaba en ejecución. Posiciones: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, o { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Requiere: marcar Ubicación en segundo plano (paso Acceso → Segundo plano) y primero el permiso de ubicación del usuario.

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

Informa de una opción incorrecta o un permiso faltante con el 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.');
}

Un complemento natural del búfer: detenerse al final de un turno y subir lo que se grabó mientras la página no estaba.

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

Notas: un servicio de ubicación iniciado desde la app sigue recibiendo posiciones en segundo plano sin «Permitir todo el tiempo»; reiniciarlo tras un reinicio del teléfono sí lo necesita (requestBackgroundLocation()). Un POST que falla se entrega a WorkManager y se envía cuando vuelve la red. Los teléfonos sin Google Play services responden play_services_unavailable. Play Console necesita una declaración de ubicación en segundo plano.

stopBackgroundService puente#

window.WebToApk.stopBackgroundService(): Boolean

Detiene el servicio iniciado con startBackgroundService (la ubicación en segundo plano, si está activa, continúa).

Ejemplo

Documentado junto con WebToApk.startBackgroundService: el ejemplo de allí también muestra este.

Mantiene tu app en ejecución mientras el usuario no está: un verdadero servicio en primer plano de Android con una notificación. Mientras se ejecuta, la página no se pausa, así que su JavaScript, <audio> y la música de fondo de la app continúan (Android aún ralentiza los temporizadores de una página que nadie puede ver).

Devuelve: startBackgroundService → una cadena JSON { ok: true, status: 'starting', type, notificationShown } o { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } o { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Los cambios de estado llegan como appmint:background-service (y window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requiere: marcar Mantener en ejecución en segundo plano (paso Acceso → Segundo plano) y elegir el tipo de servicio. En Android 13+ la notificación solo es visible una vez que el usuario permitió las notificaciones (notificationShown: false en caso contrario; el servicio igualmente se ejecuta).

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

Gestiona las respuestas que significan que la compilación o el teléfono no pueden hacerlo:

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

Combínalo con un ciclo de subida: mantén la ejecución solo mientras la cola tenga trabajo.

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

Notas: el tipo debe ser uno que la compilación haya declarado (el elegido en el asistente, más location cuando la ubicación en segundo plano está activada). Deslizar la app para cerrarla detiene un servicio de mantener en ejecución, a menos que pases keepAfterSwipe: true (entonces la página en sí ya no existe). Play Console pide una declaración de servicio en primer plano con un video corto por cada tipo declarado.

stopLocationUpdates puente#

window.WebToApk.stopLocationUpdates(): Boolean

Detiene las actualizaciones de ubicación iniciadas con startLocationUpdates; false cuando no había ninguna en marcha.

Ejemplo

Documentado junto con WebToApk.startLocationUpdates: el ejemplo de allí también muestra este.

Actualizaciones de ubicación que siguen llegando mientras la app está en segundo plano (un rastreador de carreras, una app de reparto). Con background: true (el valor por defecto) se ejecutan dentro del servicio en primer plano de la app. Cada posición va a la página como appmint:location mientras la página está presente, a un búfer (getBufferedLocations()) cuando no lo está, y, cuando das una uploadUrl, a tu servidor como un POST JSON desde código nativo, así que el seguimiento funciona aunque la página ya no esté.

Devuelve: una cadena JSON { ok: true, background, backgroundPermission, precise } o { 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 si algo estaba en ejecución. Posiciones: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, o { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Requiere: marcar Ubicación en segundo plano (paso Acceso → Segundo plano) y primero el permiso de ubicación del usuario.

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

Informa de una opción incorrecta o un permiso faltante con el 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.');
}

Un complemento natural del búfer: detenerse al final de un turno y subir lo que se grabó mientras la página no estaba.

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

Notas: un servicio de ubicación iniciado desde la app sigue recibiendo posiciones en segundo plano sin «Permitir todo el tiempo»; reiniciarlo tras un reinicio del teléfono sí lo necesita (requestBackgroundLocation()). Un POST que falla se entrega a WorkManager y se envía cuando vuelve la red. Los teléfonos sin Google Play services responden play_services_unavailable. Play Console necesita una declaración de ubicación en segundo plano.

updateBackgroundService puente#

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

Cambia la notificación del servicio en ejecución (mismas opciones que startBackgroundService) → JSON {ok} o {ok:false, reason:'not_running'}.

Ejemplo

Documentado junto con WebToApk.startBackgroundService: el ejemplo de allí también muestra este.

Mantiene tu app en ejecución mientras el usuario no está: un verdadero servicio en primer plano de Android con una notificación. Mientras se ejecuta, la página no se pausa, así que su JavaScript, <audio> y la música de fondo de la app continúan (Android aún ralentiza los temporizadores de una página que nadie puede ver).

Devuelve: startBackgroundService → una cadena JSON { ok: true, status: 'starting', type, notificationShown } o { ok: false, reason } - motivo not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } o { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Los cambios de estado llegan como appmint:background-service (y window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Requiere: marcar Mantener en ejecución en segundo plano (paso Acceso → Segundo plano) y elegir el tipo de servicio. En Android 13+ la notificación solo es visible una vez que el usuario permitió las notificaciones (notificationShown: false en caso contrario; el servicio igualmente se ejecuta).

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

Gestiona las respuestas que significan que la compilación o el teléfono no pueden hacerlo:

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

Combínalo con un ciclo de subida: mantén la ejecución solo mientras la cola tenga trabajo.

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

Notas: el tipo debe ser uno que la compilación haya declarado (el elegido en el asistente, más location cuando la ubicación en segundo plano está activada). Deslizar la app para cerrarla detiene un servicio de mantener en ejecución, a menos que pases keepAfterSwipe: true (entonces la página en sí ya no existe). Play Console pide una declaración de servicio en primer plano con un video corto por cada tipo declarado.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.