Создать APK бесплатно

JavaScript bridge API

Работа в фоне

Служба переднего плана, которую страница запускает и останавливает, геолокация, которая продолжает приходить, когда приложение в фоне, геозоны, срабатывающие при закрытом приложении, и ваш собственный код, периодически запускаемый после перезагрузки. Каждому нужен свой переключатель в сборке и декларация в Play Console.

AppMint.background помощник#

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

Пример

Периодически запускает СОБСТВЕННЫЙ код вашей страницы при закрытом приложении — обновить данные, проверить цену, отправить уведомление. Каждый запуск загружает вашу страницу невидимо (тот же origin, тот же localStorage/IndexedDB, без экрана, без рекламы) с ?appmint_background=1&appmint_task=<name>&appmint_run=<runId>, вызывает appmint:background { name, runId } после загрузки и ждёт done(runId, { ok }). Запуск прерывается примерно через 9 минут.

Возвращает: каждый метод возвращает Promise. register → { ok: true, name, intervalMinutes } или { 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 } (один запуск, как только позволят условия — для тестирования). done → true, когда запуск завершён. isBackgroundRun() и currentRun() синхронны. Требуется: отметьте Запуск кода страницы при закрытом приложении (шаг «Доступ» → «Фон»). Приложения-сайты и офлайн/ZIP; у приложений-документов и медиа нет кода страницы для запуска.

Одна страница, две роли:

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

Страницы, монтируемые поздно (React, ленивый маршрут), используют onRun, который срабатывает и если запуск уже начался, или хук страницы:

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

Управление:

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

Примечания: точный момент выбирает Android; 15 минут — самый короткий интервал, а экономия батареи может задерживать запуски. С Запуск после перезагрузки телефона каждая задача также выполняется один раз после перезагрузки. В WebToApk запуска есть только notify, showNotification, буфер геолокации, геозоны, workEnqueueOnline, isBackgroundServiceRunning и эти методы задач; всего остального нет (проверяйте наличие).

addGeofence мост#

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

Добавляет геозону {id, lat, lng, radiusM, events, dwellMs?, notify?} → JSON {ok, status:'adding'} или {ok:false, reason}; результат — appmint:geofence-added.

Пример

Круг, который вызывает enter, exit и/или dwell при закрытом приложении, при желании отправляя уведомление. Геозоны переживают перезагрузку телефона (они добавляются обратно автоматически).

Возвращает: addGeofence → строку JSON { ok: true, status: 'adding', id } или { ok: false, reason } — not_enabled, invalid_id, invalid_position, invalid_radius (от 1 м до 100 км), invalid_events, location_permission_required, background_permission_required, foreign_origin. Ответ сервисов Play приходит как appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences — 100 на приложение, play_services_unavailable). Переход приходит как appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } — queued: true, когда он произошёл при закрытом приложении и передаётся при следующем запуске. removeGeofence → true, если существовала; getGeofences → массив JSON добавленных вами. Требуется: отметьте Геозоны в сборке и «Разрешить всегда» (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');

Проверьте поддержку и сообщайте о каждом отказе:

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

Покажите, что зарегистрировано, и удалите одно:

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 мост#

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

AppMint.background.done(runId, result): завершает фоновый запуск; в самом приложении всегда false.

Пример

Описано вместе с WebToApk.backgroundRegister — пример там показывает и этот.

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

window.WebToApk.backgroundList(): String

AppMint.background.list(): массив JSON {name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult}.

Пример

Описано вместе с WebToApk.backgroundRegister — пример там показывает и этот.

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

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

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

Пример

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

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

AppMint.background.runNow(name): один фоновый запуск, как только позволят условия.

Пример

Описано вместе с WebToApk.backgroundRegister — пример там показывает и этот.

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

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

AppMint.background.unregister(name); false, если задачи с таким именем не было.

Пример

Описано вместе с WebToApk.backgroundRegister — пример там показывает и этот.

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

window.WebToApk.clearBufferedLocations(): Boolean

Очищает буфер, который читает getBufferedLocations (вызывайте после сохранения точек).

Пример

Описано вместе с WebToApk.getBufferedLocations — пример там показывает и этот.

Координаты, записанные, пока вашей страницы не было (приложение в фоне или закрыто), от старых к новым, до 10 000.

Возвращает: getBufferedLocations() → строку JSON с массивом { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]', когда фоновая геолокация выключена. clearBufferedLocations() → true, когда очищено. Требуется: Фоновая геолокация в сборке.

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

Запустите отслеживание, затем обработайте отказ. Вызов запуска отвечает строкой 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, ...
}

