Ensure contact sync waits

This commit is contained in:
Janez T
2026-03-14 10:24:14 +01:00
parent 1b801a10ef
commit b42328bd82
5 changed files with 143 additions and 51 deletions

View File

@@ -1895,12 +1895,9 @@ class AppProvider with ChangeNotifier {
// Get battery and storage information
await connectionProvider.getBatteryAndStorage();
// Load contacts
// Load contacts (waits for device to finish sending all contacts)
await connectionProvider.getContacts();
// Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500));
// Sync all channels so slot assignment and channel state mirror the device.
final channelsToSync = connectionProvider.deviceInfo.maxChannels;
debugPrint(

View File

@@ -130,6 +130,9 @@ class ConnectionProvider with ChangeNotifier {
// Completer to wait for response before sending next sync request
Completer<bool>? _syncResponseCompleter;
// Completer to wait for contacts sync to finish
Completer<void>? _contactsSyncCompleter;
// Lightweight guards for other commands that can be double-tapped
bool _isLoginInProgress = false;
DateTime? _lastLoginRequestedAt;
@@ -291,6 +294,10 @@ class ConnectionProvider with ChangeNotifier {
service.onContactsComplete = (contacts) {
debugPrint('📥 [Provider] Contacts sync complete: ${contacts.length}');
if (_contactsSyncCompleter != null &&
!_contactsSyncCompleter!.isCompleted) {
_contactsSyncCompleter!.complete();
}
onContactsComplete?.call(contacts);
};
@@ -583,6 +590,9 @@ class ConnectionProvider with ChangeNotifier {
await stopScan();
}
// Ensure we route commands to BLE, not a stale TCP service.
_connectionMode = ConnectionMode.ble;
_deviceInfo = _deviceInfo.copyWith(
deviceId: device.remoteId.toString(),
deviceName: device.platformName.isNotEmpty
@@ -591,6 +601,7 @@ class ConnectionProvider with ChangeNotifier {
connectionState: ConnectionState.connecting,
);
_error = null;
_resetSyncState();
debugPrint('✅ [Provider] Device info updated to connecting state');
notifyListeners();
@@ -648,6 +659,7 @@ class ConnectionProvider with ChangeNotifier {
}
_tcpHost = null;
_connectionMode = ConnectionMode.ble;
_resetSyncState();
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
_roomLoginManager.clearRoomLoginStates();
_pingTracker.clearAll();
@@ -670,6 +682,7 @@ class ConnectionProvider with ChangeNotifier {
await _bleService.disconnect();
_resetSyncState();
_deviceInfo = DeviceInfo(connectionState: ConnectionState.disconnected);
_roomLoginManager.clearRoomLoginStates();
_pingTracker.clearAll();
@@ -678,6 +691,24 @@ class ConnectionProvider with ChangeNotifier {
notifyListeners();
}
/// Reset message sync state so the next connect/reconnect can sync cleanly.
void _resetSyncState() {
if (_syncResponseCompleter != null &&
!_syncResponseCompleter!.isCompleted) {
_syncResponseCompleter!.complete(false);
}
_syncResponseCompleter = null;
if (_contactsSyncCompleter != null &&
!_contactsSyncCompleter!.isCompleted) {
_contactsSyncCompleter!.complete();
}
_contactsSyncCompleter = null;
_isSyncingMessages = false;
_syncRequestedWhileBusy = false;
_noMoreMessages = false;
_pendingAutomaticMessageSync = false;
}
/// Cancel ongoing reconnection attempts
/// This is useful when the user wants to manually disconnect during reconnection
void cancelReconnection() {
@@ -719,7 +750,10 @@ class ConnectionProvider with ChangeNotifier {
return _messageDeliveryTracker.getDiagnostics();
}
/// Get contacts from device
/// Get contacts from device.
///
/// Waits for the device to finish sending all contacts (up to 5 s timeout)
/// so callers don't need an arbitrary delay.
Future<void> getContacts() async {
if (!_activeService.isConnected) {
_error = 'Not connected to device';
@@ -728,10 +762,21 @@ class ConnectionProvider with ChangeNotifier {
}
try {
_contactsSyncCompleter = Completer<void>();
await _activeService.getContacts();
await _contactsSyncCompleter!.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint(
'⚠️ [Provider] Contacts sync timeout - proceeding without full list',
);
},
);
} catch (e) {
_error = 'Failed to get contacts: $e';
notifyListeners();
} finally {
_contactsSyncCompleter = null;
}
}
@@ -890,9 +935,8 @@ class ConnectionProvider with ChangeNotifier {
}
}
// If not cached, query the device
// If not cached, query the device (awaits the BLE response)
await _activeService.getChannel(channelIdx);
await Future.delayed(const Duration(milliseconds: 100));
// Check again after query
if (getChannelInfo != null) {
@@ -2370,6 +2414,7 @@ class ConnectionProvider with ChangeNotifier {
void dispose() {
_rxActivityTimer?.cancel();
_txActivityTimer?.cancel();
_stopAckCleanupTimer();
_bleService.dispose();
_tcpService?.dispose();
_sseServer.stopServer();

View File

@@ -64,6 +64,8 @@ class ContactsProvider with ChangeNotifier {
final Map<String, PendingAdvert> _pendingAdverts = {};
final ContactStorageService _storageService = ContactStorageService();
bool _isInitialized = false;
bool _isPersisting = false;
bool _persistRequested = false;
// Add default public channel on initialization
ContactsProvider() {
@@ -202,19 +204,27 @@ class ContactsProvider with ChangeNotifier {
}
}
/// Persist contacts to storage (async, non-blocking)
/// Persist contacts to storage (async, non-blocking, coalescing).
Future<void> _persistContacts() async {
_persistRequested = true;
if (_isPersisting) return;
_isPersisting = true;
try {
// Don't persist the public channel pseudo-contact (all zeros key)
const publicChannelKey =
'0000000000000000000000000000000000000000000000000000000000000000';
final contactsToSave = _contacts.entries
.where((entry) => entry.key != publicChannelKey)
.map((entry) => entry.value)
.toList();
await _storageService.saveContacts(contactsToSave);
while (_persistRequested) {
_persistRequested = false;
// Don't persist the public channel pseudo-contact (all zeros key)
const publicChannelKey =
'0000000000000000000000000000000000000000000000000000000000000000';
final contactsToSave = _contacts.entries
.where((entry) => entry.key != publicChannelKey)
.map((entry) => entry.value)
.toList();
await _storageService.saveContacts(contactsToSave);
}
} catch (e) {
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
} finally {
_isPersisting = false;
}
}
@@ -503,7 +513,6 @@ class ContactsProvider with ChangeNotifier {
if (existingContact == null) {
var newContact = incomingContact.copyWith(
isNew: true,
nameOverride: existingContact?.nameOverride,
telemetry: mergedTelemetry,
outPathLen:
retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen,
@@ -908,7 +917,7 @@ class ContactsProvider with ChangeNotifier {
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join('');
for (final contact in contacts) {
for (final contact in _contacts.values) {
if (contact.publicKeyHex.startsWith(prefixHex)) {
return contact;
}
@@ -1092,10 +1101,10 @@ class ContactsProvider with ChangeNotifier {
/// Find contact by name
Contact? findContactByName(String name) {
return contacts.firstWhere(
(c) => c.advName == name,
orElse: () => contacts.first,
);
for (final contact in _contacts.values) {
if (contact.advName == name) return contact;
}
return null;
}
/// Get contacts with low battery
@@ -1144,6 +1153,7 @@ class ContactsProvider with ChangeNotifier {
void clearContacts() {
_contacts.clear();
_pendingAdverts.clear();
_ensurePublicChannelExists();
_persistContacts();
notifyListeners();
}