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:
Janez T
2025-10-15 19:20:46 +02:00
parent 3f03b2e789
commit 91d9326804
17 changed files with 501 additions and 255 deletions

View 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;
}
}
}

View File

@@ -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.');