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 existedbackgroundList мост#
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 existedbackgroundRegister мост#
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 existedbackgroundRunNow мост#
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 existedbackgroundUnregister мост#
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 existedclearBufferedLocations мост#
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 existedisBackgroundServiceRunning мост#
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 требует декларацию службы переднего плана с коротким видео для каждого объявленного типа.