diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 4a3589b44..79eef1e29 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -42,7 +42,6 @@ import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.core.model.util.toOneLiner import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.core.service.RetryEvent import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.core.strings.Res @@ -467,123 +466,13 @@ constructor( val isAck = routingError == Routing.Error.NONE.value val p = packetRepository.get().getPacketById(requestId) val reaction = packetRepository.get().getReactionByPacketId(requestId) - val isMaxRetransmit = routingError == Routing.Error.MAX_RETRANSMIT.value - val shouldRetry = - isMaxRetransmit && - p != null && - p.port_num == PortNum.TEXT_MESSAGE_APP.value && - (p.data.from == DataPacket.ID_LOCAL || p.data.from == nodeManager.getMyId()) && - p.data.retryCount < MAX_RETRY_ATTEMPTS - val shouldRetryReaction = - isMaxRetransmit && - reaction != null && - (reaction.userId == DataPacket.ID_LOCAL || reaction.userId == nodeManager.getMyId()) && - reaction.retryCount < MAX_RETRY_ATTEMPTS && - reaction.to != null @Suppress("MaxLineLength") Logger.d { - val retryInfo = - "packetId=${p?.packetId ?: reaction?.packetId} dataId=${p?.data?.id} retry=${p?.data?.retryCount ?: reaction?.retryCount}" val statusInfo = "status=${p?.data?.status ?: reaction?.status}" "[ackNak] req=$requestId routeErr=$routingError isAck=$isAck " + - "maxRetransmit=$isMaxRetransmit shouldRetry=$shouldRetry reaction=$shouldRetryReaction $retryInfo $statusInfo" - } - - if (shouldRetry) { - val newRetryCount = p.data.retryCount + 1 - - // Emit retry event to UI and wait for user response - val retryEvent = - RetryEvent.MessageRetry( - packetId = requestId, - text = p.data.text ?: "", - attemptNumber = newRetryCount, - maxAttempts = MAX_RETRY_ATTEMPTS + 1, // +1 for initial attempt - ) - - Logger.w { "[ackNak] requesting retry for req=$requestId retry=$newRetryCount" } - Log.d("MeshDataHandler", "[ackNak] Emitting retry event for req=$requestId retry=$newRetryCount") - - val shouldProceed = serviceRepository.requestRetry(retryEvent, RETRY_DELAY_MS) - Log.d("MeshDataHandler", "[ackNak] Retry response for req=$requestId: shouldProceed=$shouldProceed") - - if (shouldProceed) { - val newId = commandSender.generatePacketId() - val updatedData = - p.data.copy( - id = newId, - status = MessageStatus.QUEUED, - retryCount = newRetryCount, - relayNode = null, - ) - val updatedPacket = - p.copy(packetId = newId, data = updatedData, routingError = Routing.Error.NONE.value) - packetRepository.get().update(updatedPacket) - - Logger.w { "[ackNak] retrying req=$requestId newId=$newId retry=$newRetryCount" } - commandSender.sendData(updatedData) - } else { - // User cancelled retry - mark as ERROR - Logger.w { "[ackNak] retry cancelled by user for req=$requestId" } - p.data.status = MessageStatus.ERROR - packetRepository.get().update(p) - } - return@handledLaunch - } - - if (shouldRetryReaction) { - val newRetryCount = reaction.retryCount + 1 - - // Emit retry event to UI and wait for user response - val retryEvent = - RetryEvent.ReactionRetry( - packetId = requestId, - emoji = reaction.emoji, - attemptNumber = newRetryCount, - maxAttempts = MAX_RETRY_ATTEMPTS + 1, // +1 for initial attempt - ) - - Logger.w { "[ackNak] requesting retry for reaction req=$requestId retry=$newRetryCount" } - - val shouldProceed = serviceRepository.requestRetry(retryEvent, RETRY_DELAY_MS) - - if (shouldProceed) { - val newId = commandSender.generatePacketId() - - val reactionPacket = - DataPacket( - to = reaction.to, - channel = reaction.channel, - bytes = reaction.emoji.encodeToByteArray().toByteString(), - dataType = PortNum.TEXT_MESSAGE_APP.value, - replyId = reaction.replyId, - wantAck = true, - emoji = reaction.emoji.codePointAt(0), - id = newId, - retryCount = newRetryCount, - ) - - val updatedReaction = - reaction.copy( - packetId = newId, - status = MessageStatus.QUEUED, - retryCount = newRetryCount, - relayNode = null, - routingError = Routing.Error.NONE.value, - ) - packetRepository.get().updateReaction(updatedReaction) - - Logger.w { "[ackNak] retrying reaction req=$requestId newId=$newId retry=$newRetryCount" } - commandSender.sendData(reactionPacket) - } else { - // User cancelled retry - mark as ERROR - Logger.w { "[ackNak] retry cancelled by user for reaction req=$requestId" } - val errorReaction = reaction.copy(status = MessageStatus.ERROR, routingError = routingError) - packetRepository.get().updateReaction(errorReaction) - } - return@handledLaunch + "maxRetransmit=$isMaxRetransmit packetId=${p?.packetId ?: reaction?.packetId} dataId=${p?.data?.id} $statusInfo" } val m = @@ -893,8 +782,6 @@ constructor( } companion object { - private const val MAX_RETRY_ATTEMPTS = 2 - private const val RETRY_DELAY_MS = 5_000L private const val MILLISECONDS_IN_SECOND = 1000L private const val HOPS_AWAY_UNAVAILABLE = -1 diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index f46108cd7..fb2a9f015 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -195,7 +195,6 @@ class MeshService : Service() { override fun onDestroy() { Logger.i { "Destroying mesh service" } ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - serviceRepository.cancelPendingRetries() serviceJob.cancel() super.onDestroy() } diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt index 6419e39df..1314ddb7e 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshDataHandlerTest.kt @@ -17,12 +17,10 @@ package com.geeksville.mesh.service import dagger.Lazy -import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -37,13 +35,11 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.service.MeshServiceNotifications -import org.meshtastic.core.service.RetryEvent import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.filter.MessageFilterService import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum -import org.meshtastic.proto.Routing import org.meshtastic.proto.StoreForwardPlusPlus class MeshDataHandlerTest { @@ -157,58 +153,4 @@ class MeshDataHandlerTest { ) } } - - @Test - fun `handleAckNak triggers RetryEvent when MAX_RETRANSMIT and conditions met`() = runTest { - val requestId = 555 - val originalPacket = - org.meshtastic.core.database.entity.Packet( - uuid = 1L, - myNodeNum = 123, - packetId = requestId, - port_num = PortNum.TEXT_MESSAGE_APP.value, - contact_key = "contact", - received_time = System.currentTimeMillis(), - read = true, - data = - DataPacket( - to = "recipient", - bytes = "Important Message".encodeToByteArray().toByteString(), - dataType = PortNum.TEXT_MESSAGE_APP.value, - channel = 0, - id = requestId, - from = "!0000007b", // ID_LOCAL or my ID - retryCount = 0, - ), - ) - - coEvery { packetRepository.getPacketById(requestId) } returns originalPacket - coEvery { packetRepository.getReactionByPacketId(requestId) } returns null - coEvery { serviceRepository.requestRetry(any(), any()) } returns true - every { commandSender.generatePacketId() } returns 888 - - val routingPayload = Routing(error_reason = Routing.Error.MAX_RETRANSMIT) - val payload = Routing.ADAPTER.encode(routingPayload).toByteString() - - val meshPacket = - MeshPacket( - from = 123, - decoded = Data(portnum = PortNum.ROUTING_APP, payload = payload, request_id = requestId), - id = 2002, - ) - - every { dataMapper.toNodeID(any()) } returns "!0000007b" - - meshDataHandler.handleReceivedData(meshPacket, 123) - - val retryEventSlot = slot() - coVerify { serviceRepository.requestRetry(capture(retryEventSlot), any()) } - - assert(retryEventSlot.captured.packetId == requestId) - assert(retryEventSlot.captured.attemptNumber == 1) - - // Verify update and resend - coVerify { packetRepository.update(any()) } - coVerify { commandSender.sendData(any()) } - } } diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/34.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/34.json index d87655c8b..8bc719502 100644 --- a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/34.json +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/34.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 34, - "identityHash": "34352663e54f76b7b9c13de31d9ac8e7", + "identityHash": "25bf8e7feb6d0e7f9eab4dfccf546e45", "entities": [ { "tableName": "my_node", @@ -611,7 +611,7 @@ }, { "tableName": "reactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `retry_count` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", "fields": [ { "fieldPath": "myNodeNum", @@ -686,13 +686,6 @@ "notNull": true, "defaultValue": "0" }, - { - "fieldPath": "retryCount", - "columnName": "retry_count", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, { "fieldPath": "relays", "columnName": "relays", @@ -1010,7 +1003,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '34352663e54f76b7b9c13de31d9ac8e7')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '25bf8e7feb6d0e7f9eab4dfccf546e45')" ] } } \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/35.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/35.json new file mode 100644 index 000000000..6423585de --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/35.json @@ -0,0 +1,1009 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "25bf8e7feb6d0e7f9eab4dfccf546e45", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '25bf8e7feb6d0e7f9eab4dfccf546e45')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index 6547d36bd..6a22a3d5b 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -91,7 +91,7 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity AutoMigration(from = 30, to = 31), AutoMigration(from = 31, to = 32), AutoMigration(from = 32, to = 33), - AutoMigration(from = 33, to = 34), + AutoMigration(from = 33, to = 34, spec = AutoMigration33to34::class), ], version = 34, exportSchema = true, @@ -126,3 +126,7 @@ class AutoMigration12to13 : AutoMigrationSpec @DeleteColumn.Entries(DeleteColumn(tableName = "packet", columnName = "reply_id")) class AutoMigration29to30 : AutoMigrationSpec + +@DeleteColumn(tableName = "packet", columnName = "retry_count") +@DeleteColumn(tableName = "reactions", columnName = "retry_count") +class AutoMigration33to34 : AutoMigrationSpec diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index f6b16fbcd..999b5d515 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -57,7 +57,6 @@ data class PacketEntity( viaMqtt = data.viaMqtt, relayNode = data.relayNode, relays = data.relays, - retryCount = data.retryCount, filtered = filtered, ) } @@ -140,7 +139,6 @@ data class Reaction( val packetId: Int = 0, val status: MessageStatus = MessageStatus.UNKNOWN, val routingError: Int = 0, - val retryCount: Int = 0, val relays: Int = 0, val relayNode: Int? = null, val to: String? = null, @@ -166,7 +164,6 @@ data class ReactionEntity( @ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0, @ColumnInfo(name = "status", defaultValue = "0") val status: MessageStatus = MessageStatus.UNKNOWN, @ColumnInfo(name = "routing_error", defaultValue = "0") val routingError: Int = 0, - @ColumnInfo(name = "retry_count", defaultValue = "0") val retryCount: Int = 0, @ColumnInfo(name = "relays", defaultValue = "0") val relays: Int = 0, @ColumnInfo(name = "relay_node") val relayNode: Int? = null, @ColumnInfo(name = "to") val to: String? = null, @@ -187,7 +184,6 @@ private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) packetId = packetId, status = status, routingError = routingError, - retryCount = retryCount, relays = relays, relayNode = relayNode, to = to, diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt index 03deaf5a9..e5c71cfb6 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt @@ -91,7 +91,6 @@ data class Message( val viaMqtt: Boolean = false, val relayNode: Int? = null, val relays: Int = 0, - val retryCount: Int = 0, val filtered: Boolean = false, ) { fun getStatusStringRes(): Pair { diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt index be956a73a..033268755 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt @@ -66,7 +66,6 @@ data class DataPacket( var relayNode: Int? = null, var relays: Int = 0, var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path - var retryCount: Int = 0, // Number of automatic retry attempts var emoji: Int = 0, @Serializable(with = ByteStringSerializer::class) @TypeParceler @@ -107,7 +106,6 @@ data class DataPacket( relayNode = if (parcel.readInt() == 0) null else parcel.readInt() relays = parcel.readInt() viaMqtt = parcel.readInt() != 0 - retryCount = parcel.readInt() emoji = parcel.readInt() sfppHash = ByteStringParceler.create(parcel) } diff --git a/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketParcelTest.kt b/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketParcelTest.kt index 7a2842e1b..94bf4f5a4 100644 --- a/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketParcelTest.kt +++ b/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketParcelTest.kt @@ -114,7 +114,6 @@ class DataPacketParcelTest { relayNode = 202, relays = 1, viaMqtt = true, - retryCount = 2, emoji = 0x1F600, sfppHash = "sfpp".toByteArray().toByteString(), ) @@ -137,7 +136,6 @@ class DataPacketParcelTest { assertEquals("relayNode", expected.relayNode, actual.relayNode) assertEquals("relays", expected.relays, actual.relays) assertEquals("viaMqtt", expected.viaMqtt, actual.viaMqtt) - assertEquals("retryCount", expected.retryCount, actual.retryCount) assertEquals("emoji", expected.emoji, actual.emoji) assertEquals("sfppHash", expected.sfppHash, actual.sfppHash) } diff --git a/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketTest.kt b/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketTest.kt index 850aaca4f..5dddd5858 100644 --- a/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketTest.kt +++ b/core/model/src/test/kotlin/org/meshtastic/core/model/DataPacketTest.kt @@ -102,7 +102,6 @@ class DataPacketTest { relayNode = 123, relays = 2, viaMqtt = true, - retryCount = 1, emoji = 10, sfppHash = sfppHash, ) @@ -132,7 +131,6 @@ class DataPacketTest { assertEquals(123, packetToUpdate.relayNode) assertEquals(2, packetToUpdate.relays) assertEquals(true, packetToUpdate.viaMqtt) - assertEquals(1, packetToUpdate.retryCount) assertEquals(10, packetToUpdate.emoji) assertEquals(sfppHash, packetToUpdate.sfppHash) diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt index 995b68f0b..63a3d7172 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt @@ -17,40 +17,17 @@ package org.meshtastic.core.service import co.touchlab.kermit.Logger -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.withTimeoutOrNull import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.MeshPacket -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton -sealed class RetryEvent { - abstract val packetId: Int - abstract val attemptNumber: Int - abstract val maxAttempts: Int - - data class MessageRetry( - override val packetId: Int, - val text: String, - override val attemptNumber: Int, - override val maxAttempts: Int, - ) : RetryEvent() - - data class ReactionRetry( - override val packetId: Int, - val emoji: String, - override val attemptNumber: Int, - override val maxAttempts: Int, - ) : RetryEvent() -} - data class TracerouteResponse( val message: String, val destinationNodeNum: Int, @@ -158,51 +135,4 @@ class ServiceRepository @Inject constructor() { suspend fun onServiceAction(action: ServiceAction) { _serviceAction.send(action) } - - // Retry management - private val _retryEvents = MutableStateFlow(null) - val retryEvents: StateFlow - get() = _retryEvents - - private val pendingRetries = ConcurrentHashMap>() - - /** - * Request a retry for a message or reaction. Emits a retry event to the UI and waits for user response. - * - * @param event The retry event containing packet information - * @param timeoutMs Maximum time to wait for user response (defaults to auto-retry) - * @return true if should proceed with retry, false if user cancelled - */ - suspend fun requestRetry(event: RetryEvent, timeoutMs: Long): Boolean { - val packetId = event.packetId - val deferred = CompletableDeferred() - pendingRetries[packetId] = deferred - - Logger.i { "ServiceRepository: Setting retry event for packet $packetId" } - _retryEvents.value = event - Logger.i { "ServiceRepository: Retry event set, waiting for response..." } - - // Wait for user response with timeout - // If timeout occurs (user doesn't respond), default to retry - val result = withTimeoutOrNull(timeoutMs) { deferred.await() } ?: true - Logger.i { "ServiceRepository: Retry result for packet $packetId: $result" } - return result - } - - /** - * Respond to a retry request. Called by the UI when user interacts with retry dialog. - * - * @param packetId The packet ID of the message/reaction - * @param shouldRetry true to proceed with retry, false to cancel - */ - fun respondToRetry(packetId: Int, shouldRetry: Boolean) { - pendingRetries.remove(packetId)?.complete(shouldRetry) - _retryEvents.value = null // Clear the event to prevent replay - } - - /** Cancel all pending retry requests. Should be called when service is stopped or restarted. */ - fun cancelPendingRetries() { - pendingRetries.forEach { (_, deferred) -> deferred.complete(false) } - pendingRetries.clear() - } } diff --git a/core/service/src/test/kotlin/org/meshtastic/core/service/ServiceRepositoryRetryTest.kt b/core/service/src/test/kotlin/org/meshtastic/core/service/ServiceRepositoryRetryTest.kt deleted file mode 100644 index 7976e9273..000000000 --- a/core/service/src/test/kotlin/org/meshtastic/core/service/ServiceRepositoryRetryTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.service - -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** Unit tests for ServiceRepository retry management functionality. */ -class ServiceRepositoryRetryTest { - - private lateinit var serviceRepository: ServiceRepository - - @Before - fun setUp() { - serviceRepository = ServiceRepository() - } - - @Test - fun `requestRetry returns true when user confirms`() = runTest { - val testEvent = - RetryEvent.MessageRetry(packetId = 123, text = "Test message", attemptNumber = 1, maxAttempts = 3) - - // Start retry request in background - val retryDeferred = async { serviceRepository.requestRetry(testEvent, timeoutMs = 5000) } - - // Wait for non-null event to be set - val emittedEvent = serviceRepository.retryEvents.first { it != null } - assertEquals(testEvent, emittedEvent) - - // Simulate user clicking "Retry Now" - serviceRepository.respondToRetry(testEvent.packetId, shouldRetry = true) - - // Verify result - val result = retryDeferred.await() - assertTrue("Expected retry to proceed", result) - } - - @Test - fun `requestRetry returns false when user cancels`() = runTest { - val testEvent = RetryEvent.ReactionRetry(packetId = 456, emoji = "👍", attemptNumber = 2, maxAttempts = 3) - - // Start retry request in background - val retryDeferred = async { serviceRepository.requestRetry(testEvent, timeoutMs = 5000) } - - // Wait for non-null event to be set - val emittedEvent = serviceRepository.retryEvents.first { it != null } - assertEquals(testEvent, emittedEvent) - - // Simulate user clicking "Cancel Retry" - serviceRepository.respondToRetry(testEvent.packetId, shouldRetry = false) - - // Verify result - val result = retryDeferred.await() - assertFalse("Expected retry to be cancelled", result) - } - - @Test - fun `requestRetry returns true on timeout when user does not respond`() = runTest { - val testEvent = - RetryEvent.MessageRetry(packetId = 789, text = "Timeout test", attemptNumber = 1, maxAttempts = 3) - - // Start retry request with short timeout - val result = serviceRepository.requestRetry(testEvent, timeoutMs = 100) - - // Should auto-retry on timeout - assertTrue("Expected auto-retry on timeout", result) - } - - @Test - fun `multiple simultaneous retry requests handled independently`() = runTest { - val event1 = RetryEvent.MessageRetry(packetId = 100, text = "Message 1", attemptNumber = 1, maxAttempts = 3) - val event2 = RetryEvent.MessageRetry(packetId = 200, text = "Message 2", attemptNumber = 1, maxAttempts = 3) - - // Start two retry requests simultaneously - val retry1 = async { serviceRepository.requestRetry(event1, timeoutMs = 5000) } - val retry2 = async { serviceRepository.requestRetry(event2, timeoutMs = 5000) } - - // Give time for events to be emitted - delay(50) - - // Respond differently to each - serviceRepository.respondToRetry(event1.packetId, shouldRetry = true) - serviceRepository.respondToRetry(event2.packetId, shouldRetry = false) - - // Verify results - val result1 = retry1.await() - val result2 = retry2.await() - - assertTrue("First retry should proceed", result1) - assertFalse("Second retry should be cancelled", result2) - } - - @Test - fun `cancelPendingRetries completes all pending requests with false`() = runTest { - val event1 = RetryEvent.MessageRetry(packetId = 111, text = "Message 1", attemptNumber = 1, maxAttempts = 3) - val event2 = RetryEvent.MessageRetry(packetId = 222, text = "Message 2", attemptNumber = 1, maxAttempts = 3) - - // Start two retry requests - val retry1 = async { serviceRepository.requestRetry(event1, timeoutMs = 10000) } - val retry2 = async { serviceRepository.requestRetry(event2, timeoutMs = 10000) } - - // Give time for requests to register - delay(50) - - // Cancel all pending retries - serviceRepository.cancelPendingRetries() - - // Verify both completed with false - val result1 = retry1.await() - val result2 = retry2.await() - - assertFalse("First retry should be cancelled", result1) - assertFalse("Second retry should be cancelled", result2) - } - - @Test - fun `retryEvents are cleared after user responds`() = runTest { - val testEvent = RetryEvent.MessageRetry(packetId = 333, text = "Clear test", attemptNumber = 1, maxAttempts = 3) - - // Start retry request - val retryDeferred = async { serviceRepository.requestRetry(testEvent, timeoutMs = 5000) } - - // Wait for event to be set - val emittedEvent = serviceRepository.retryEvents.first { it != null } - assertEquals("Should receive event", testEvent, emittedEvent) - - // Respond to the retry - serviceRepository.respondToRetry(testEvent.packetId, shouldRetry = true) - - // Wait for response to complete - retryDeferred.await() - - // Verify event is cleared - assertEquals("Event should be cleared after responding", null, serviceRepository.retryEvents.value) - } - - @Test - fun `respondToRetry does nothing for unknown packetId`() = runTest { - // This should not throw or cause issues - serviceRepository.respondToRetry(999, shouldRetry = true) - // Test passes if no exception thrown - } -} diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 202ec96b4..1356b4875 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -55,12 +55,6 @@ Queued for sending Routing via SF++ chain… Confirmed on SF++ chain - Retries: %1$d / %2$d - Message Failed to Send - Retrying in %1$d seconds… (Attempt %2$d of %3$d) - Retrying reaction in %1$d seconds… (Attempt %2$d of %3$d) - Retry Now - Cancel Retry Acknowledged No route Received a negative acknowledgment @@ -657,7 +651,7 @@ IPv4 mode IP Gateway - Subnet + Subred Paxcounter Config Paxcounter enabled Status Message @@ -940,7 +934,7 @@ Notifications for newly discovered nodes. Low Battery Notifications for low battery alerts for the connected device. - Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center. + Select packets sent as critical will ignore the msg switch and Do Not Disturb settings in the OS notification center. Configure notification permissions Phone Location Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from settings. diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt index 5778d5a2b..65d73f6c7 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt @@ -31,7 +31,6 @@ import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.close -import org.meshtastic.core.strings.message_retry_count import org.meshtastic.core.strings.relays import org.meshtastic.core.strings.resend import org.meshtastic.core.ui.component.MeshtasticDialog @@ -44,8 +43,6 @@ fun DeliveryInfo( text: StringResource? = null, relayNodeName: String? = null, relays: Int = 0, - retryCount: Int = 0, - maxRetries: Int = 0, onConfirm: (() -> Unit) = {}, onDismiss: () -> Unit = {}, ) = MeshtasticDialog( @@ -63,14 +60,6 @@ fun DeliveryInfo( style = MaterialTheme.typography.bodyMedium, ) } - if (maxRetries > 0) { - Text( - text = stringResource(Res.string.message_retry_count, retryCount, maxRetries), - modifier = Modifier.padding(top = 8.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - ) - } if (relays != 0) { Text( text = pluralStringResource(Res.plurals.relays, relays, relays), diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt index 95b0e391f..7e3ae510b 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -105,7 +105,6 @@ import org.meshtastic.core.database.model.Message import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.util.getChannel -import org.meshtastic.core.service.RetryEvent import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.alert_bell_text import org.meshtastic.core.strings.cancel_reply @@ -138,7 +137,6 @@ import org.meshtastic.core.ui.component.SecurityIcon import org.meshtastic.core.ui.component.SharedContactDialog import org.meshtastic.core.ui.component.smartScrollToIndex import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.feature.messaging.component.RetryConfirmationDialog import org.meshtastic.proto.ChannelSet import java.nio.charset.StandardCharsets @@ -189,24 +187,6 @@ fun MessageScreen( val showFiltered by viewModel.showFiltered.collectAsStateWithLifecycle() val filteringDisabled = contactSettings[contactKey]?.filteringDisabled ?: false - // Retry dialog state - var currentRetryEvent by remember { mutableStateOf(null) } - - // Observe retry events from the service - // Key on contactKey to restart collection when navigating between conversations - LaunchedEffect(contactKey) { - android.util.Log.d("MessageScreen", "Starting retry event collection for contact: $contactKey") - viewModel.retryEvents.collect { event -> - if (event != null) { - android.util.Log.d("MessageScreen", "Received retry event: ${event.packetId}") - currentRetryEvent = event - } else { - android.util.Log.d("MessageScreen", "Retry event cleared") - currentRetryEvent = null - } - } - } - // Prevent the message TextField from stealing focus when the screen opens LaunchedEffect(contactKey) { focusManager.clearFocus() } @@ -323,29 +303,6 @@ fun MessageScreen( sharedContact?.let { contact -> SharedContactDialog(contact = contact, onDismiss = { sharedContact = null }) } - // Show retry confirmation dialog - currentRetryEvent?.let { event -> - RetryConfirmationDialog( - retryEvent = event, - countdownSeconds = 5, - onConfirm = { - // User clicked "Retry Now" - proceed immediately - viewModel.respondToRetry(event.packetId, shouldRetry = true) - currentRetryEvent = null - }, - onCancel = { - // User clicked "Cancel Retry" - stop retrying - viewModel.respondToRetry(event.packetId, shouldRetry = false) - currentRetryEvent = null - }, - onTimeout = { - // Countdown reached 0 - auto-retry - viewModel.respondToRetry(event.packetId, shouldRetry = true) - currentRetryEvent = null - }, - ) - } - Scaffold( modifier = Modifier.fillMaxSize(), topBar = { diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index 9aa81b528..c5c50c82d 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -122,8 +122,6 @@ internal fun MessageListPaged( nodes = state.nodes, ourNode = state.ourNode, resendOption = message.status?.equals(MessageStatus.ERROR) ?: false, - retryCount = message.retryCount, - maxRetries = 2, onResend = { handlers.onDeleteMessages(listOf(message.uuid)) handlers.onSendMessage(message.text, state.contactKey) @@ -510,8 +508,6 @@ internal fun MessageStatusDialog( nodes: List, ourNode: Node?, resendOption: Boolean, - retryCount: Int, - maxRetries: Int, onResend: () -> Unit, onDismiss: () -> Unit, ) { @@ -530,8 +526,6 @@ internal fun MessageStatusDialog( text = text, relayNodeName = relayNodeName, relays = message.relays, - retryCount = retryCount, - maxRetries = maxRetries, onConfirm = onResend, onDismiss = onDismiss, ) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 260b36ad0..7e3130d9d 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -93,8 +93,6 @@ constructor( val contactSettings: StateFlow> = packetRepository.getContactSettings().stateInWhileSubscribed(initialValue = emptyMap()) - val retryEvents = serviceRepository.retryEvents - private val contactKeyForPagedMessages: MutableStateFlow = MutableStateFlow(null) private val pagedMessagesForContactKey: Flow> = combine(contactKeyForPagedMessages.filterNotNull(), _showFiltered, contactSettings) { @@ -270,8 +268,4 @@ constructor( Logger.e { "Send DataPacket error: ${ex.message}" } } } - - fun respondToRetry(packetId: Int, shouldRetry: Boolean) { - serviceRepository.respondToRetry(packetId, shouldRetry) - } } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index aeb72693e..06b80bed8 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -232,8 +232,6 @@ internal fun ReactionDialog( onDismiss = { showStatusDialog = null }, relayNodeName = relayNodeName, relays = reaction.relays, - retryCount = reaction.retryCount, - maxRetries = 2, ) } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/RetryConfirmationDialog.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/RetryConfirmationDialog.kt deleted file mode 100644 index 791892ad2..000000000 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/RetryConfirmationDialog.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.messaging.component - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.service.RetryEvent -import org.meshtastic.core.strings.Res -import org.meshtastic.core.strings.retry_dialog_cancel -import org.meshtastic.core.strings.retry_dialog_confirm -import org.meshtastic.core.strings.retry_dialog_message -import org.meshtastic.core.strings.retry_dialog_reaction_message -import org.meshtastic.core.strings.retry_dialog_title -import org.meshtastic.core.ui.component.MeshtasticDialog - -private const val COUNTDOWN_DELAY_MS = 1000L -private const val MESSAGE_PREVIEW_LENGTH = 50 - -@Composable -private fun RetryDialogContent(retryEvent: RetryEvent, timeRemaining: Int) { - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - when (retryEvent) { - is RetryEvent.MessageRetry -> { - // Show message preview - if (retryEvent.text.isNotEmpty()) { - Text( - text = - "\"${retryEvent.text.take(MESSAGE_PREVIEW_LENGTH)}${ - if (retryEvent.text.length > MESSAGE_PREVIEW_LENGTH) "…" else "" - }\"", - modifier = Modifier.padding(bottom = 8.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Text( - text = - stringResource( - Res.string.retry_dialog_message, - timeRemaining, - retryEvent.attemptNumber, - retryEvent.maxAttempts, - ), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - ) - } - is RetryEvent.ReactionRetry -> { - // Show emoji preview - Text( - text = retryEvent.emoji, - modifier = Modifier.padding(bottom = 8.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.displayMedium, - ) - Text( - text = - stringResource( - Res.string.retry_dialog_reaction_message, - timeRemaining, - retryEvent.attemptNumber, - retryEvent.maxAttempts, - ), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - } -} - -@Composable -fun RetryConfirmationDialog( - retryEvent: RetryEvent, - countdownSeconds: Int = 5, - onConfirm: () -> Unit, - onCancel: () -> Unit, - onTimeout: () -> Unit, -) { - var timeRemaining by remember { mutableIntStateOf(countdownSeconds) } - - LaunchedEffect(retryEvent.packetId) { - timeRemaining = countdownSeconds // Reset countdown for new event - while (timeRemaining > 0) { - delay(COUNTDOWN_DELAY_MS) - timeRemaining-- - } - // Countdown reached 0, auto-retry - onTimeout() - } - - MeshtasticDialog( - onDismiss = onCancel, - dismissText = stringResource(Res.string.retry_dialog_cancel), - confirmText = stringResource(Res.string.retry_dialog_confirm), - onConfirm = onConfirm, - title = stringResource(Res.string.retry_dialog_title), - text = { RetryDialogContent(retryEvent, timeRemaining) }, - dismissable = false, - ) -}