Живые координаты, пока страница открыта, приходят как appmint:location; остальное хранит буфер.

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 мост#

window.WebToApk.clearGeofenceEvents(): Boolean

Очищает историю переходов, которую читает getGeofenceEvents.

Пример

Описано вместе с WebToApk.getGeofenceEvents — пример там показывает и этот.

Последние 100 переходов геозон, от старых к новым — включая те, что произошли при закрытом приложении. Читайте при старте, если ваша страница монтируется поздно и может пропустить события appmint:geofence из очереди.

Возвращает: getGeofenceEvents() → строку JSON с массивом { id, event, time, lat, lng }; clearGeofenceEvents() → true. Требуется: Геозоны в сборке.

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

Добавьте геозону, затем следите за переходами, пока страница открыта.

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

Покажите, что зарегистрировано, и удалите одно, когда пользователь покидает функцию.

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 мост#

window.WebToApk.getBufferedLocations(): String

Массив JSON с координатами, записанными, пока страницы не было, от старых к новым (до 10 000).

Пример

Координаты, записанные, пока вашей страницы не было (приложение в фоне или закрыто), от старых к новым, до 10 000.

Возвращает: getBufferedLocations() → строку JSON с массивом { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]', когда фоновая геолокация выключена. clearBufferedLocations() → true, когда очищено. Требуется: Фоновая геолокация в сборке.

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

Запустите отслеживание, затем обработайте отказ. Вызов запуска отвечает строкой 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, ...
}

Живые координаты, пока страница открыта, приходят как appmint:location; остальное хранит буфер.

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 мост#

window.WebToApk.getGeofenceEvents(): String

Массив JSON с последними 100 переходами геозон {id, event, time, lat, lng}, от старых к новым.

Пример

Последние 100 переходов геозон, от старых к новым — включая те, что произошли при закрытом приложении. Читайте при старте, если ваша страница монтируется поздно и может пропустить события appmint:geofence из очереди.

Возвращает: getGeofenceEvents() → строку JSON с массивом { id, event, time, lat, lng }; clearGeofenceEvents() → true. Требуется: Геозоны в сборке.

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

Добавьте геозону, затем следите за переходами, пока страница открыта.

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

Покажите, что зарегистрировано, и удалите одно, когда пользователь покидает функцию.

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 мост#

window.WebToApk.getGeofences(): String

Массив JSON геозон, добавленных этим приложением (они переживают перезапуски).

Пример

Описано вместе с WebToApk.addGeofence — пример там показывает и этот.

Круг, который вызывает enter, exit и/или dwell при закрытом приложении, при желании отправляя уведомление. Геозоны переживают перезагрузку телефона (они добавляются обратно автоматически).

