mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
- 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.
256 lines
8.0 KiB
Dart
256 lines
8.0 KiB
Dart
import 'dart:typed_data';
|
||
import 'package:flutter/foundation.dart';
|
||
import '../models/contact.dart';
|
||
import '../services/cayenne_lpp_parser.dart';
|
||
import '../services/contact_storage_service.dart';
|
||
|
||
/// Contacts Provider - manages contact list and telemetry
|
||
class ContactsProvider with ChangeNotifier {
|
||
final Map<String, Contact> _contacts = {};
|
||
final ContactStorageService _storageService = ContactStorageService();
|
||
bool _isInitialized = false;
|
||
|
||
// Add default public channel on initialization
|
||
ContactsProvider() {
|
||
_ensurePublicChannelExists();
|
||
}
|
||
|
||
bool get isInitialized => _isInitialized;
|
||
|
||
/// Initialize and load persisted contacts
|
||
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
||
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
||
if (_isInitialized) return;
|
||
|
||
try {
|
||
print('📦 [ContactsProvider] Loading persisted contacts...');
|
||
final storedContacts = await _storageService.loadContacts(
|
||
excludePublicKey: devicePublicKey,
|
||
);
|
||
|
||
// Add stored contacts
|
||
for (final contact in storedContacts) {
|
||
_contacts[contact.publicKeyHex] = contact;
|
||
}
|
||
|
||
_isInitialized = true;
|
||
print('✅ [ContactsProvider] Loaded ${storedContacts.length} persisted contacts');
|
||
|
||
// Ensure public channel exists after loading
|
||
_ensurePublicChannelExists();
|
||
|
||
notifyListeners();
|
||
} catch (e) {
|
||
print('❌ [ContactsProvider] Error initializing: $e');
|
||
_isInitialized = true; // Mark as initialized even on error
|
||
_ensurePublicChannelExists();
|
||
}
|
||
}
|
||
|
||
/// Ensure public channel always exists in the list
|
||
void _ensurePublicChannelExists() {
|
||
const publicChannelKey = 'public_channel_0';
|
||
if (!_contacts.containsKey(publicChannelKey)) {
|
||
// Create a pseudo-contact for the public channel (ephemeral broadcast)
|
||
_contacts[publicChannelKey] = Contact(
|
||
publicKey: Uint8List.fromList(List.filled(32, 0)), // Zero key for public
|
||
type: ContactType.channel, // Channel type (not room!)
|
||
flags: 0,
|
||
outPathLen: 0,
|
||
outPath: Uint8List(64),
|
||
advName: 'Public Channel',
|
||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||
advLat: 0,
|
||
advLon: 0,
|
||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Persist contacts to storage (async, non-blocking)
|
||
Future<void> _persistContacts() async {
|
||
try {
|
||
// Don't persist the public channel pseudo-contact
|
||
final contactsToSave = _contacts.values
|
||
.where((c) => c.publicKeyHex != 'public_channel_0')
|
||
.toList();
|
||
await _storageService.saveContacts(contactsToSave);
|
||
} catch (e) {
|
||
print('❌ [ContactsProvider] Error persisting contacts: $e');
|
||
}
|
||
}
|
||
|
||
List<Contact> get contacts => _contacts.values.toList();
|
||
|
||
List<Contact> get chatContacts =>
|
||
contacts.where((c) => c.isChat).toList()..sort(_sortByLastSeen);
|
||
|
||
List<Contact> get repeaters =>
|
||
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
||
|
||
List<Contact> get rooms =>
|
||
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||
|
||
List<Contact> get channels {
|
||
// Always ensure public channel exists when getting channels
|
||
_ensurePublicChannelExists();
|
||
return contacts.where((c) => c.isChannel).toList()..sort(_sortByLastSeen);
|
||
}
|
||
|
||
/// Get both rooms and channels (destinations for SAR markers)
|
||
List<Contact> get roomsAndChannels {
|
||
_ensurePublicChannelExists();
|
||
return contacts.where((c) => c.isRoom || c.isChannel).toList()..sort(_sortByLastSeen);
|
||
}
|
||
|
||
/// Get contacts with location (for map display)
|
||
List<Contact> get contactsWithLocation =>
|
||
contacts.where((c) => c.displayLocation != null).toList();
|
||
|
||
/// Get chat contacts with location (team members on map)
|
||
List<Contact> get chatContactsWithLocation =>
|
||
chatContacts.where((c) => c.displayLocation != null).toList();
|
||
|
||
/// Sort contacts by last seen (most recent first)
|
||
int _sortByLastSeen(Contact a, Contact b) {
|
||
return b.lastSeenTime.compareTo(a.lastSeenTime);
|
||
}
|
||
|
||
/// Add or update a contact
|
||
/// Excludes contacts that match the device's own public key
|
||
void addOrUpdateContact(Contact contact, {Uint8List? devicePublicKey}) {
|
||
// Don't add contacts that match our device's public key
|
||
if (devicePublicKey != null && _publicKeysMatch(contact.publicKey, devicePublicKey)) {
|
||
print('ℹ️ [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}');
|
||
return;
|
||
}
|
||
|
||
_contacts[contact.publicKeyHex] = contact;
|
||
_persistContacts();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// 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;
|
||
}
|
||
|
||
/// Add multiple contacts
|
||
/// Excludes contacts that match the device's own public key
|
||
void addContacts(List<Contact> contacts, {Uint8List? devicePublicKey}) {
|
||
int excluded = 0;
|
||
for (final contact in contacts) {
|
||
// Don't add contacts that match our device's public key
|
||
if (devicePublicKey != null && _publicKeysMatch(contact.publicKey, devicePublicKey)) {
|
||
print('ℹ️ [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}');
|
||
excluded++;
|
||
continue;
|
||
}
|
||
_contacts[contact.publicKeyHex] = contact;
|
||
}
|
||
if (excluded > 0) {
|
||
print('ℹ️ [ContactsProvider] Excluded $excluded contact(s) matching device public key');
|
||
}
|
||
_persistContacts();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Update contact telemetry
|
||
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
||
// Find contact by public key prefix
|
||
final contact = _findContactByPrefix(publicKeyPrefix);
|
||
if (contact == null) return;
|
||
|
||
try {
|
||
// Parse Cayenne LPP data
|
||
final telemetry = CayenneLppParser.parse(lppData);
|
||
|
||
// Update contact with new telemetry
|
||
final updatedContact = contact.copyWith(telemetry: telemetry);
|
||
_contacts[contact.publicKeyHex] = updatedContact;
|
||
_persistContacts();
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('Failed to parse telemetry: $e');
|
||
}
|
||
}
|
||
|
||
/// Find contact by public key prefix (6 bytes)
|
||
Contact? _findContactByPrefix(Uint8List prefix) {
|
||
if (prefix.length < 6) return null;
|
||
|
||
final prefixHex = prefix
|
||
.sublist(0, 6)
|
||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||
.join('');
|
||
|
||
for (final contact in contacts) {
|
||
if (contact.publicKeyHex.startsWith(prefixHex)) {
|
||
return contact;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Find contact by public key
|
||
Contact? findContactByKey(Uint8List publicKey) {
|
||
final keyHex =
|
||
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||
return _contacts[keyHex];
|
||
}
|
||
|
||
/// Find contact by name
|
||
Contact? findContactByName(String name) {
|
||
return contacts.firstWhere(
|
||
(c) => c.advName == name,
|
||
orElse: () => contacts.first,
|
||
);
|
||
}
|
||
|
||
/// Get contacts with low battery
|
||
List<Contact> get lowBatteryContacts {
|
||
return contacts.where((c) {
|
||
final battery = c.displayBattery;
|
||
return battery != null && battery < 20.0;
|
||
}).toList();
|
||
}
|
||
|
||
/// Get recently seen contacts (within last 10 minutes)
|
||
List<Contact> get recentlySeenContacts {
|
||
return contacts.where((c) => c.isRecentlySeen).toList();
|
||
}
|
||
|
||
/// Clear all contacts
|
||
void clearContacts() {
|
||
_contacts.clear();
|
||
_persistContacts();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Remove a contact
|
||
void removeContact(String publicKeyHex) {
|
||
_contacts.remove(publicKeyHex);
|
||
_persistContacts();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Get storage statistics
|
||
Future<Map<String, dynamic>> getStorageStats() async {
|
||
return await _storageService.getStorageStats();
|
||
}
|
||
|
||
/// Get contact count by type
|
||
Map<String, int> get contactCounts {
|
||
return {
|
||
'chat': chatContacts.length,
|
||
'repeater': repeaters.length,
|
||
'room': rooms.length,
|
||
'total': contacts.length,
|
||
};
|
||
}
|
||
}
|