feat: add dialog for message status information

This commit is contained in:
andrekir 2024-09-24 19:39:20 -03:00 committed by Andre K
parent 056f6b28cf
commit a075dfbd3a
16 changed files with 803 additions and 103 deletions

View file

@ -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')"
]
}
}

View file

@ -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)

View file

@ -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<Long>) = withContext(Dispatchers.IO) {

View file

@ -130,10 +130,15 @@ interface PacketDao {
)
fun getDataPackets(): List<DataPacket>
@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<DataPacket>? =

View file

@ -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")

View file

@ -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<Int, Int> {
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
}
}

View file

@ -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()

View file

@ -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<AppOnlyProtos.ChannelSet> get() = _channels
val channelSet get() = channels.value
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
@ -339,6 +328,7 @@ class UIViewModel @Inject constructor(
time = it.data.time,
read = it.read,
status = it.data.status,
routingError = it.routingError,
)
}
}

View file

@ -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)
}
}

View file

@ -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)
)
}

View file

@ -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() },
)
}
}

View file

@ -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<Message?>(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 }
)
}
}

View file

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.21,12.04l-1.53,-0.11 -0.3,-1.5C16.88,7.86 14.62,6 12,6 9.94,6 8.08,7.14 7.12,8.96l-0.5,0.95 -1.07,0.11C3.53,10.24 2,11.95 2,14c0,2.21 1.79,4 4,4h13c1.65,0 3,-1.35 3,-3 0,-1.55 -1.22,-2.86 -2.79,-2.96z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4 0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11 0.5,-0.95C8.08,7.14 9.94,6 12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3z"/>
</vector>

View file

@ -1,20 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillAlpha="0.3"
android:fillColor="@android:color/white"
android:pathData="M11,8m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:strokeAlpha="0.3" />
<path
android:fillAlpha="0.3"
android:fillColor="@android:color/white"
android:pathData="M5,18h4.99L9,17l0.93,-0.94C7.55,16.33 5.2,17.37 5,18z"
android:strokeAlpha="0.3" />
<path
android:fillColor="@android:color/white"
android:pathData="M11,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM11,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM10,18L5,18c0.2,-0.63 2.55,-1.67 4.93,-1.94h0.03l0.46,-0.45L12,14.06c-0.39,-0.04 -0.68,-0.06 -1,-0.06 -2.67,0 -8,1.34 -8,4v2h9l-2,-2zM20.6,12.5l-5.13,5.17 -2.07,-2.08L12,17l3.47,3.5L22,13.91z" />
</vector>

View file

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillAlpha="0.3"
android:fillColor="@android:color/white"
android:pathData="M4.47,19h15.06L12,5.99 4.47,19zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"
android:strokeAlpha="0.3" />
<path
android:fillColor="@android:color/white"
android:pathData="M1,21h22L12,2 1,21zM4.47,19L12,5.99 19.53,19L4.47,19zM11,16h2v2h-2zM11,10h2v4h-2z" />
</vector>

View file

@ -25,6 +25,24 @@
<string name="node_sort_last_heard">Last heard</string>
<string name="node_sort_via_mqtt">via MQTT</string>
<string name="unrecognized">Unrecognized</string>
<string name="message_status_enroute">Waiting to be acknowledged</string>
<string name="message_status_queued">Queued for sending</string>
<string name="routing_error_none">Acknowledged</string>
<string name="routing_error_no_route">No route</string>
<string name="routing_error_got_nak">Received a negative acknowledgment</string>
<string name="routing_error_timeout">Timeout</string>
<string name="routing_error_no_interface">No Interface</string>
<string name="routing_error_max_retransmit">Max Retransmission Reached</string>
<string name="routing_error_no_channel">No Channel</string>
<string name="routing_error_too_large">Packet too large</string>
<string name="routing_error_no_response">No response</string>
<string name="routing_error_bad_request">Bad Request</string>
<string name="routing_error_duty_cycle_limit">Regional Duty Cycle Limit Reached</string>
<string name="routing_error_not_authorized">Not Authorized</string>
<string name="routing_error_pki_failed">Encrypted Send Failed</string>
<string name="routing_error_pki_unknown_pubkey">Unknown Public Key</string>
<string name="elevation_suffix" translatable="false">MSL</string>
<string name="channel_air_util" translatable="false">ChUtil %.1f%% AirUtilTX %.1f%%</string>