Files
ttstd_family_care/lib/features/home/data/location_controller.dart
TongTongStudio 06cdc77172 feat(android): 支持沉浸式状态栏、地图定位与自定义签名
- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配
- 新增百度地图权限、AK 配置及定位权限说明
- 接入 MethodChannel 实现返回键退后台热启动优化
- 配置自定义签名并更新构建逻辑
- 完善 token 刷新与鉴权失效统一处理
- 新增地图页与截图页路由
- 修复返回键拦截导致的 PopScope 失效问题
2026-08-19 03:43:12 +08:00

226 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'package:flutter_bmflocation/flutter_bmflocation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../app/constants/app_constants.dart';
/// 家属端手机本机定位结果。
///
/// 来自百度定位插件 `flutter_bmflocation`,坐标为百度 BD09LL
/// (与地图 SDK 坐标系一致,可直接用于 `BMFMapController.updateLocationData`)。
class DeviceCurrentLocation {
const DeviceCurrentLocation({
required this.latitude,
required this.longitude,
this.radius,
this.address,
this.altitude,
this.speed,
this.course,
});
final double latitude;
final double longitude;
/// 定位精度Android 返回)。
final double? radius;
/// 反地理编码地址。
final String? address;
/// 海拔(米)。
final double? altitude;
/// 速度m/s
final double? speed;
/// 方向角(度,正北为 0顺时针
final double? course;
factory DeviceCurrentLocation.fromBaidu(BaiduLocation src) =>
DeviceCurrentLocation(
latitude: src.latitude ?? 0,
longitude: src.longitude ?? 0,
radius: src.radius,
address: src.address,
altitude: src.altitude,
speed: src.speed,
course: src.course,
);
}
/// 本机定位状态。
sealed class LocationState {
const LocationState();
}
/// 定位初始化中 / 尚未定位成功。
class LocationIdle extends LocationState {
const LocationIdle();
}
/// 定位成功。
class LocationSuccess extends LocationState {
const LocationSuccess(this.location);
final DeviceCurrentLocation location;
}
/// 定位失败。
class LocationFailed extends LocationState {
const LocationFailed(this.message);
final String message;
}
/// 家属端手机本机定位控制器。
///
/// 封装百度定位插件 `LocationFlutterPlugin`
/// - 构建时先请求运行时定位权限Android 6.0+ / iOS 动态授权),
/// 授权成功后才初始化并启动连续定位。
/// - 坐标类型设为 BD09LL与地图 SDK 一致),无需二次坐标转换。
/// - 全流程输出 [debugPrint] 日志,便于排查定位失败原因。
/// - Provider 被 dispose如地图页退出时自动停止定位并释放插件。
class LocationController extends Notifier<LocationState> {
LocationFlutterPlugin? _plugin;
bool _started = false;
@override
LocationState build() {
_started = false;
_plugin = null;
// Provider 被首次 watch 时自动启动本机定位。
_init();
// Provider 被 dispose如地图页退出时自动停止定位并释放插件。
ref.onDispose(_teardown);
return const LocationIdle();
}
/// 初始化:请求定位权限 -> 初始化定位插件 -> 启动连续定位。
Future<void> _init() async {
if (_started) return;
_started = true;
// 1. 请求运行时定位权限。
final granted = await _ensureLocationPermission();
if (!granted) {
state = const LocationFailed('定位权限被拒绝,请到系统设置开启');
return;
}
// 2. 初始化定位插件。
final plugin = _plugin ??= LocationFlutterPlugin();
// 同意隐私(百度定位 SDK 要求)。
// ⚠️ 该插件 Android 原生端对 setAgreePrivacy 从不返回 MethodChannel 结果
// onMethodCall 里仅调用 LocationClient.setAgreePrivacy未调用 result.success
// 且 dispatchMethodHandler 的 switch 对该 method 落入 default永不回调
// 直接 await 会永久挂起,导致后续 prepareLoc/startLocation 不执行、定位无输出。
// 故加超时保护:超时视为已同意,继续走定位流程。
try {
await plugin.setAgreePrivacy(true).timeout(
const Duration(seconds: 2),
onTimeout: () => false,
);
} catch (_) {}
debugPrint('[Location] setAgreePrivacy done');
// authAK 仅对 iOS/鸿蒙生效Android 复用 Manifest 中的 AK不调用。
if (!kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS)) {
final authed = await plugin.authAK(AppConstants.kBaiduMapAk);
debugPrint('[Location] authAK(${AppConstants.kBaiduMapAk}) => $authed');
if (!_started) return;
} else {
debugPrint('[Location] 跳过 authAKAndroid 使用 Manifest AK');
}
// 3. 注册连续定位回调,把最新结果写入 state。
plugin.seriesLocationCallback(callback: (result) {
_handleLocationResult(result);
});
// 4. 设置 Android / iOS 定位参数BD09LL、高精度、需要地址信息。
final androidMap = BaiduLocationAndroidOption(
coordType: BMFLocationCoordType.bd09ll,
locationMode: BMFLocationMode.hightAccuracy,
isNeedAddress: true,
isNeedLocationDescribe: true,
scanspan: 1000,
).getMap();
final iosMap = BaiduLocationIOSOption(
coordType: BMFLocationCoordType.bd09ll,
desiredAccuracy: BMFDesiredAccuracy.best,
isNeedNewVersionRgc: true,
).getMap();
final prepared = await plugin.prepareLoc(androidMap, iosMap, const <String, Object?>{});
debugPrint('[Location] prepareLoc => $prepared');
if (!_started) return;
final started = await plugin.startLocation();
debugPrint('[Location] startLocation => $started');
}
/// 请求运行时定位权限WhenInUse 足够前台展示地图定位点)。
Future<bool> _ensureLocationPermission() async {
PermissionStatus status = await Permission.locationWhenInUse.status;
debugPrint('[Location] permission.status => ${status.name}');
if (status.isGranted) {
return true;
}
if (status.isPermanentlyDenied || status.isRestricted) {
debugPrint('[Location] permission permanentlyDenied/restricted需到系统设置开启');
// 打开应用设置引导用户手动开启。
await openAppSettings();
return false;
}
status = await Permission.locationWhenInUse.request();
debugPrint('[Location] permission.request => ${status.name}');
return status.isGranted;
}
/// 处理定位回调结果。
void _handleLocationResult(BaiduLocation result) {
final lat = result.latitude;
final lon = result.longitude;
debugPrint('[Location] onResult latitude=$lat longitude=$lon '
'radius=${result.radius} address=${result.address} '
'detail=${result.locationDetail} errorInfo=${result.errorInfo} '
'errorCode=${result.errorCode} locType=${result.locType}');
if (lat == null || lon == null) {
state = LocationFailed(result.errorInfo ?? '定位失败');
return;
}
state = LocationSuccess(DeviceCurrentLocation.fromBaidu(result));
// 定位已有有效结果且非错误,主动停止连续定位,避免常驻耗电。
stop();
}
/// 停止连续定位(保留插件实例,可再次 [startLocation])。
///
/// 本机定位拿到首个有效结果后即停止,符合「有结果且非定位错误就停止定位」的约束。
void stop() {
if (!_started) return;
debugPrint('[Location] stop: stopLocation');
_started = false;
_plugin?.stopLocation();
}
/// 停止定位并释放插件。
void _teardown() {
if (!_started) return;
debugPrint('[Location] teardown: stopLocation');
_started = false;
_plugin?.stopLocation();
_plugin = null;
}
}
/// 家属端手机本机定位 Provider。
///
/// 页面级使用:地图页 watch 时自动启动定位,页面销毁时自动停止,
/// 避免常驻定位导致电量与资源浪费(符合项目 keepAlive 约束,不设 keepAlive
final locationControllerProvider =
NotifierProvider<LocationController, LocationState>(
LocationController.new,
);