diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/11.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/11.json new file mode 100644 index 000000000..cd11409ff --- /dev/null +++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/11.json @@ -0,0 +1,656 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "d5d55af54cacbb3f4f42f8e96e91acda", + "entities": [ + { + "tableName": "MyNodeInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `hasGPS` 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, `channelUtilization` REAL NOT NULL, `airUtilTx` REAL NOT NULL, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGPS", + "columnName": "hasGPS", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "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": "channelUtilization", + "columnName": "channelUtilization", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "airUtilTx", + "columnName": "airUtilTx", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NodeInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `lastHeard` INTEGER NOT NULL, `channel` INTEGER NOT NULL, `hopsAway` INTEGER NOT NULL DEFAULT 0, `user_id` TEXT, `user_longName` TEXT, `user_shortName` TEXT, `user_hwModel` TEXT, `user_isLicensed` INTEGER, `user_role` INTEGER DEFAULT 0, `position_latitude` REAL, `position_longitude` REAL, `position_altitude` INTEGER, `position_time` INTEGER, `position_satellitesInView` INTEGER, `position_groundSpeed` INTEGER, `position_groundTrack` INTEGER, `position_precisionBits` INTEGER, `devMetrics_time` INTEGER, `devMetrics_batteryLevel` INTEGER, `devMetrics_voltage` REAL, `devMetrics_channelUtilization` REAL, `devMetrics_airUtilTx` REAL, `devMetrics_uptimeSeconds` INTEGER, `envMetrics_time` INTEGER, `envMetrics_temperature` REAL, `envMetrics_relativeHumidity` REAL, `envMetrics_barometricPressure` REAL, `envMetrics_gasResistance` REAL, `envMetrics_voltage` REAL, `envMetrics_current` REAL, `envMetrics_iaq` INTEGER, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "lastHeard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "user.id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.longName", + "columnName": "user_longName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.shortName", + "columnName": "user_shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.hwModel", + "columnName": "user_hwModel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.isLicensed", + "columnName": "user_isLicensed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "user.role", + "columnName": "user_role", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "position.latitude", + "columnName": "position_latitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "position.longitude", + "columnName": "position_longitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "position.altitude", + "columnName": "position_altitude", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position.time", + "columnName": "position_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position.satellitesInView", + "columnName": "position_satellitesInView", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position.groundSpeed", + "columnName": "position_groundSpeed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position.groundTrack", + "columnName": "position_groundTrack", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "position.precisionBits", + "columnName": "position_precisionBits", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.time", + "columnName": "devMetrics_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.batteryLevel", + "columnName": "devMetrics_batteryLevel", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.voltage", + "columnName": "devMetrics_voltage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.channelUtilization", + "columnName": "devMetrics_channelUtilization", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.airUtilTx", + "columnName": "devMetrics_airUtilTx", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "deviceMetrics.uptimeSeconds", + "columnName": "devMetrics_uptimeSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.time", + "columnName": "envMetrics_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.temperature", + "columnName": "envMetrics_temperature", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.relativeHumidity", + "columnName": "envMetrics_relativeHumidity", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.barometricPressure", + "columnName": "envMetrics_barometricPressure", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.gasResistance", + "columnName": "envMetrics_gasResistance", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.voltage", + "columnName": "envMetrics_voltage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.current", + "columnName": "envMetrics_current", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "environmentMetrics.iaq", + "columnName": "envMetrics_iaq", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, 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", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": false + }, + { + "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": "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 + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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)", + "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" + } + ], + "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`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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, 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 + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, 'd5d55af54cacbb3f4f42f8e96e91acda')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt index 9188540b7..b4bf85855 100644 --- a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt +++ b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt @@ -36,8 +36,9 @@ import com.geeksville.mesh.database.entity.QuickChatAction AutoMigration (from = 7, to = 8), AutoMigration (from = 8, to = 9), AutoMigration (from = 9, to = 10), + AutoMigration (from = 10, to = 11), ], - version = 10, + version = 11, exportSchema = true, ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt index 63d75bd70..b3cc3925e 100644 --- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt @@ -50,8 +50,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz packetDao.updateMessageId(d, id) } - suspend fun getDataPacketById(requestId: Int) = withContext(Dispatchers.IO) { - packetDao.getDataPacketById(requestId) + suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) { + packetDao.getPacketById(requestId) } suspend fun deleteMessages(uuidList: List) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt index 8f4317a09..eae63234c 100644 --- a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt +++ b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt @@ -130,10 +130,15 @@ interface PacketDao { ) fun getDataPackets(): List - @Transaction - fun getDataPacketById(requestId: Int): DataPacket? { - return getDataPackets().lastOrNull { it.id == requestId } - } + @Query( + """ + SELECT * FROM packet + WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo)) + AND packet_id = :requestId + ORDER BY received_time DESC + """ + ) + fun getPacketById(requestId: Int): Packet? @Transaction fun getQueuedPackets(): List? = diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt index 69479af63..3952519f1 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt @@ -22,7 +22,9 @@ data class Packet( @ColumnInfo(name = "contact_key") val contact_key: String, @ColumnInfo(name = "received_time") val received_time: Long, @ColumnInfo(name = "read", defaultValue = "1") val read: Boolean, - @ColumnInfo(name = "data") val data: DataPacket + @ColumnInfo(name = "data") val data: DataPacket, + @ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0, + @ColumnInfo(name = "routing_error", defaultValue = "-1") var routingError: Int = -1, ) @Entity(tableName = "contact_settings") diff --git a/app/src/main/java/com/geeksville/mesh/model/Message.kt b/app/src/main/java/com/geeksville/mesh/model/Message.kt new file mode 100644 index 000000000..fe791edf2 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/Message.kt @@ -0,0 +1,52 @@ +package com.geeksville.mesh.model + +import com.geeksville.mesh.MeshProtos.Routing +import com.geeksville.mesh.MeshUser +import com.geeksville.mesh.MessageStatus +import com.geeksville.mesh.R + +val Routing.Error.stringRes: Int + get() = when (this) { + Routing.Error.NONE -> R.string.routing_error_none + Routing.Error.NO_ROUTE -> R.string.routing_error_no_route + Routing.Error.GOT_NAK -> R.string.routing_error_got_nak + Routing.Error.TIMEOUT -> R.string.routing_error_timeout + Routing.Error.NO_INTERFACE -> R.string.routing_error_no_interface + Routing.Error.MAX_RETRANSMIT -> R.string.routing_error_max_retransmit + Routing.Error.NO_CHANNEL -> R.string.routing_error_no_channel + Routing.Error.TOO_LARGE -> R.string.routing_error_too_large + Routing.Error.NO_RESPONSE -> R.string.routing_error_no_response + Routing.Error.DUTY_CYCLE_LIMIT -> R.string.routing_error_duty_cycle_limit + Routing.Error.BAD_REQUEST -> R.string.routing_error_bad_request + Routing.Error.NOT_AUTHORIZED -> R.string.routing_error_not_authorized + Routing.Error.PKI_FAILED -> R.string.routing_error_pki_failed + Routing.Error.PKI_UNKNOWN_PUBKEY -> R.string.routing_error_pki_unknown_pubkey + else -> R.string.unrecognized + } + +data class Message( + val uuid: Long, + val receivedTime: Long, + val user: MeshUser, + val text: String, + val time: Long, + val read: Boolean, + val status: MessageStatus?, + val routingError: Int, +) { + private fun getStatusStringRes(value: Int): Int { + val error = Routing.Error.forNumber(value) ?: Routing.Error.UNRECOGNIZED + return error.stringRes + } + + fun getStatusStringRes(): Pair { + val title = if (routingError > 0) R.string.error else R.string.message_delivery_status + val text = when (status) { + MessageStatus.RECEIVED -> R.string.delivery_confirmed + MessageStatus.QUEUED -> R.string.message_status_queued + MessageStatus.ENROUTE -> R.string.message_status_enroute + else -> getStatusStringRes(routingError) + } + return title to text + } +} diff --git a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt index eafabfb79..2e83b0732 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt @@ -463,7 +463,7 @@ class RadioConfigViewModel @Inject constructor( val parsed = MeshProtos.Routing.parseFrom(data.payload) debug(debugMsg.format(parsed.errorReason.name)) if (parsed.errorReason != MeshProtos.Routing.Error.NONE) { - setResponseStateError(parsed.errorReason.name) + setResponseStateError(app.getString(parsed.errorReason.stringRes)) } else if (packet.from == destNum && route.isEmpty()) { requestIds.update { it.apply { remove(data.requestId) } } if (requestIds.value.isEmpty()) setResponseStateSuccess() diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 80df57027..15d860e7b 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -130,16 +130,6 @@ data class Contact( val isMuted: Boolean, ) -data class Message( - val uuid: Long, - val receivedTime: Long, - val user: MeshUser, - val text: String, - val time: Long, - val read: Boolean, - val status: MessageStatus?, -) - // return time if within 24 hours, otherwise date internal fun getShortDateTime(time: Long): String? { val date = if (time != 0L) Date(time) else return null @@ -181,7 +171,6 @@ class UIViewModel @Inject constructor( private val _channels = MutableStateFlow(channelSet {}) val channels: StateFlow get() = _channels - val channelSet get() = channels.value private val _quickChatActions = MutableStateFlow>(emptyList()) val quickChatActions: StateFlow> = _quickChatActions @@ -339,6 +328,7 @@ class UIViewModel @Inject constructor( time = it.data.time, read = it.read, status = it.data.status, + routingError = it.routingError, ) } } 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 6abe56e52..868eb9d68 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -601,13 +601,14 @@ class MeshService : Service(), Logging { val contactKey = "${dataPacket.channel}$contactId" val packetToSave = Packet( - 0L, // autoGenerated - myNodeNum, - dataPacket.dataType, - contactKey, - System.currentTimeMillis(), - fromLocal, - dataPacket + uuid = 0L, // autoGenerated + myNodeNum = myNodeNum, + packetId = dataPacket.id, + port_num = dataPacket.dataType, + contact_key = contactKey, + received_time = System.currentTimeMillis(), + read = fromLocal, + data = dataPacket ) serviceScope.handledLaunch { packetRepository.get().apply { @@ -686,13 +687,12 @@ class MeshService : Service(), Logging { // We always send ACKs to other apps, because they might care about the messages they sent shouldBroadcast = true val u = MeshProtos.Routing.parseFrom(data.payload) - val isAck = u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE if (u.errorReason == MeshProtos.Routing.Error.DUTY_CYCLE_LIMIT) { radioConfigRepository.setErrorMessage(getString(R.string.error_duty_cycle)) } - handleAckNak(isAck, fromId, data.requestId) + handleAckNak(data.requestId, fromId, u.errorReasonValue) queueResponse.remove(data.requestId)?.complete(true) } @@ -1011,7 +1011,7 @@ class MeshService : Service(), Logging { private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000) { var dataPacket: DataPacket? = null while (dataPacket == null) { - dataPacket = packetRepository.get().getDataPacketById(packetId) + dataPacket = packetRepository.get().getPacketById(packetId)?.data if (dataPacket == null) delay(100) } dataPacket @@ -1031,14 +1031,21 @@ class MeshService : Service(), Logging { /** * Handle an ack/nak packet by updating sent message status */ - private fun handleAckNak(isAck: Boolean, fromId: String, requestId: Int) { + private fun handleAckNak(requestId: Int, fromId: String, routingError: Int) { serviceScope.handledLaunch { - val p = getDataPacketById(requestId) + val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE + val p = packetRepository.get().getPacketById(requestId) // distinguish real ACKs coming from the intended receiver - val m = if (isAck && fromId == p?.to) MessageStatus.RECEIVED - else if (isAck) MessageStatus.DELIVERED else MessageStatus.ERROR - if (p != null && p.status != MessageStatus.RECEIVED) - packetRepository.get().updateMessageStatus(p, m) + val m = when { + isAck && fromId == p?.data?.to -> MessageStatus.RECEIVED + isAck -> MessageStatus.DELIVERED + else -> MessageStatus.ERROR + } + if (p != null && p.data.status != MessageStatus.RECEIVED) { + p.data.status = m + p.routingError = routingError + packetRepository.get().update(p) + } serviceBroadcasts.broadcastMessageStatus(requestId, m) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index 04ecd5d01..70dccc5aa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -33,6 +33,7 @@ import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowRight +import androidx.compose.material.icons.twotone.Warning import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -42,7 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -686,15 +686,15 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un horizontalArrangement = Arrangement.Center, ) { Icon( - painterResource(R.drawable.ic_twotone_warning_24), - "warning", + imageVector = Icons.TwoTone.Warning, + contentDescription = "warning", modifier = Modifier.padding(end = 8.dp) ) Text( text = "${stringResource(title)}?\n") Icon( - painterResource(R.drawable.ic_twotone_warning_24), - "warning", + imageVector = Icons.TwoTone.Warning, + contentDescription = "warning", modifier = Modifier.padding(start = 8.dp) ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/MessageItem.kt index ba0fb7530..345c49963 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessageItem.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,14 +21,19 @@ import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Cloud +import androidx.compose.material.icons.twotone.CloudDone +import androidx.compose.material.icons.twotone.CloudOff +import androidx.compose.material.icons.twotone.CloudUpload +import androidx.compose.material.icons.twotone.HowToReg +import androidx.compose.material.icons.twotone.Warning import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -54,6 +60,7 @@ internal fun MessageItem( onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, onChipClick: () -> Unit = {}, + onStatusClick: () -> Unit = {}, ) { val fromLocal = shortName == null val messageColor = if (fromLocal) R.color.colorMyMsg else R.color.colorMsg @@ -134,18 +141,19 @@ internal fun MessageItem( fontSize = MaterialTheme.typography.caption.fontSize, ) AnimatedVisibility(visible = fromLocal) { - val icon = when (messageStatus) { - MessageStatus.RECEIVED -> R.drawable.ic_twotone_how_to_reg_24 - MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24 - MessageStatus.DELIVERED -> R.drawable.cloud_on - MessageStatus.ENROUTE -> R.drawable.ic_twotone_cloud_24 - MessageStatus.ERROR -> R.drawable.cloud_off - else -> R.drawable.ic_twotone_warning_24 - } Icon( - imageVector = ImageVector.vectorResource(id = icon), + imageVector = when (messageStatus) { + MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg + MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload + MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone + MessageStatus.ENROUTE -> Icons.TwoTone.Cloud + MessageStatus.ERROR -> Icons.TwoTone.CloudOff + else -> Icons.TwoTone.Warning + }, contentDescription = stringResource(R.string.message_delivery_status), - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier + .padding(start = 8.dp) + .clickable { onStatusClick() }, ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt b/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt index 8ade74e1b..c4fbe2ed7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt @@ -9,11 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.geeksville.mesh.DataPacket import com.geeksville.mesh.model.Message +import com.geeksville.mesh.ui.components.SimpleAlertDialog import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce @@ -34,6 +37,13 @@ internal fun MessageListView( AutoScrollToBottom(listState, messages) UpdateUnreadCount(listState, messages, onUnreadChanged) + var showStatusDialog by remember { mutableStateOf(null) } + if (showStatusDialog != null) { + val msg = showStatusDialog ?: return + val (title, text) = msg.getStatusStringRes() + SimpleAlertDialog(title = title, text = text) { showStatusDialog = null } + } + LazyColumn( modifier = Modifier.fillMaxSize(), state = listState, @@ -52,6 +62,7 @@ internal fun MessageListView( onClick = { onClick(msg) }, onLongClick = { onLongClick(msg) }, onChipClick = { onChipClick(msg) }, + onStatusClick = { showStatusDialog = msg } ) } } diff --git a/app/src/main/res/drawable/ic_twotone_cloud_24.xml b/app/src/main/res/drawable/ic_twotone_cloud_24.xml deleted file mode 100644 index d6cd3cf01..000000000 --- a/app/src/main/res/drawable/ic_twotone_cloud_24.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_twotone_how_to_reg_24.xml b/app/src/main/res/drawable/ic_twotone_how_to_reg_24.xml deleted file mode 100644 index 578905e5b..000000000 --- a/app/src/main/res/drawable/ic_twotone_how_to_reg_24.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_twotone_warning_24.xml b/app/src/main/res/drawable/ic_twotone_warning_24.xml deleted file mode 100644 index e75aa8b08..000000000 --- a/app/src/main/res/drawable/ic_twotone_warning_24.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbd4f8320..31d4e851c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,24 @@ Last heard via MQTT + Unrecognized + Waiting to be acknowledged + Queued for sending + Acknowledged + No route + Received a negative acknowledgment + Timeout + No Interface + Max Retransmission Reached + No Channel + Packet too large + No response + Bad Request + Regional Duty Cycle Limit Reached + Not Authorized + Encrypted Send Failed + Unknown Public Key + MSL ChUtil %.1f%% AirUtilTX %.1f%%