import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/ble_device.dart'; import '../models/bp_reading.dart'; import '../services/health_ble_service.dart'; import 'auth_provider.dart'; import 'data_providers.dart'; const _boundDevicesKey = 'bound_ble_devices'; const _readingFingerprintsKey = 'ble_reading_fingerprints'; const _legacyBpMacKey = 'bp_device_mac'; const _legacyBpNameKey = 'bp_device_name'; const _legacyBpLastSyncKey = 'bp_last_sync'; final healthBleServiceProvider = Provider((ref) { final service = HealthBleService(); ref.onDispose(service.dispose); return service; }); class DeviceBindState { final List devices; final bool isConnected; const DeviceBindState({this.devices = const [], this.isConnected = false}); bool get isBound => devices.isNotEmpty; DeviceBindState copyWith({List? devices, bool? isConnected}) { return DeviceBindState( devices: devices ?? this.devices, isConnected: isConnected ?? this.isConnected, ); } List devicesOfType(BleDeviceType type) { return devices.where((device) => device.type == type).toList(); } BoundBleDevice? findById(String id) { for (final device in devices) { if (device.id == id) return device; } return null; } } class DeviceBindNotifier extends Notifier { StreamSubscription? _connSub; @override DeviceBindState build() { ref.onDispose(() => _connSub?.cancel()); _loadBinding(); _listenConnection(); return const DeviceBindState(); } void _listenConnection() { _connSub?.cancel(); _connSub = ref.read(healthBleServiceProvider).connectionState.listen(( connected, ) { if (state.isConnected != connected) { state = state.copyWith(isConnected: connected); } }); } Future _loadBinding() async { final db = ref.read(localDbProvider); await _migrateLegacyBloodPressureDevice(); final raw = await db.read(_boundDevicesKey); final devices = _decodeDevices(raw); state = state.copyWith(devices: devices); } Future _migrateLegacyBloodPressureDevice() async { final db = ref.read(localDbProvider); final existing = await db.read(_boundDevicesKey); final legacyMac = await db.read(_legacyBpMacKey); if (existing != null || legacyMac == null || legacyMac.isEmpty) return; final legacyName = await db.read(_legacyBpNameKey); final migrated = BoundBleDevice( id: legacyMac, name: legacyName?.isNotEmpty == true ? legacyName! : '血压计', type: BleDeviceType.bloodPressure, serviceUuid: BleDeviceType.bloodPressure.serviceUuid, ); await db.write(_boundDevicesKey, jsonEncode([migrated.toJson()])); await db.delete(_legacyBpMacKey); await db.delete(_legacyBpNameKey); await db.delete(_legacyBpLastSyncKey); } List _decodeDevices(String? raw) { if (raw == null || raw.isEmpty) return []; try { final decoded = jsonDecode(raw); if (decoded is! List) return []; return decoded .whereType() .map( (item) => BoundBleDevice.fromJson(Map.from(item)), ) .whereType() .toList(); } catch (_) { return []; } } Future _saveDevices(List devices) async { final db = ref.read(localDbProvider); await db.write( _boundDevicesKey, jsonEncode(devices.map((device) => device.toJson()).toList()), ); state = state.copyWith(devices: devices); } Future bind(BoundBleDevice device) async { final devices = [...state.devices]; final index = devices.indexWhere((item) => item.id == device.id); if (index >= 0) { devices[index] = device.copyWith(lastSyncAt: devices[index].lastSyncAt); } else { devices.add(device); } await _saveDevices(devices); } Future unbind(String id) async { await _saveDevices( state.devices.where((device) => device.id != id).toList(), ); } Future recordSuccessfulSync({ required BoundBleDevice device, required BpReading reading, }) async { await _rememberReadingFingerprint(device, reading); final syncedAt = DateTime.now(); final devices = state.devices.map((item) { if (item.id != device.id) return item; return item.copyWith(lastSyncAt: syncedAt); }).toList(); await _saveDevices(devices); ref.invalidate(latestHealthProvider); } Future isDuplicateReading( BoundBleDevice device, BpReading reading, ) async { final fingerprints = await _loadFingerprints(); final now = DateTime.now(); final pruned = _pruneFingerprints(fingerprints, now); if (pruned.length != fingerprints.length) { await _saveFingerprints(pruned); } final key = _readingFingerprint(device, reading); final lastSeenRaw = pruned[key]; if (lastSeenRaw == null) return false; final lastSeen = DateTime.tryParse(lastSeenRaw)?.toLocal(); if (lastSeen == null) return false; if (reading.hasDeviceTimestamp) return true; return now.difference(lastSeen).inSeconds < 10; } Future _rememberReadingFingerprint( BoundBleDevice device, BpReading reading, ) async { final now = DateTime.now(); final fingerprints = _pruneFingerprints(await _loadFingerprints(), now); fingerprints[_readingFingerprint(device, reading)] = now .toUtc() .toIso8601String(); await _saveFingerprints(fingerprints); } Future> _loadFingerprints() async { final db = ref.read(localDbProvider); final raw = await db.read(_readingFingerprintsKey); if (raw == null || raw.isEmpty) return {}; try { final decoded = jsonDecode(raw); if (decoded is! Map) return {}; return decoded.map( (key, value) => MapEntry(key.toString(), value.toString()), ); } catch (_) { return {}; } } Future _saveFingerprints(Map fingerprints) async { final db = ref.read(localDbProvider); await db.write(_readingFingerprintsKey, jsonEncode(fingerprints)); } Map _pruneFingerprints( Map fingerprints, DateTime now, ) { final pruned = {}; for (final entry in fingerprints.entries) { final seenAt = DateTime.tryParse(entry.value)?.toLocal(); if (seenAt == null) continue; if (now.difference(seenAt).inHours <= 24) { pruned[entry.key] = entry.value; } } return pruned; } String _readingFingerprint(BoundBleDevice device, BpReading reading) { final timePart = reading.hasDeviceTimestamp ? reading.timestamp.toUtc().toIso8601String() : 'no-device-time'; return [ device.id, device.type.code, timePart, reading.systolic, reading.diastolic, reading.pulse ?? 0, ].join('|'); } } final omronDeviceProvider = NotifierProvider( DeviceBindNotifier.new, );