mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Refactor contacts management and improve path handling
- Updated ContactsProvider to initialize with device public key for filtering out self contacts. - Added methods to check if a contact has a learned routing path and to get path quality indicators. - Enhanced AppProvider to initialize ContactsProvider with device public key. - Modified ConnectionProvider to log path status when sending messages. - Created ContactStorageService for persisting contacts to local storage. - Removed import/export functionality from MapManagementScreen. - Updated UI components in DirectMessageSheet and SarUpdateSheet to use theme colors. - Removed file_picker dependency from pubspec.yaml and generated plugin registrant.
This commit is contained in:
210
lib/services/contact_storage_service.dart
Normal file
210
lib/services/contact_storage_service.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/contact_telemetry.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting contacts to local storage
|
||||
class ContactStorageService {
|
||||
static const String _contactsKey = 'stored_contacts';
|
||||
static const int _maxStoredContacts = 500; // Store up to 500 contacts
|
||||
|
||||
/// Save contacts to persistent storage
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Convert contacts to JSON
|
||||
final jsonList = contacts.map((contact) => _contactToJson(contact)).toList();
|
||||
|
||||
// Limit to max stored contacts (keep most recent)
|
||||
final limitedList = jsonList.length > _maxStoredContacts
|
||||
? jsonList.sublist(jsonList.length - _maxStoredContacts)
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_contactsKey, jsonString);
|
||||
|
||||
print('✅ [ContactStorage] Saved ${limitedList.length} contacts to storage');
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error saving contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load contacts from persistent storage
|
||||
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
|
||||
Future<List<Contact>> loadContacts({Uint8List? excludePublicKey}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
print('ℹ️ [ContactStorage] No stored contacts found');
|
||||
return [];
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
final contacts = jsonList
|
||||
.map((json) => _contactFromJson(json as Map<String, dynamic>))
|
||||
.where((contact) => contact != null)
|
||||
.cast<Contact>()
|
||||
.toList();
|
||||
|
||||
// Filter out contacts with the excluded public key
|
||||
final filteredContacts = excludePublicKey != null
|
||||
? contacts.where((contact) {
|
||||
final matches = _publicKeysMatch(contact.publicKey, excludePublicKey);
|
||||
if (matches) {
|
||||
print('ℹ️ [ContactStorage] Excluding contact with matching public key: ${contact.advName}');
|
||||
}
|
||||
return !matches;
|
||||
}).toList()
|
||||
: contacts;
|
||||
|
||||
print('✅ [ContactStorage] Loaded ${filteredContacts.length} contacts from storage'
|
||||
'${excludePublicKey != null ? ' (${contacts.length - filteredContacts.length} excluded)' : ''}');
|
||||
return filteredContacts;
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error loading contacts: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare two public keys for equality
|
||||
bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
|
||||
if (key1.length != key2.length) return false;
|
||||
for (int i = 0; i < key1.length; i++) {
|
||||
if (key1[i] != key2[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Clear all stored contacts
|
||||
Future<void> clearContacts() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_contactsKey);
|
||||
print('✅ [ContactStorage] Cleared all stored contacts');
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error clearing contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {
|
||||
'contactCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
}
|
||||
|
||||
final sizeBytes = jsonString.length;
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
|
||||
return {
|
||||
'contactCount': jsonList.length,
|
||||
'storageSizeBytes': sizeBytes,
|
||||
'storageSizeKB': (sizeBytes / 1024).toStringAsFixed(2),
|
||||
};
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error getting storage stats: $e');
|
||||
return {
|
||||
'contactCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Contact to JSON
|
||||
Map<String, dynamic> _contactToJson(Contact contact) {
|
||||
return {
|
||||
'publicKey': base64Encode(contact.publicKey),
|
||||
'type': contact.type.value,
|
||||
'flags': contact.flags,
|
||||
'outPathLen': contact.outPathLen,
|
||||
'outPath': base64Encode(contact.outPath),
|
||||
'advName': contact.advName,
|
||||
'lastAdvert': contact.lastAdvert,
|
||||
'advLat': contact.advLat,
|
||||
'advLon': contact.advLon,
|
||||
'lastMod': contact.lastMod,
|
||||
'telemetry': contact.telemetry != null ? _telemetryToJson(contact.telemetry!) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert JSON to Contact
|
||||
Contact? _contactFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
type: ContactType.fromValue(json['type'] as int),
|
||||
flags: json['flags'] as int,
|
||||
outPathLen: json['outPathLen'] as int,
|
||||
outPath: Uint8List.fromList(base64Decode(json['outPath'] as String)),
|
||||
advName: json['advName'] as String,
|
||||
lastAdvert: json['lastAdvert'] as int,
|
||||
advLat: json['advLat'] as int,
|
||||
advLon: json['advLon'] as int,
|
||||
lastMod: json['lastMod'] as int,
|
||||
telemetry: json['telemetry'] != null
|
||||
? _telemetryFromJson(json['telemetry'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error parsing contact from JSON: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert ContactTelemetry to JSON
|
||||
Map<String, dynamic> _telemetryToJson(ContactTelemetry telemetry) {
|
||||
return {
|
||||
'gpsLocation': telemetry.gpsLocation != null
|
||||
? {
|
||||
'latitude': telemetry.gpsLocation!.latitude,
|
||||
'longitude': telemetry.gpsLocation!.longitude,
|
||||
}
|
||||
: null,
|
||||
'batteryPercentage': telemetry.batteryPercentage,
|
||||
'batteryMilliVolts': telemetry.batteryMilliVolts,
|
||||
'temperature': telemetry.temperature,
|
||||
'humidity': telemetry.humidity,
|
||||
'pressure': telemetry.pressure,
|
||||
'timestampMillis': telemetry.timestamp.millisecondsSinceEpoch,
|
||||
'extraSensorData': telemetry.extraSensorData,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert JSON to ContactTelemetry
|
||||
ContactTelemetry? _telemetryFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return ContactTelemetry(
|
||||
gpsLocation: json['gpsLocation'] != null
|
||||
? LatLng(
|
||||
json['gpsLocation']['latitude'] as double,
|
||||
json['gpsLocation']['longitude'] as double,
|
||||
)
|
||||
: null,
|
||||
batteryPercentage: json['batteryPercentage'] as double?,
|
||||
batteryMilliVolts: json['batteryMilliVolts'] as double?,
|
||||
temperature: json['temperature'] as double?,
|
||||
humidity: json['humidity'] as double?,
|
||||
pressure: json['pressure'] as double?,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['timestampMillis'] as int),
|
||||
extraSensorData: json['extraSensorData'] as Map<String, dynamic>?,
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ [ContactStorage] Error parsing telemetry from JSON: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
|
||||
import 'package:flutter_map_tile_caching/custom_backend_api.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../models/map_layer.dart';
|
||||
|
||||
class TileCacheService {
|
||||
@@ -124,42 +122,6 @@ class TileCacheService {
|
||||
return stats / (1024 * 1024);
|
||||
}
|
||||
|
||||
Future<String> exportCache() async {
|
||||
if (!_isInitialized) {
|
||||
throw StateError('TileCacheService not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final exportPath = '${directory.path}/meshcore_maps_$timestamp.fmtc';
|
||||
|
||||
// Export using FMTCBackendAccess
|
||||
await FMTCBackendAccess.internal.exportStores(
|
||||
storeNames: [_storeName],
|
||||
path: exportPath,
|
||||
);
|
||||
|
||||
return exportPath;
|
||||
}
|
||||
|
||||
Future<void> importCache(String filePath) async {
|
||||
if (!_isInitialized) {
|
||||
throw StateError('TileCacheService not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
throw Exception('Import file not found: $filePath');
|
||||
}
|
||||
|
||||
// Import using FMTCBackendAccess
|
||||
await FMTCBackendAccess.internal.importStores(
|
||||
storeNames: [_storeName],
|
||||
path: filePath,
|
||||
strategy: ImportConflictStrategy.rename,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> getAvailableStores() async {
|
||||
if (!_isInitialized) {
|
||||
throw StateError('TileCacheService not initialized. Call initialize() first.');
|
||||
|
||||
Reference in New Issue
Block a user