mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
upgraded flutter and other fixes
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
const int _codec2Mode1300 = 4;
|
||||
|
||||
class Codec2Ffi {
|
||||
Codec2Ffi._(this._lib)
|
||||
: _codec2Create = _lib
|
||||
.lookupFunction<_codec2_create_c, _codec2_create_d>('codec2_create'),
|
||||
_codec2Destroy = _lib
|
||||
.lookupFunction<_codec2_destroy_c, _codec2_destroy_d>('codec2_destroy'),
|
||||
_codec2Encode = _lib
|
||||
.lookupFunction<_codec2_encode_c, _codec2_encode_d>('codec2_encode'),
|
||||
_codec2Decode = _lib
|
||||
.lookupFunction<_codec2_decode_c, _codec2_decode_d>('codec2_decode'),
|
||||
_codec2SamplesPerFrame = _lib.lookupFunction<_codec2_samples_per_frame_c,
|
||||
_codec2_samples_per_frame_d>('codec2_samples_per_frame'),
|
||||
_codec2BytesPerFrame = _lib.lookupFunction<_codec2_bytes_per_frame_c,
|
||||
_codec2_bytes_per_frame_d>('codec2_bytes_per_frame');
|
||||
|
||||
static final Codec2Ffi instance = Codec2Ffi._(_openLibrary());
|
||||
|
||||
final DynamicLibrary _lib;
|
||||
final _codec2_create_d _codec2Create;
|
||||
final _codec2_destroy_d _codec2Destroy;
|
||||
final _codec2_encode_d _codec2Encode;
|
||||
final _codec2_decode_d _codec2Decode;
|
||||
final _codec2_samples_per_frame_d _codec2SamplesPerFrame;
|
||||
final _codec2_bytes_per_frame_d _codec2BytesPerFrame;
|
||||
|
||||
Codec2Session createSession() {
|
||||
final handle = _codec2Create(_codec2Mode1300);
|
||||
if (handle == nullptr) {
|
||||
throw StateError('codec2_create returned null');
|
||||
}
|
||||
return Codec2Session._(
|
||||
handle: handle,
|
||||
destroy: _codec2Destroy,
|
||||
encode: _codec2Encode,
|
||||
decode: _codec2Decode,
|
||||
samplesPerFrame: _codec2SamplesPerFrame,
|
||||
bytesPerFrame: _codec2BytesPerFrame,
|
||||
);
|
||||
}
|
||||
|
||||
static DynamicLibrary _openLibrary() {
|
||||
if (Platform.isAndroid) {
|
||||
return DynamicLibrary.open('libcodec2.so');
|
||||
}
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
return DynamicLibrary.process();
|
||||
}
|
||||
throw UnsupportedError('Codec2 is only supported on Android and iOS.');
|
||||
}
|
||||
}
|
||||
|
||||
class Codec2Session {
|
||||
Codec2Session._({
|
||||
required this.handle,
|
||||
required this.destroy,
|
||||
required this.encode,
|
||||
required this.decode,
|
||||
required this.samplesPerFrame,
|
||||
required this.bytesPerFrame,
|
||||
});
|
||||
|
||||
final Pointer<Void> handle;
|
||||
final _codec2_destroy_d destroy;
|
||||
final _codec2_encode_d encode;
|
||||
final _codec2_decode_d decode;
|
||||
final _codec2_samples_per_frame_d samplesPerFrame;
|
||||
final _codec2_bytes_per_frame_d bytesPerFrame;
|
||||
|
||||
int get samplesPerFrameValue => samplesPerFrame(handle);
|
||||
int get bytesPerFrameValue => bytesPerFrame(handle);
|
||||
|
||||
Uint8List encodePcmFrame(Int16List pcmFrame) {
|
||||
final bytesOut = calloc<Uint8>(bytesPerFrameValue);
|
||||
final pcmIn = calloc<Int16>(samplesPerFrameValue);
|
||||
try {
|
||||
final sampleCount = samplesPerFrameValue;
|
||||
final pcmBuffer = pcmIn.asTypedList(sampleCount);
|
||||
final copyLen = pcmFrame.length < sampleCount ? pcmFrame.length : sampleCount;
|
||||
pcmBuffer.setRange(0, copyLen, pcmFrame);
|
||||
if (copyLen < sampleCount) {
|
||||
for (var i = copyLen; i < sampleCount; i++) {
|
||||
pcmBuffer[i] = 0;
|
||||
}
|
||||
}
|
||||
encode(handle, bytesOut, pcmIn);
|
||||
return Uint8List.fromList(bytesOut.asTypedList(bytesPerFrameValue));
|
||||
} finally {
|
||||
calloc.free(bytesOut);
|
||||
calloc.free(pcmIn);
|
||||
}
|
||||
}
|
||||
|
||||
Int16List decodeCodecFrame(Uint8List codecFrame) {
|
||||
final pcmOut = calloc<Int16>(samplesPerFrameValue);
|
||||
final bytesIn = calloc<Uint8>(bytesPerFrameValue);
|
||||
try {
|
||||
final codecBuffer = bytesIn.asTypedList(bytesPerFrameValue);
|
||||
codecBuffer.setRange(0, bytesPerFrameValue, codecFrame);
|
||||
decode(handle, pcmOut, bytesIn);
|
||||
return Int16List.fromList(pcmOut.asTypedList(samplesPerFrameValue));
|
||||
} finally {
|
||||
calloc.free(bytesIn);
|
||||
calloc.free(pcmOut);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
destroy(handle);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _codec2_create_c = Pointer<Void> Function(Int32 mode);
|
||||
typedef _codec2_create_d = Pointer<Void> Function(int mode);
|
||||
|
||||
typedef _codec2_destroy_c = Void Function(Pointer<Void> codec2State);
|
||||
typedef _codec2_destroy_d = void Function(Pointer<Void> codec2State);
|
||||
|
||||
typedef _codec2_encode_c = Void Function(
|
||||
Pointer<Void> codec2State,
|
||||
Pointer<Uint8> bytes,
|
||||
Pointer<Int16> speechIn,
|
||||
);
|
||||
typedef _codec2_encode_d = void Function(
|
||||
Pointer<Void> codec2State,
|
||||
Pointer<Uint8> bytes,
|
||||
Pointer<Int16> speechIn,
|
||||
);
|
||||
|
||||
typedef _codec2_decode_c = Void Function(
|
||||
Pointer<Void> codec2State,
|
||||
Pointer<Int16> speechOut,
|
||||
Pointer<Uint8> bytes,
|
||||
);
|
||||
typedef _codec2_decode_d = void Function(
|
||||
Pointer<Void> codec2State,
|
||||
Pointer<Int16> speechOut,
|
||||
Pointer<Uint8> bytes,
|
||||
);
|
||||
|
||||
typedef _codec2_samples_per_frame_c = Int32 Function(Pointer<Void> codec2State);
|
||||
typedef _codec2_samples_per_frame_d = int Function(Pointer<Void> codec2State);
|
||||
|
||||
typedef _codec2_bytes_per_frame_c = Int32 Function(Pointer<Void> codec2State);
|
||||
typedef _codec2_bytes_per_frame_d = int Function(Pointer<Void> codec2State);
|
||||
@@ -183,7 +183,7 @@ class MapTileCacheService {
|
||||
int _lonToTileX(double lon, int zoom, int maxIndex) {
|
||||
final n = 1 << zoom;
|
||||
final value = ((lon + 180.0) / 360.0 * n).floor();
|
||||
return value.clamp(0, maxIndex) as int;
|
||||
return value.clamp(0, maxIndex);
|
||||
}
|
||||
|
||||
int _latToTileY(double lat, int zoom, int maxIndex) {
|
||||
@@ -194,12 +194,12 @@ class MapTileCacheService {
|
||||
2 *
|
||||
n)
|
||||
.floor();
|
||||
return value.clamp(0, maxIndex) as int;
|
||||
return value.clamp(0, maxIndex);
|
||||
}
|
||||
|
||||
double _clampLatitude(double lat) {
|
||||
const maxLat = 85.05112878;
|
||||
return lat.clamp(-maxLat, maxLat) as double;
|
||||
return lat.clamp(-maxLat, maxLat);
|
||||
}
|
||||
|
||||
String _buildTileUrl(int x, int y, int zoom) {
|
||||
|
||||
@@ -19,6 +19,16 @@ class _AckHistoryEntry {
|
||||
});
|
||||
}
|
||||
|
||||
class _AckHashMapping {
|
||||
final String messageId;
|
||||
final DateTime timestamp;
|
||||
|
||||
_AckHashMapping({
|
||||
required this.messageId,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
class MessageRetryService extends ChangeNotifier {
|
||||
static const int maxRetries = 5;
|
||||
static const int maxAckHistorySize = 100;
|
||||
@@ -28,8 +38,10 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final Map<String, Message> _pendingMessages = {};
|
||||
final Map<String, Contact> _pendingContacts = {};
|
||||
final Map<String, PathSelection> _pendingPathSelections = {};
|
||||
final Map<String, List<Uint8List>> _expectedAckHashes = {}; // Track all expected ACKs for retries
|
||||
final Map<String, _AckHashMapping> _ackHashToMessageId = {}; // ackHashHex → messageId + timestamp for O(1) lookup
|
||||
final Map<String, List<Uint8List>> _expectedAckHashes = {}; // Track all expected ACKs for retries (for history)
|
||||
final List<_AckHistoryEntry> _ackHistory = []; // Rolling buffer of recent ACK hashes
|
||||
final Map<String, List<String>> _pendingMessageQueuePerContact = {}; // contactPubKeyHex → FIFO queue of messageIds
|
||||
|
||||
Function(Contact, String, int, int)? _sendMessageCallback;
|
||||
Function(String, Message)? _addMessageCallback;
|
||||
@@ -65,17 +77,16 @@ class MessageRetryService extends ChangeNotifier {
|
||||
Future<void> sendMessageWithRetry({
|
||||
required Contact contact,
|
||||
required String text,
|
||||
bool clearPath = false,
|
||||
PathSelection? pathSelection,
|
||||
Uint8List? pathBytes,
|
||||
int? pathLength,
|
||||
}) async {
|
||||
final messageId = const Uuid().v4();
|
||||
final useClearPath = clearPath || (pathSelection?.useFlood ?? false);
|
||||
final useFlood = pathSelection?.useFlood ?? false;
|
||||
final messagePathBytes =
|
||||
pathBytes ?? _resolveMessagePathBytes(contact, useClearPath, pathSelection);
|
||||
pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection);
|
||||
final messagePathLength =
|
||||
pathLength ?? _resolveMessagePathLength(contact, useClearPath, pathSelection);
|
||||
pathLength ?? _resolveMessagePathLength(contact, useFlood, pathSelection);
|
||||
final message = Message(
|
||||
senderKey: contact.publicKey,
|
||||
text: text,
|
||||
@@ -126,6 +137,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
final attempt = message.retryCount.clamp(0, 3);
|
||||
|
||||
// Enqueue this message to track send order for ACK hash mapping (FIFO)
|
||||
_pendingMessageQueuePerContact[contact.publicKeyHex] ??= [];
|
||||
_pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId);
|
||||
debugPrint('Enqueued message $messageId for ${contact.name} (queue size: ${_pendingMessageQueuePerContact[contact.publicKeyHex]!.length})');
|
||||
|
||||
if (_sendMessageCallback != null) {
|
||||
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||
_sendMessageCallback!(
|
||||
@@ -138,58 +154,103 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||
for (var entry in _pendingMessages.entries) {
|
||||
final message = entry.value;
|
||||
// Only update if pending (waiting to send) or already sent with matching ACK
|
||||
if (message.status == MessageStatus.pending ||
|
||||
(message.status == MessageStatus.sent &&
|
||||
message.expectedAckHash != null &&
|
||||
listEquals(message.expectedAckHash, ackHash))) {
|
||||
final contact = _pendingContacts[entry.key];
|
||||
final selection = _pendingPathSelections[entry.key];
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
// Add this ACK hash to the list of expected ACKs for this message
|
||||
_expectedAckHashes[entry.key] ??= [];
|
||||
if (!_expectedAckHashes[entry.key]!.any((hash) => listEquals(hash, ackHash))) {
|
||||
_expectedAckHashes[entry.key]!.add(Uint8List.fromList(ackHash));
|
||||
debugPrint('Added ACK hash ${ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join()} to message ${entry.key} (total: ${_expectedAckHashes[entry.key]!.length})');
|
||||
}
|
||||
// Dequeue the next message from the FIFO queue to match with this RESP_CODE_SENT
|
||||
// We iterate through contacts to find which one has a pending message in their queue
|
||||
String? messageId;
|
||||
Contact? contact;
|
||||
|
||||
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
|
||||
int actualTimeout = timeoutMs;
|
||||
if (timeoutMs <= 0 && _calculateTimeoutCallback != null && contact != null) {
|
||||
int pathLengthValue;
|
||||
if (selection != null) {
|
||||
pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
||||
if (pathLengthValue < 0) pathLengthValue = contact.pathLength;
|
||||
} else if (message.pathLength != null) {
|
||||
pathLengthValue = message.pathLength!;
|
||||
} else {
|
||||
pathLengthValue = contact.pathLength;
|
||||
for (var entry in _pendingMessageQueuePerContact.entries) {
|
||||
final contactKey = entry.key;
|
||||
final queue = entry.value;
|
||||
|
||||
if (queue.isNotEmpty) {
|
||||
// Dequeue the first (oldest) message from this contact's queue
|
||||
final candidateMessageId = queue.removeAt(0);
|
||||
|
||||
// Verify this message is still pending
|
||||
if (_pendingMessages.containsKey(candidateMessageId)) {
|
||||
messageId = candidateMessageId;
|
||||
contact = _pendingContacts[candidateMessageId];
|
||||
debugPrint('Dequeued message $messageId for $contactKey (remaining in queue: ${queue.length})');
|
||||
break;
|
||||
} else {
|
||||
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
||||
// Continue to next message in queue
|
||||
if (queue.isNotEmpty) {
|
||||
final nextMessageId = queue.removeAt(0);
|
||||
if (_pendingMessages.containsKey(nextMessageId)) {
|
||||
messageId = nextMessageId;
|
||||
contact = _pendingContacts[nextMessageId];
|
||||
debugPrint('Dequeued next message $messageId for $contactKey (remaining: ${queue.length})');
|
||||
break;
|
||||
}
|
||||
}
|
||||
actualTimeout = _calculateTimeoutCallback!(pathLengthValue, message.text.length);
|
||||
debugPrint('Using calculated timeout: ${actualTimeout}ms for ${contact.pathLength} hops');
|
||||
}
|
||||
|
||||
final updatedMessage = message.copyWith(
|
||||
status: MessageStatus.sent,
|
||||
expectedAckHash: ackHash, // Keep the most recent one for display
|
||||
estimatedTimeoutMs: actualTimeout,
|
||||
sentAt: DateTime.now(),
|
||||
);
|
||||
|
||||
_pendingMessages[entry.key] = updatedMessage;
|
||||
|
||||
if (_updateMessageCallback != null) {
|
||||
_updateMessageCallback!(updatedMessage);
|
||||
}
|
||||
|
||||
_startTimeoutTimer(entry.key, actualTimeout);
|
||||
debugPrint('Updated message ${entry.key} with ACK hash: ${ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
debugPrint('No pending message found for ACK hash: ${ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}');
|
||||
|
||||
if (messageId == null || contact == null) {
|
||||
debugPrint('No pending message found for ACK hash: $ackHashHex (all queues empty or stale)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the mapping for future lookups (e.g., when ACK arrives)
|
||||
// Keep timestamp so we can clean up old mappings later
|
||||
_ackHashToMessageId[ackHashHex] = _AckHashMapping(
|
||||
messageId: messageId,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
debugPrint('Mapped ACK hash $ackHashHex to message $messageId');
|
||||
|
||||
final message = _pendingMessages[messageId];
|
||||
final selection = _pendingPathSelections[messageId];
|
||||
|
||||
if (message == null) {
|
||||
debugPrint('Message $messageId no longer pending for ACK hash: $ackHashHex');
|
||||
_ackHashToMessageId.remove(ackHashHex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||
_expectedAckHashes[messageId] ??= [];
|
||||
if (!_expectedAckHashes[messageId]!.any((hash) => listEquals(hash, ackHash))) {
|
||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
||||
debugPrint('Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})');
|
||||
}
|
||||
|
||||
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
|
||||
int actualTimeout = timeoutMs;
|
||||
if (timeoutMs <= 0 && _calculateTimeoutCallback != null) {
|
||||
int pathLengthValue;
|
||||
if (selection != null) {
|
||||
pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
||||
if (pathLengthValue < 0) pathLengthValue = contact.pathLength;
|
||||
} else if (message.pathLength != null) {
|
||||
pathLengthValue = message.pathLength!;
|
||||
} else {
|
||||
pathLengthValue = contact.pathLength;
|
||||
}
|
||||
actualTimeout = _calculateTimeoutCallback!(pathLengthValue, message.text.length);
|
||||
debugPrint('Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue');
|
||||
}
|
||||
|
||||
final updatedMessage = message.copyWith(
|
||||
status: MessageStatus.sent,
|
||||
expectedAckHash: ackHash,
|
||||
estimatedTimeoutMs: actualTimeout,
|
||||
sentAt: DateTime.now(),
|
||||
);
|
||||
|
||||
_pendingMessages[messageId] = updatedMessage;
|
||||
|
||||
if (_updateMessageCallback != null) {
|
||||
_updateMessageCallback!(updatedMessage);
|
||||
}
|
||||
|
||||
_startTimeoutTimer(messageId, actualTimeout);
|
||||
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
|
||||
}
|
||||
|
||||
void _startTimeoutTimer(String messageId, int timeoutMs) {
|
||||
@@ -204,7 +265,10 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final contact = _pendingContacts[messageId];
|
||||
final selection = _pendingPathSelections[messageId];
|
||||
|
||||
if (message == null || contact == null) return;
|
||||
if (message == null || contact == null) {
|
||||
debugPrint('Timeout fired but message $messageId no longer pending (likely already delivered)');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})');
|
||||
|
||||
@@ -225,7 +289,12 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
debugPrint('Scheduling retry after ${backoffMs}ms');
|
||||
Timer(Duration(milliseconds: backoffMs), () {
|
||||
_attemptSend(messageId);
|
||||
// Double-check message is still pending before retry
|
||||
if (_pendingMessages.containsKey(messageId)) {
|
||||
_attemptSend(messageId);
|
||||
} else {
|
||||
debugPrint('Retry cancelled: message $messageId was delivered while waiting');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Max retries reached - mark as failed
|
||||
@@ -240,6 +309,12 @@ class MessageRetryService extends ChangeNotifier {
|
||||
_timeoutTimers[messageId]?.cancel();
|
||||
_timeoutTimers.remove(messageId);
|
||||
|
||||
// Clean up the queue entry for this contact
|
||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
|
||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||
}
|
||||
|
||||
// Check if we should clear the path on max retry
|
||||
if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
||||
_clearContactPathCallback != null) {
|
||||
@@ -291,28 +366,44 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms');
|
||||
debugPrint('Pending messages:');
|
||||
for (var entry in _pendingMessages.entries) {
|
||||
final message = entry.value;
|
||||
final expectedHex = message.expectedAckHash?.map((b) => b.toRadixString(16).padLeft(2, '0')).join() ?? 'none';
|
||||
final allExpectedHashes = _expectedAckHashes[entry.key]?.map((h) => h.map((b) => b.toRadixString(16).padLeft(2, '0')).join()).join(', ') ?? 'none';
|
||||
debugPrint(' ${entry.key}: status=${message.status}, latestAck=$expectedHex, allAcks=[$allExpectedHashes], retry=${message.retryCount}');
|
||||
|
||||
// First, clean up old ACK hash mappings (older than 15 minutes)
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||
final hashesToRemove = <String>[];
|
||||
for (var entry in _ackHashToMessageId.entries) {
|
||||
if (entry.value.timestamp.isBefore(cutoffTime)) {
|
||||
hashesToRemove.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (var hash in hashesToRemove) {
|
||||
_ackHashToMessageId.remove(hash);
|
||||
}
|
||||
if (hashesToRemove.isNotEmpty) {
|
||||
debugPrint('Cleaned up ${hashesToRemove.length} old ACK hash mappings');
|
||||
}
|
||||
|
||||
// Check against ALL expected ACK hashes (from all retry attempts)
|
||||
for (var entry in _expectedAckHashes.entries) {
|
||||
final messageId = entry.key;
|
||||
final expectedHashes = entry.value;
|
||||
// Use direct O(1) lookup via ACK hash mapping
|
||||
final mapping = _ackHashToMessageId[ackHashHex];
|
||||
if (mapping != null) {
|
||||
matchedMessageId = mapping.messageId;
|
||||
debugPrint('Matched ACK to message via direct lookup: $matchedMessageId');
|
||||
} else {
|
||||
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
|
||||
debugPrint('ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)');
|
||||
for (var entry in _expectedAckHashes.entries) {
|
||||
final messageId = entry.key;
|
||||
final expectedHashes = entry.value;
|
||||
|
||||
for (final expectedHash in expectedHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
matchedMessageId = messageId;
|
||||
debugPrint('Matched ACK to message: $matchedMessageId (matched hash from attempt ${expectedHashes.indexOf(expectedHash)})');
|
||||
break;
|
||||
for (final expectedHash in expectedHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
matchedMessageId = messageId;
|
||||
debugPrint('Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedMessageId != null) break;
|
||||
if (matchedMessageId != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedMessageId != null) {
|
||||
@@ -337,6 +428,14 @@ class MessageRetryService extends ChangeNotifier {
|
||||
_pendingContacts.remove(matchedMessageId);
|
||||
_pendingPathSelections.remove(matchedMessageId);
|
||||
|
||||
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
||||
if (contact != null) {
|
||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(matchedMessageId);
|
||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
|
||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
if (_updateMessageCallback != null) {
|
||||
_updateMessageCallback!(deliveredMessage);
|
||||
}
|
||||
@@ -361,12 +460,25 @@ class MessageRetryService extends ChangeNotifier {
|
||||
bool forceFlood,
|
||||
PathSelection? selection,
|
||||
) {
|
||||
// Priority 1: Check user's path override
|
||||
if (contact.pathOverride != null) {
|
||||
if (contact.pathOverride! < 0) {
|
||||
return Uint8List(0); // Force flood
|
||||
}
|
||||
return contact.pathOverrideBytes ?? Uint8List(0);
|
||||
}
|
||||
|
||||
// Priority 2: Check forceFlood or device flood mode
|
||||
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
// Priority 3: Check PathSelection (auto-rotation)
|
||||
if (selection != null && selection.pathBytes.isNotEmpty) {
|
||||
return Uint8List.fromList(selection.pathBytes);
|
||||
}
|
||||
|
||||
// Priority 4: Use device's discovered path
|
||||
return contact.path;
|
||||
}
|
||||
|
||||
@@ -375,12 +487,22 @@ class MessageRetryService extends ChangeNotifier {
|
||||
bool forceFlood,
|
||||
PathSelection? selection,
|
||||
) {
|
||||
// Priority 1: Check user's path override
|
||||
if (contact.pathOverride != null) {
|
||||
return contact.pathOverride;
|
||||
}
|
||||
|
||||
// Priority 2: Check forceFlood or device flood mode
|
||||
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Priority 3: Check PathSelection (auto-rotation)
|
||||
if (selection != null && selection.pathBytes.isNotEmpty) {
|
||||
return selection.hopCount;
|
||||
}
|
||||
|
||||
// Priority 4: Use device's discovered path
|
||||
return contact.pathLength;
|
||||
}
|
||||
|
||||
@@ -442,6 +564,8 @@ class MessageRetryService extends ChangeNotifier {
|
||||
_pendingPathSelections.clear();
|
||||
_expectedAckHashes.clear();
|
||||
_ackHistory.clear();
|
||||
_ackHashToMessageId.clear();
|
||||
_pendingMessageQueuePerContact.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,27 +67,30 @@ class NotificationService {
|
||||
required String contactName,
|
||||
required String message,
|
||||
String? contactId,
|
||||
int? badgeCount,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'messages',
|
||||
'Messages',
|
||||
channelDescription: 'New message notifications',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
number: badgeCount,
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
final iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
badgeNumber: badgeCount,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
@@ -143,27 +146,30 @@ class NotificationService {
|
||||
required String channelName,
|
||||
required String message,
|
||||
int? channelIndex,
|
||||
int? badgeCount,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'channel_messages',
|
||||
'Channel Messages',
|
||||
channelDescription: 'New channel message notifications',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
number: badgeCount,
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
final iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
badgeNumber: badgeCount,
|
||||
);
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user