Fix: background GPS points lost on OS service restart

Regression from per-session points file change (1feb2ef): the service
returns START_STICKY, so when the OS kills it during screen-off, it
restarts with a null intent and onStartCommand minted a NEW points file,
repointing gps_live_points.jsonl and orphaning accumulated background
points. Evidence: many tiny orphaned gps_points_*.jsonl fragments.

Fix: onStartCommand creates a new session file only on a genuine start
from JS (intent with extras). A START_STICKY restart (null intent) keeps
writing to the existing session file from the pointer — one file per
session until Stop.

Verified on device: am kill (= START_STICKY restart) no longer creates
a new file, pointer unchanged, trail resumes on return. Background
writing confirmed: 49 points in 70s with screen off (Dozing) at 1Hz.
This commit is contained in:
4aiman 2026-07-02 22:29:29 +03:00
commit d4ef9a1f9f
3 changed files with 18 additions and 9 deletions

View file

@ -49,7 +49,7 @@
| Файл | Строк | Назначение |
|------|-------|------------|
| `MainActivity.kt` | ~300 | FileObserver IPC, seed DB, safe insets, SAF, BLE commands, OTA check |
| `GpsRecorderService.kt` | ~500 | Foreground service GPS, WakeLock, JSONL, 50Hz акселерометр/гироскоп, dead reckoning, raw sensor files |
| `GpsRecorderService.kt` | ~500 | Foreground service GPS, WakeLock, JSONL (один файл на сессию до Стоп, идемпотентный onStartCommand при START_STICKY-перезапуске ОС), 50Hz акселерометр/гироскоп, dead reckoning, raw sensor files |
| `BleHrService.kt` | ~340 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect |
| `generated/*.kt` | ~1100 | Tauri-сгенерированные: WebView, IPC, permissions |

View file

@ -5,7 +5,7 @@
## Возможности
- **Дневник питания** — 24400+ продуктов (Магнит, 5ка, Health-Diet.ru, Ашан, Макдоналдс, KFC, Додо Пицца, KDV, ИОН, Гриднев). 37 нутриентов (витамины, минералы, omega-3/6, вода, клетчатка). Рецепты с термообработкой. Формулы порций (4*30, 300+50).
- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне. Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. Фильтр altitude outlier'ов и GPS-залипаний. Вращение карты по компасу (leaflet-rotate, EMA-сглаживание, gimbal lock компенсация). Ландшафтный режим (CSS rotation + панель справа, 3 режима: off/auto/lock). Dead reckoning при потере GPS (гироскоп + акселерометр). Placeholder до GPS (пиксельарт). Карта проезженных дорог (дедупликация 2м, вело/пешие). Импорт тайлов по регионам (SQLite, роутер LAN).
- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне (foreground service, точки пишутся при выключенном экране; один файл на сессию, переживает START_STICKY-перезапуск ОС). Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. Фильтр altitude outlier'ов и GPS-залипаний. Вращение карты по компасу (leaflet-rotate, EMA-сглаживание, gimbal lock компенсация). Ландшафтный режим (CSS rotation + панель справа, 3 режима: off/auto/lock). Dead reckoning при потере GPS (гироскоп + акселерометр). Placeholder до GPS (пиксельарт). Карта проезженных дорог (дедупликация 2м, вело/пешие). Импорт тайлов по регионам (SQLite, роутер LAN).
- **BLE пульсометр** — подключение к FitPro браслетам и стандартным HR датчикам (0x180D). ЧСС, SpO2, давление в реальном времени при записи трека.
- **Расчёт ккал** — физическая модель для велосипеда (мощность из gradient + Crr + CdA). MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
- **Тренировки** — приседания (подходы x повторения), скакалка, растяжки, бассейн (дистанция). Автоматический расчёт ккал по MET и весу.

View file

@ -95,15 +95,24 @@ class GpsRecorderService : Service(), LocationListener, SensorEventListener {
// Points file in app root data dir (same as Rust app_data_dir)
val appDataDir = filesDir.parentFile ?: filesDir
// Each session gets its own file — never overwrite previous sessions.
// Old files are only deleted by JS after confirmed DB insert.
val sessionTs = System.currentTimeMillis()
pointsFile = File(appDataDir, "gps_points_$sessionTs.jsonl")
// Write current filename so JS knows which file is active
File(appDataDir, "gps_live_points.jsonl").writeText(pointsFile!!.name)
// Keep writing to ONE file until "Stop". A genuine start from JS carries
// an intent (with config extras); a START_STICKY restart by the OS comes
// with a null intent — in that case resume the existing session file so
// background points are never orphaned.
val ptrFile = File(appDataDir, "gps_live_points.jsonl")
val existingName = if (ptrFile.exists()) ptrFile.readText().trim() else ""
if (intent == null && existingName.endsWith(".jsonl") && File(appDataDir, existingName).exists()) {
// START_STICKY restart by OS — keep appending to the same session file
pointsFile = File(appDataDir, existingName)
Log.d(TAG, "Service restarted by OS — resuming session file $existingName")
} else {
// Genuine start from JS — new session file
pointsFile = File(appDataDir, "gps_points_${System.currentTimeMillis()}.jsonl")
ptrFile.writeText(pointsFile!!.name)
}
// Raw sensor file (50Hz, ~5MB/hour) with rotation (keep last 15)
rawSensorFile = File(appDataDir, "sensor_raw_$sessionTs.jsonl")
rawSensorFile = File(appDataDir, "sensor_raw_${System.currentTimeMillis()}.jsonl")
// Rotate: delete oldest raw files if more than 15
try {
val rawFiles = appDataDir.listFiles { f -> f.name.startsWith("sensor_raw_") && f.name.endsWith(".jsonl") }