diff --git a/docs/session-logs/2026-10-17_bt-gps-prompts.md b/docs/session-logs/2026-10-17_bt-gps-prompts.md new file mode 100644 index 0000000..6167196 --- /dev/null +++ b/docs/session-logs/2026-10-17_bt-gps-prompts.md @@ -0,0 +1,30 @@ +# Сессия 2026-10-17 (2) - попап "включи BT/гео" при старте записи + 2 настройки "не спрашивать" + +## Задача +Пользователь забывает включать Bluetooth и GPS. Нужно: при старте записи, если выключено - +предложить включить; дать отказаться; 2 отдельные настройки "не спрашивать". + +Факт-чек: телефон С Google-сервисами (ранее ошибочно решил, что без). Тихо включать BT/гео +приложение на target 36 не может (ОС), поэтому - попап + действие: BT через системный диалог +включения, гео через экран настроек геолокации. + +## Сделано +- Rust `read_state_file(name)` (allowlist "location_state.json") - читает ответ Kotlin из + native_root. Зарегистрирован. +- Kotlin MainActivity: команды `check_location` (пишет `location_state.json` {enabled} через + LocationManager.isLocationEnabled) и `open_location_settings` (ACTION_LOCATION_SOURCE_SETTINGS). +- `gps.js` `GPS.isLocationEnabled()` - дёргает check_location, читает state-файл; true/false или + null (неизвестно - тогда не спрашивать). +- `main.js` `ensureSystemReady(needBt, needGps)` - перед startRecording: BT off -> + appConfirm "Включить?" -> `ble_request_enable`; гео off -> appConfirm "Открыть настройки?" -> + `open_location_settings`. Гейтится настройками `baldav_dontask_bt` / `baldav_dontask_gps`. + Вызов в кнопке записи: needBt = есть привязанный датчик, needGps = не зарядка. +- Настройки (раздел Пульсометр): 2 чекбокса "Не спрашивать про Bluetooth" / "...геолокацию". + +Ограничение (честно): авто-ВЫКЛ BT/гео после тренировки приложение сделать не может (ОС). +Тихое включение - тоже; поэтому попап + один тап. + +## Проверка +Синтаксис JS ок. На телефоне: выключить BT и гео, старт записи трека -> два попапа по очереди +(Включить BT / Открыть настройки гео); отказ -> запись идёт как есть; включить чекбоксы "не +спрашивать" -> попапы не показываются. diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 81641f6..e86c11e 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -822,6 +822,19 @@ GPS.requestBackgroundLocation = async function() { } catch (e) { console.log("[GPS] reqbgloc:", e); } }; +// Is the OS location toggle ON (not just the permission)? Kotlin checks LocationManager and +// writes location_state.json; we read it back. Returns true/false, or null if undeterminable +// (desktop / no answer) — callers must not prompt on null. +GPS.isLocationEnabled = async function() { + try { + await window.__TAURI__.core.invoke("gps_service_command", { cmd: "check_location" }); + await new Promise(r => setTimeout(r, 300)); + let raw = await window.__TAURI__.core.invoke("read_state_file", { name: "location_state.json" }); + if (!raw) return null; + return !!JSON.parse(raw).enabled; + } catch { return null; } +}; + // BLE HR monitor — tied to GPS session (start→stop), but NOT paused when GPS pauses. // HR keeps measuring even when rider is stationary. GPS._startHrMonitor = async function() { diff --git a/frontend/js/main.js b/frontend/js/main.js index caca390..2e0d2b8 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1182,6 +1182,26 @@ function setBtnProgress(el, pct) { el.style.setProperty("--pct", Math.max(0, Math.min(100, Math.round(pct))) + "%"); } +// Before a recording, nudge the user to turn on what it needs: Bluetooth (for the HR strap) and +// location (for the track). Can't silently toggle them on modern Android, so prompt + act: BT via +// the system enable dialog, location via the location-settings screen. Declining is fine, and the +// two "Не спрашивать" settings suppress each prompt. Never prompts when the state is unknown. +async function ensureSystemReady(needBt, needGps) { + let inv = window.__TAURI__.core.invoke; + if (needBt && localStorage.getItem("baldav_dontask_bt") !== "1") { + let bt = "on"; try { bt = await inv("ble_check_bluetooth"); } catch {} + if (bt === "off" && await appConfirm("Для пульсометра нужен Bluetooth. Включить?", "Включить")) { + try { await inv("ble_request_enable"); } catch {} + } + } + if (needGps && localStorage.getItem("baldav_dontask_gps") !== "1") { + let locOn = await GPS.isLocationEnabled(); + if (locOn === false && await appConfirm("Для записи трека нужна геолокация. Открыть настройки, чтобы включить?", "Открыть")) { + try { await inv("gps_service_command", { cmd: "open_location_settings" }); } catch {} + } + } +} + async function reopenDetail(sessionId) { let count = 0; while (winStack.length > 0) { @@ -2846,6 +2866,10 @@ async function renderGpsScreen(container) { GPS.recording.gearCount = bike.gear_count; } } + // Nudge BT (for the strap) / location (for the track) on if off. Non-blocking to the user's + // choice: they can decline, and the two "Не спрашивать" settings suppress each prompt. + await ensureSystemReady(!!state.profile?.ble_device_addr, type !== "workout"); + // Workout (HR-only): pass the profile so finish can compute kcal from HR (Keytel). let hrProfile = type === "workout" ? { weightKg: wkg, @@ -7330,6 +7354,16 @@ async function renderSettings(container) {
+ +
@@ -7764,6 +7798,11 @@ async function renderSettings(container) { bleConnectBtn.disabled = false; bleConnectBtn.textContent = "Подключить"; }; + let dontAskBt = container.querySelector("#s-dontask-bt"); + if (dontAskBt) dontAskBt.onchange = () => { try { localStorage.setItem("baldav_dontask_bt", dontAskBt.checked ? "1" : "0"); } catch {} }; + let dontAskGps = container.querySelector("#s-dontask-gps"); + if (dontAskGps) dontAskGps.onchange = () => { try { localStorage.setItem("baldav_dontask_gps", dontAskGps.checked ? "1" : "0"); } catch {} }; + let bleScanBtn = container.querySelector("#s-ble-scan"); let bleResults = container.querySelector("#s-ble-results"); if (bleScanBtn) { diff --git a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt index d22b344..caf488c 100644 --- a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt @@ -514,6 +514,19 @@ class MainActivity : TauriActivity() { catch (e: Exception) { Log.e("BalDav", "BT enable request failed: ${e.message}") } } } + cmd == "check_location" -> { + val lm = getSystemService(LOCATION_SERVICE) as? LocationManager + val enabled = when { + lm == null -> false + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> lm.isLocationEnabled + else -> lm.isProviderEnabled(LocationManager.GPS_PROVIDER) || lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + File(appDataDir, "location_state.json").writeText("""{"enabled":$enabled}""") + } + cmd == "open_location_settings" -> { + try { startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) } + catch (e: Exception) { Log.e("BalDav", "open location settings: ${e.message}") } + } cmd == "check_update" -> { checkForUpdate(appDataDir) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index be7794b..7a8be8c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -228,6 +228,14 @@ fn read_points_file(app: tauri::AppHandle, filename: String) -> Result Result { + if name != "location_state.json" { return Err("bad name".to_string()); } + let p = native_root(&app).join(&name); + if p.exists() { std::fs::read_to_string(&p).map_err(|e| e.to_string()) } else { Ok(String::new()) } +} + // Download binary file from URL to local path (for tile DB import from router) // read_to_vec() has a 10MB limit, so use as_reader() for unlimited binary reads #[tauri::command] @@ -1154,7 +1162,7 @@ pub fn run() { .add_migrations("sqlite:baldav.db", migrations) .build(), ) - .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr, ble::ble_request_enable]) + .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr, ble::ble_request_enable, read_state_file]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }