upgraded flutter and other fixes

This commit is contained in:
zach
2025-12-31 22:19:48 -07:00
parent be97e5c7fc
commit 44be6cd5e7
24 changed files with 2082 additions and 442 deletions
-152
View File
@@ -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);
+3 -3
View File
@@ -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) {
+194 -70
View File
@@ -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();
}
}
+12 -6
View File
@@ -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,
);