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

@@ -42,12 +42,20 @@ class AppProvider with ChangeNotifier {
void _setupCallbacks() {
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
contactsProvider.addOrUpdateContact(contact);
// Pass device public key to filter out our own contact
contactsProvider.addOrUpdateContact(
contact,
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
};
// When all contacts are received
connectionProvider.onContactsComplete = (contacts) {
contactsProvider.addContacts(contacts);
// Pass device public key to filter out our own contact
contactsProvider.addContacts(
contacts,
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
debugPrint('Received ${contacts.length} contacts');
};
@@ -118,6 +126,14 @@ class AppProvider with ChangeNotifier {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
// Initialize contacts provider with device public key to exclude self
// This must happen before getContacts to ensure proper filtering
if (!contactsProvider.isInitialized) {
await contactsProvider.initialize(
devicePublicKey: connectionProvider.deviceInfo.publicKey,
);
}
// Sync device time
await connectionProvider.syncDeviceTime();

View File

@@ -463,10 +463,12 @@ class ConnectionProvider with ChangeNotifier {
/// only that it was queued on the companion radio.
///
/// [messageId] - optional message ID to track delivery status
/// [contact] - optional contact object for path status logging
Future<bool> sendTextMessage({
required Uint8List contactPublicKey,
required String text,
String? messageId,
Contact? contact,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
@@ -475,6 +477,18 @@ class ConnectionProvider with ChangeNotifier {
}
try {
// Log path status if contact info is available
if (contact != null) {
print('📤 [ConnectionProvider] Sending message to ${contact.advName}');
print(' Type: ${contact.type.displayName}');
print(' Path status: ${contact.pathDescription}');
if (contact.hasPath) {
print(' ✅ Using learned path (${contact.outPathLen} bytes)');
} else {
print(' ⚠️ No path available - will use flood mode');
}
}
// IMPORTANT: Track pending message BEFORE sending to avoid race condition
// The SENT response can arrive so quickly that if we track after sending,
// the callback will fire before we add the message ID to the queue.

View File

@@ -1,19 +1,52 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import '../models/contact.dart';
import '../models/contact_telemetry.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';
@@ -34,6 +67,19 @@ class ContactsProvider with ChangeNotifier {
}
}
/// 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 =>
@@ -71,16 +117,45 @@ class ContactsProvider with ChangeNotifier {
}
/// Add or update a contact
void addOrUpdateContact(Contact 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
void addContacts(List<Contact> 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();
}
@@ -97,6 +172,7 @@ class ContactsProvider with ChangeNotifier {
// 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');
@@ -151,15 +227,22 @@ class ContactsProvider with ChangeNotifier {
/// 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 {