Возвращает: addGeofence → строку JSON { ok: true, status: 'adding', id } или { ok: false, reason } — not_enabled, invalid_id, invalid_position, invalid_radius (от 1 м до 100 км), invalid_events, location_permission_required, background_permission_required, foreign_origin. Ответ сервисов Play приходит как appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences — 100 на приложение, play_services_unavailable). Переход приходит как appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } — queued: true, когда он произошёл при закрытом приложении и передаётся при следующем запуске. removeGeofence → true, если существовала; getGeofences → массив JSON добавленных вами. Требуется: отметьте Геозоны в сборке и «Разрешить всегда» (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');

Проверьте поддержку и сообщайте о каждом отказе:

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

Покажите, что зарегистрировано, и удалите одно:

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 мост#

window.WebToApk.getLocationPermissionState(): String

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

Пример

Описано вместе с WebToApk.requestBackgroundLocation — пример там показывает и этот.

Запрашивает геолокацию, затем «Разрешить всегда» (Android 10+ запрашивает их отдельно; на Android 11+ второй шаг открывает страницу настроек геолокации приложения). Это нужно геозонам, а также для перезапуска отслеживания геолокации после перезагрузки телефона.

Возвращает: напрямую ничего; ответ приходит как appmint:location-permission (и window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → тот же объект строкой JSON, без запроса. Требуется: Фоновая геолокация или Геозоны в сборке.

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

Сначала проверьте состояние и объясните, что должен нажать пользователь:

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

Запросите разрешение, затем добавьте геозону, когда оно выдано:

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 мост#

window.WebToApk.isBackgroundRun(): Boolean

Здесь false: страница самого приложения никогда не бывает фоновым запуском (безголовый запуск возвращает true).

Пример

Описано вместе с WebToApk.backgroundRegister — пример там показывает и этот.

Исходные методы за AppMint.background — используйте AppMint.background.register(...) (он возвращает promise и обрабатывает событие запуска). Перечислены здесь, чтобы у каждого метода моста был пример.

Возвращает: backgroundRegister → JSON { ok: true, name, intervalMinutes } или { ok: false, reason } (not_enabled, invalid_name — буквы, цифры, _, -, до 64 — interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true, если существовала. backgroundList → массив JSON. backgroundRunNow → JSON { ok, name } или { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true при фоновом запуске, всегда false в приложении. isBackgroundRun() → false в приложении, true при фоновом запуске. Требуется: отметьте Запуск кода страницы при закрытом приложении.

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

Проверьте поддержку и отличите запуск приложения от фонового запуска:

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

Проверьте задачу и приберите за собой:

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 мост#

window.WebToApk.isBackgroundServiceRunning(): Boolean

Работает ли служба переднего плана приложения (для «продолжать работу» или фоновой геолокации).

Пример

Описано вместе с WebToApk.startBackgroundService — пример там показывает и этот.

Сохраняет работу вашего приложения, пока пользователя нет: настоящая служба переднего плана Android с уведомлением. Пока она работает, страница не приостанавливается, поэтому её JavaScript, <audio> и фоновая музыка приложения продолжают работать (Android всё же замедляет таймеры страницы, которую никто не видит).

Возвращает: startBackgroundService → строку JSON { ok: true, status: 'starting', type, notificationShown } или { ok: false, reason } — причина not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } или { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Изменения состояния приходят как appmint:background-service (и window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Требуется: отметьте Продолжать работу в фоне (шаг «Доступ» → «Фон») и выберите тип службы. На Android 13+ уведомление видно, только когда пользователь разрешил уведомления (иначе notificationShown: false — служба всё равно работает).

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

Обработайте ответы, означающие, что сборка или телефон не могут это сделать:

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

Сочетайте с циклом загрузки: продолжайте работать, только пока в очереди есть работа.

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

Примечания: тип должен быть из объявленных сборкой (выбранный в мастере плюс location, когда включена фоновая геолокация). Смахивание приложения останавливает службу «продолжать работу», если не передать keepAfterSwipe: true (страницы тогда уже нет). Play Console требует декларацию службы переднего плана с коротким видео для каждого объявленного типа.

isIgnoringBatteryOptimizations мост#

window.WebToApk.isIgnoringBatteryOptimizations(): Boolean

Исключил ли пользователь это приложение из оптимизации батареи (Doze, режим ожидания приложений).

Пример

Проверяет и запрашивает исключение из оптимизации батареи, чтобы Doze не откладывал вашу фоновую работу.

Возвращает: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (системный диалог «Разрешить/Запретить» — только если в сборке отмечено Запрашивать отключение оптимизации батареи напрямую), 'settings_opened' (список оптимизации батареи; без разрешения и без декларации Play) или 'unavailable'. Требуется: ничего для списка настроек. Прямой диалог объявляет REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, который Play принимает, только если это основная функция приложения.

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

Сообщайте, что произошло, для каждого ответа.

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

Используйте вместе с фоновым отслеживанием, поскольку Doze его приостановил бы.

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 мост#

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

Удаляет геозону с этим id; false, если её не было.

Пример

Описано вместе с WebToApk.addGeofence — пример там показывает и этот.

Круг, который вызывает enter, exit и/или dwell при закрытом приложении, при желании отправляя уведомление. Геозоны переживают перезагрузку телефона (они добавляются обратно автоматически).

Возвращает: addGeofence → строку JSON { ok: true, status: 'adding', id } или { ok: false, reason } — not_enabled, invalid_id, invalid_position, invalid_radius (от 1 м до 100 км), invalid_events, location_permission_required, background_permission_required, foreign_origin. Ответ сервисов Play приходит как appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences — 100 на приложение, play_services_unavailable). Переход приходит как appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } — queued: true, когда он произошёл при закрытом приложении и передаётся при следующем запуске. removeGeofence → true, если существовала; getGeofences → массив JSON добавленных вами. Требуется: отметьте Геозоны в сборке и «Разрешить всегда» (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');

Проверьте поддержку и сообщайте о каждом отказе:

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

Покажите, что зарегистрировано, и удалите одно:

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 мост#

window.WebToApk.requestBackgroundLocation()

Запрашивает геолокацию, затем «Разрешить всегда»; результат приходит как appmint:location-permission.

Пример

Запрашивает геолокацию, затем «Разрешить всегда» (Android 10+ запрашивает их отдельно; на Android 11+ второй шаг открывает страницу настроек геолокации приложения). Это нужно геозонам, а также для перезапуска отслеживания геолокации после перезагрузки телефона.

Возвращает: напрямую ничего; ответ приходит как appmint:location-permission (и window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → тот же объект строкой JSON, без запроса. Требуется: Фоновая геолокация или Геозоны в сборке.

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

Сначала проверьте состояние и объясните, что должен нажать пользователь:

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

Запросите разрешение, затем добавьте геозону, когда оно выдано:

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 мост#

window.WebToApk.requestIgnoreBatteryOptimizations(): String

Запрашивает исключение из экономии батареи: 'already_ignoring' | 'dialog_opened' | 'settings_opened' | 'unavailable'.

Пример

Описано вместе с WebToApk.isIgnoringBatteryOptimizations — пример там показывает и этот.

Проверяет и запрашивает исключение из оптимизации батареи, чтобы Doze не откладывал вашу фоновую работу.

Возвращает: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (системный диалог «Разрешить/Запретить» — только если в сборке отмечено Запрашивать отключение оптимизации батареи напрямую), 'settings_opened' (список оптимизации батареи; без разрешения и без декларации Play) или 'unavailable'. Требуется: ничего для списка настроек. Прямой диалог объявляет REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, который Play принимает, только если это основная функция приложения.

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

Сообщайте, что произошло, для каждого ответа.

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

Используйте вместе с фоновым отслеживанием, поскольку Doze его приостановил бы.

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 мост#

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

Запускает службу переднего плана приложения: {title, text, type?, progress?, stopLabel?, keepAfterSwipe?} → JSON {ok, status, type, notificationShown} или {ok:false, reason}.

Пример

Сохраняет работу вашего приложения, пока пользователя нет: настоящая служба переднего плана Android с уведомлением. Пока она работает, страница не приостанавливается, поэтому её JavaScript, <audio> и фоновая музыка приложения продолжают работать (Android всё же замедляет таймеры страницы, которую никто не видит).

Возвращает: startBackgroundService → строку JSON { ok: true, status: 'starting', type, notificationShown } или { ok: false, reason } — причина not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } или { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Изменения состояния приходят как appmint:background-service (и window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Требуется: отметьте Продолжать работу в фоне (шаг «Доступ» → «Фон») и выберите тип службы. На Android 13+ уведомление видно, только когда пользователь разрешил уведомления (иначе notificationShown: false — служба всё равно работает).

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

Обработайте ответы, означающие, что сборка или телефон не могут это сделать:

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

Сочетайте с циклом загрузки: продолжайте работать, только пока в очереди есть работа.

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

Примечания: тип должен быть из объявленных сборкой (выбранный в мастере плюс location, когда включена фоновая геолокация). Смахивание приложения останавливает службу «продолжать работу», если не передать keepAfterSwipe: true (страницы тогда уже нет). Play Console требует декларацию службы переднего плана с коротким видео для каждого объявленного типа.

startLocationUpdates мост#

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

Обновления геолокации {intervalMs, minDistanceM, accuracy, background, uploadUrl?, headers?} → JSON {ok, background, backgroundPermission, precise} или {ok:false, reason}; координаты приходят как appmint:location.

Пример

Обновления геолокации, которые продолжают приходить, пока приложение в фоне (трекер бега, приложение доставки). С background: true (по умолчанию) они работают внутри службы переднего плана приложения. Каждая координата идёт на страницу как appmint:location, пока страница есть, в буфер (getBufferedLocations()), когда её нет, и — если вы задали uploadUrl — на ваш сервер как JSON POST из нативного кода, так что отслеживание работает и без страницы.

Возвращает: строку JSON { ok: true, background, backgroundPermission, precise } или { ok: false, reason } — not_enabled, invalid_options, invalid_interval (от 1 с до 24 ч), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true, если что-то работало. Координаты: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background } или { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Требуется: отметьте Фоновая геолокация (шаг «Доступ» → «Фон») и сначала получите разрешение пользователя на геолокацию.

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

Сообщайте о неверном параметре или отсутствующем разрешении через 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.');
}

Естественная пара к буферу: остановитесь в конце смены и загрузите то, что было записано, пока страницы не было.

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

Примечания: служба геолокации, запущенная из приложения, продолжает получать координаты в фоне и без «Разрешить всегда»; для её перезапуска после перезагрузки телефона оно нужно (requestBackgroundLocation()). POST, который не удался, передаётся в WorkManager и отправляется, когда сеть вернётся. Телефоны без сервисов Google Play отвечают play_services_unavailable. Play Console требует декларацию фоновой геолокации.

stopBackgroundService мост#

window.WebToApk.stopBackgroundService(): Boolean

Останавливает службу, запущенную startBackgroundService (фоновая геолокация, если запущена, продолжается).

Пример

Описано вместе с WebToApk.startBackgroundService — пример там показывает и этот.

Сохраняет работу вашего приложения, пока пользователя нет: настоящая служба переднего плана Android с уведомлением. Пока она работает, страница не приостанавливается, поэтому её JavaScript, <audio> и фоновая музыка приложения продолжают работать (Android всё же замедляет таймеры страницы, которую никто не видит).

Возвращает: startBackgroundService → строку JSON { ok: true, status: 'starting', type, notificationShown } или { ok: false, reason } — причина not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } или { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Изменения состояния приходят как appmint:background-service (и window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Требуется: отметьте Продолжать работу в фоне (шаг «Доступ» → «Фон») и выберите тип службы. На Android 13+ уведомление видно, только когда пользователь разрешил уведомления (иначе notificationShown: false — служба всё равно работает).

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

Обработайте ответы, означающие, что сборка или телефон не могут это сделать:

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

Сочетайте с циклом загрузки: продолжайте работать, только пока в очереди есть работа.

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

Примечания: тип должен быть из объявленных сборкой (выбранный в мастере плюс location, когда включена фоновая геолокация). Смахивание приложения останавливает службу «продолжать работу», если не передать keepAfterSwipe: true (страницы тогда уже нет). Play Console требует декларацию службы переднего плана с коротким видео для каждого объявленного типа.

stopLocationUpdates мост#

window.WebToApk.stopLocationUpdates(): Boolean

Останавливает обновления геолокации, запущенные startLocationUpdates; false, если ничего не было запущено.

Пример

Описано вместе с WebToApk.startLocationUpdates — пример там показывает и этот.

Обновления геолокации, которые продолжают приходить, пока приложение в фоне (трекер бега, приложение доставки). С background: true (по умолчанию) они работают внутри службы переднего плана приложения. Каждая координата идёт на страницу как appmint:location, пока страница есть, в буфер (getBufferedLocations()), когда её нет, и — если вы задали uploadUrl — на ваш сервер как JSON POST из нативного кода, так что отслеживание работает и без страницы.

Возвращает: строку JSON { ok: true, background, backgroundPermission, precise } или { ok: false, reason } — not_enabled, invalid_options, invalid_interval (от 1 с до 24 ч), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true, если что-то работало. Координаты: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background } или { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Требуется: отметьте Фоновая геолокация (шаг «Доступ» → «Фон») и сначала получите разрешение пользователя на геолокацию.

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

Сообщайте о неверном параметре или отсутствующем разрешении через 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.');
}

Естественная пара к буферу: остановитесь в конце смены и загрузите то, что было записано, пока страницы не было.

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

Примечания: служба геолокации, запущенная из приложения, продолжает получать координаты в фоне и без «Разрешить всегда»; для её перезапуска после перезагрузки телефона оно нужно (requestBackgroundLocation()). POST, который не удался, передаётся в WorkManager и отправляется, когда сеть вернётся. Телефоны без сервисов Google Play отвечают play_services_unavailable. Play Console требует декларацию фоновой геолокации.

updateBackgroundService мост#

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

Изменяет уведомление работающей службы (те же параметры, что у startBackgroundService) → JSON {ok} или {ok:false, reason:'not_running'}.

Пример

Описано вместе с WebToApk.startBackgroundService — пример там показывает и этот.

Сохраняет работу вашего приложения, пока пользователя нет: настоящая служба переднего плана Android с уведомлением. Пока она работает, страница не приостанавливается, поэтому её JavaScript, <audio> и фоновая музыка приложения продолжают работать (Android всё же замедляет таймеры страницы, которую никто не видит).

Возвращает: startBackgroundService → строку JSON { ok: true, status: 'starting', type, notificationShown } или { ok: false, reason } — причина not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } или { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. Изменения состояния приходят как appmint:background-service (и window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Требуется: отметьте Продолжать работу в фоне (шаг «Доступ» → «Фон») и выберите тип службы. На Android 13+ уведомление видно, только когда пользователь разрешил уведомления (иначе notificationShown: false — служба всё равно работает).

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

Обработайте ответы, означающие, что сборка или телефон не могут это сделать:

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

Сочетайте с циклом загрузки: продолжайте работать, только пока в очереди есть работа.

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

Примечания: тип должен быть из объявленных сборкой (выбранный в мастере плюс location, когда включена фоновая геолокация). Смахивание приложения останавливает службу «продолжать работу», если не передать keepAfterSwipe: true (страницы тогда уже нет). Play Console требует декларацию службы переднего плана с коротким видео для каждого объявленного типа.