HR connect: one BT-enable prompt per start, only on user action

setHrOn gains an 'interactive' flag; it pops the OS enable-Bluetooth dialog
only on a user tap (record-screen toggle). Auto-calls at recording start no
longer re-request enable after the single ensureSystemReady prompt, so a
decline is not immediately followed by the system dialog and is not re-asked
until the next start.
This commit is contained in:
4aiman 2026-10-19 13:43:55 +03:00
commit eeca9a7d94
3 changed files with 69 additions and 4 deletions

View file

@ -0,0 +1,54 @@
# Сессия 2026-10-19 - краш при выключении Bluetooth + один запрос на включение вместо нескольких
## Контекст
Пользователь: "при выключении блютуса балдав падает", и попапы про включение BT лезут
повторно ("если запретил - не просить до нового трекинга; просить ОДИН раз, а не 20").
Действовали по утверждённому плану `~/.claude/plans/merry-discovering-dream.md` (финальный
вариант; сохранён в docs/plans не был - план короткий, воспроизведён в этом логе).
## Разбор (две независимые причины)
1. Краш. Крэш-буфер телефона: нативный abort libc FORTIFY "pthread_mutex_lock called on a
destroyed mutex" (pid 19579), не Kotlin-исключение. Причина: живой GATT/scanner трогают
уже после выключения BT-адаптера. В `BleHrService.kt` не было приёмника состояния BT, а
отложенные `scanner.stopScan`, fitpro-ретриггер и периодические хендлеры продолжали дёргать
GATT. Точный фрейм без tombstone не прибивался - фикс сделан defense-in-depth (решение
пользователя: сразу защита + проверка воспроизведением).
2. Двойной запрос. `ensureSystemReady` (main.js) спрашивает один раз через appConfirm, но
`startRecording` авто-зовёт `setHrOn(true)`, а тот при BT off САМ дёргал нативный
`ble_request_enable` (gps.js:899), игнорируя отказ -> второе (системное) окно сразу после
отказа, и так каждый старт.
## Сделано (2 коммита)
Коммит 1 (краш, Kotlin `BleHrService.kt`):
- Единый `handler = Handler(Looper.getMainLooper())`; все postDelayed через него;
`handler.removeCallbacksAndMessages(null)` в `disconnect()` и `onDestroy()`.
- `BroadcastReceiver` на `BluetoothAdapter.ACTION_STATE_CHANGED`: на STATE_TURNING_OFF/OFF -
снять хендлеры, disconnect, `writeData(status:bt_off)`, stopSelf. Снятие в onDestroy в try.
- try/catch на всех нативных BLE-вызовах: startScan/stopScan/connectGatt/discoverServices/
writeCharacteristic/setCharacteristicNotification/writeDescriptor; directConnect проверяет
adapter.isEnabled.
Коммит 2 (попапы, JS):
- `GPS.setHrOn(on, interactive = false)` (gps.js): системное окно `ble_request_enable` только
при `interactive === true`. Авто-режим при BT off: hrOn=false, не подключаться, без окна.
- Авто-вызовы на старте (gps.js 1433 зарядка, 1462 трек) остаются `setHrOn(true)` (без
interactive) -> единственный авто-запрос при старте = один appConfirm в ensureSystemReady.
- Ручной тоггл ЧСС (main.js:5148) -> `setHrOn(..., true)`: одно системное окно на тап.
Ручное "Подключить" (настройки) не трогали - оно и так user-initiated.
## Проверка
Синтаксис JS ок (node -c обоих файлов). На телефоне (сборка `./build.sh -i -d android`):
1) ЧСС подключён, выключить BT в записи -> app НЕ падает, ЧСС гаснет, трек идёт; в logcat
`BleHR` виден STATE_OFF и чистый stop, нет нового FATAL/FORTIFY.
2) BT off, старт трека -> ровно одно окно "Включить Bluetooth?"; отказ -> системное окно НЕ
появляется, до конца записи повторов нет; новый старт -> снова один вопрос.
3) Тап по тогглу ЧСС при BT off -> одно системное окно.
4) Регрессии рядом: зарядка с ЧСС считает kcal; "Не спрашивать" гасят свои запросы;
"Подключить" в настройках работает на ходу.
## Отдельно (read-only, без правок)
Разобрал `wind_marks` в 5 треках из 228: отметки встречного ветра нигде не используются
(коммент в коде - "ground truth для оценки ветра из трека", которой в коде нет), и
эмпирически сигнал противоречив (в 2 из 4 велотреков "в ветер" скорость даже выше). Вывод:
пользы нет; кнопки ветра уже нет, новые треки пишут пустой wind_marks, помех нет - оставили
как есть.