import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:location_service/location_service.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../core/utils/logger.dart'; import '../../devices/device_hub.dart'; import '../../devices/device_vendors.dart'; /// 给百炼会话提供「用户在哪、现在几点」(2026-09-25,车载香薰)。 /// /// 车载助手的提示词里有「系统条件」:`${location}`(用户当前位置)、`${date}`(今天的日期)。 /// 以前一个都没传,AI 问天气只能反问「你要查哪里」。现在两条通道一起给 /// (见 [BailianMultimodalService.start]): /// - `client_info.location` = {latitude, longitude, city_name}(lumi 也是这么传经纬度的); /// - `biz_params.user_prompt_params` = {location: 省市区, date: 年月日 星期 时:分}。 /// /// 节奏照 lumi 的 LocationService:启动 / 回到前台时刷新,10 分钟内不重复,结果落盘; /// **建会话时只读缓存,从不等定位**,所以不拖慢唤醒。 /// /// 权限:车载香薰第一次连上(且 App 在前台)时申请一次「使用期间」定位权限, /// 用户拒绝后不再追问——没有权限就只是不带位置。 class DeviceLocationService extends GetxService with WidgetsBindingObserver { static const String _tag = 'DeviceLocation'; static DeviceLocationService? get maybe => Get.isRegistered() ? Get.find() : null; static const String _cacheKey = 'device_location_cache'; static const String _askedKey = 'device_location_perm_asked'; static const Duration _minRefreshInterval = Duration(minutes: 10); final GetStorage _storage = GetStorage(); Map? _place; DateTime? _lastRefreshAt; bool _refreshing = false; Worker? _hubWorker; AppLifecycleState? _lifecycle = AppLifecycleState.resumed; @override void onInit() { super.onInit(); WidgetsBinding.instance.addObserver(this); try { final c = _storage.read(_cacheKey); if (c != null) _place = c.map((k, v) => MapEntry('$k', v)); } catch (_) {} } @override void onReady() { super.onReady(); unawaited(_logReady()); refresh(); // DeviceHub 在 InitialBinding 里晚于本服务注册,所以到 onReady 再盯; // 香薰可能在这之前就已经回连上了,所以先按当前列表判一次 if (Get.isRegistered()) { _checkSessions(DeviceHub.to.sessions); _hubWorker = ever(DeviceHub.to.sessions, _checkSessions); } else { Logger.w(_tag, 'DeviceHub 未注册,连上香薰时不会申请定位权限'); } } void _checkSessions(List list) { if (list.any((s) => s.vendor == DeviceVendors.eaicar)) { unawaited(askPermissionOnce()); } } Future _logReady() async { try { final st = await Permission.locationWhenInUse.status; Logger.i(_tag, '位置服务就绪(定位权限=$st,缓存位置=${placeText()})'); } catch (_) {} } @override void onClose() { WidgetsBinding.instance.removeObserver(this); _hubWorker?.dispose(); super.onClose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { _lifecycle = state; if (state == AppLifecycleState.resumed) refresh(); } /// 车载香薰第一次连上时申请一次定位权限。只问一次,拒绝后不再打扰。 Future askPermissionOnce() async { if (_storage.read(_askedKey) == true) return; // 权限框要在前台弹;在后台(被动回连)时先不问,下次前台连上再说 if (_lifecycle != AppLifecycleState.resumed) { Logger.i(_tag, 'App 不在前台($_lifecycle),暂不申请定位权限'); return; } await _storage.write(_askedKey, true); try { final st = await Permission.locationWhenInUse.request(); Logger.i(_tag, '车载香薰首次连接,申请定位权限 → $st'); if (st.isGranted || st.isLimited) await refresh(force: true); } catch (e) { Logger.w(_tag, '申请定位权限失败: $e'); } } /// 后台刷新一次位置(不弹权限框、不抛异常)。 Future refresh({bool force = false}) async { if (_refreshing) return; final last = _lastRefreshAt; if (!force && last != null && DateTime.now().difference(last) < _minRefreshInterval) { return; } _refreshing = true; try { final st = await Permission.locationWhenInUse.status; if (!(st.isGranted || st.isLimited)) { Logger.i(_tag, '没有定位权限($st),不取位置'); return; } final r = await LocationService().currentPlace(); if (r['error'] != null) { Logger.w(_tag, '定位未成功: ${r['error']}(沿用缓存: ${placeText()})'); return; } _place = r; _lastRefreshAt = DateTime.now(); await _storage.write(_cacheKey, r); Logger.i(_tag, '位置已更新: ${placeText()}(原始: $r)'); } catch (e) { Logger.w(_tag, '刷新位置失败: $e'); } finally { _refreshing = false; } } String _s(String k) => (_place?[k] ?? '').toString().trim(); /// `${location}` 的取值:省市区(去掉重复的直辖市名),拿不到就是「未知」。 String placeText() { final parts = []; for (final k in ['province', 'city', 'district']) { final v = _s(k); if (v.isNotEmpty && !parts.contains(v)) parts.add(v); } if (parts.isEmpty) { final addr = _s('address'); if (addr.isNotEmpty) return addr; // 反查不到地名时用坐标描述,模型/插件还能据此判断城市,比「未知」强 final lat = _place?['latitude']; final lng = _place?['longitude']; if (lat is num && lng is num) { return '经纬度 ${lat.toStringAsFixed(4)},${lng.toStringAsFixed(4)}'; } return '未知'; } return parts.join(); } /// `client_info.location`:没有经纬度时返回 null(整块不发)。 Map? clientLocation() { final lat = _place?['latitude']; final lng = _place?['longitude']; if (lat is! num || lng is! num) return null; final city = _s('city'); return { 'latitude': lat.toStringAsFixed(6), 'longitude': lng.toStringAsFixed(6), if (city.isNotEmpty) 'city_name': city, }; } /// `${date}` 的取值:手机本地时间,形如「2026年9月25日 星期四 21:30」。 static String dateText([DateTime? now]) { final t = now ?? DateTime.now(); const week = ['一', '二', '三', '四', '五', '六', '日']; String two(int v) => v.toString().padLeft(2, '0'); return '${t.year}年${t.month}月${t.day}日 星期${week[t.weekday - 1]} ' '${two(t.hour)}:${two(t.minute)}'; } }