Prompt to enable Bluetooth/location at recording start + two 'don't ask' settings
User forgets to turn BT/GPS on. Modern Android won't let an app toggle them silently, so prompt and act: BT via the system enable dialog, location via the location-settings screen. - ensureSystemReady(needBt, needGps) runs before startRecording: BT off -> confirm -> ble_request_enable; location off -> confirm -> open settings. Gated by two settings (baldav_dontask_bt / _gps); declining proceeds. - GPS.isLocationEnabled polls a new Kotlin check (check_location writes location_state.json via LocationManager; read_state_file reads it); open_location_settings opens ACTION_LOCATION_SOURCE_SETTINGS. - Settings: 'Не спрашивать про Bluetooth' / '... геолокацию' checkboxes.
This commit is contained in:
parent
f7ba059628
commit
2d0e51bd8d
5 changed files with 104 additions and 1 deletions
30
docs/session-logs/2026-10-17_bt-gps-prompts.md
Normal file
30
docs/session-logs/2026-10-17_bt-gps-prompts.md
Normal file
|
|
@ -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 / Открыть настройки гео); отказ -> запись идёт как есть; включить чекбоксы "не
|
||||
спрашивать" -> попапы не показываются.
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</div>
|
||||
</div>
|
||||
<div id="s-ble-results" style="padding:0 14px"></div>
|
||||
<label class="settings-item" style="cursor:pointer">
|
||||
<div class="settings-item-left"><div class="settings-item-title">Не спрашивать про Bluetooth</div>
|
||||
<div class="settings-item-desc">Не предлагать включить BT при старте записи</div></div>
|
||||
<input id="s-dontask-bt" type="checkbox" ${localStorage.getItem("baldav_dontask_bt") === "1" ? "checked" : ""} style="width:20px;height:20px;">
|
||||
</label>
|
||||
<label class="settings-item" style="cursor:pointer">
|
||||
<div class="settings-item-left"><div class="settings-item-title">Не спрашивать про геолокацию</div>
|
||||
<div class="settings-item-desc">Не предлагать включить GPS при старте записи трека</div></div>
|
||||
<input id="s-dontask-gps" type="checkbox" ${localStorage.getItem("baldav_dontask_gps") === "1" ? "checked" : ""} style="width:20px;height:20px;">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,6 +228,14 @@ fn read_points_file(app: tauri::AppHandle, filename: String) -> Result<String, S
|
|||
}
|
||||
}
|
||||
|
||||
// Read a small app state file (allowlisted) written by Kotlin, e.g. the location on/off state.
|
||||
#[tauri::command]
|
||||
fn read_state_file(app: tauri::AppHandle, name: String) -> Result<String, String> {
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue