feat: support for switching between devices (#1078)

pull/1083/head
Andre K 2024-06-08 10:25:47 -03:00 zatwierdzone przez GitHub
rodzic 9ba44ad087
commit 5b3c78316b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
16 zmienionych plików z 934 dodań i 206 usunięć

Wyświetl plik

@ -0,0 +1,513 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "4bc80e30d6ff7782394dddc7aafb75ba",
"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, `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": "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": "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)",
"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
}
],
"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, '4bc80e30d6ff7782394dddc7aafb75ba')"
]
}
}

Wyświetl plik

@ -0,0 +1,141 @@
package com.geeksville.mesh
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PacketDaoTest {
private lateinit var database: MeshtasticDatabase
private lateinit var nodeInfoDao: NodeInfoDao
private lateinit var packetDao: PacketDao
private val myNodeInfo: MyNodeInfo = MyNodeInfo(
myNodeNum = 42424242,
hasGPS = false,
model = null,
firmwareVersion = null,
couldUpdate = false,
shouldUpdate = false,
currentPacketId = 1L,
messageTimeoutMsec = 5 * 60 * 1000,
minAppVersion = 1,
maxChannels = 8,
hasWifi = false,
channelUtilization = 0f,
airUtilTx = 0f,
)
private val myNodeNum: Int get() = myNodeInfo.myNodeNum
private val testContactKeys = listOf(
"0${DataPacket.ID_BROADCAST}",
"1!test1234",
)
private fun generateTestPackets(myNodeNum: Int) = testContactKeys.flatMap { contactKey ->
List(SAMPLE_SIZE) {
Packet(
uuid = 0L,
myNodeNum = myNodeNum,
port_num = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
contact_key = contactKey,
received_time = System.currentTimeMillis(),
read = false,
DataPacket(DataPacket.ID_BROADCAST, 0, "Message $it!"),
)
}
}
@Before
fun createDb(): Unit = runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
database = Room.inMemoryDatabaseBuilder(context, MeshtasticDatabase::class.java).build()
nodeInfoDao = database.nodeInfoDao().apply {
setMyNodeInfo(myNodeInfo)
}
packetDao = database.packetDao().apply {
generateTestPackets(42424243).forEach(::insert)
generateTestPackets(myNodeNum).forEach(::insert)
}
}
@After
fun closeDb() {
database.close()
}
@Test
fun test_myNodeNum() = runBlocking {
val myNodeInfo = nodeInfoDao.getMyNodeInfo().first()
assertEquals(myNodeNum, myNodeInfo?.myNodeNum)
}
@Test
fun test_getAllPackets() = runBlocking {
val packets = packetDao.getAllPackets(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE).first()
assertEquals(testContactKeys.size * SAMPLE_SIZE, packets.size)
val onlyMyNodeNum = packets.all { it.myNodeNum == myNodeNum }
assertTrue(onlyMyNodeNum)
}
@Test
fun test_getContactKeys() = runBlocking {
val contactKeys = packetDao.getContactKeys().first()
assertEquals(testContactKeys.size, contactKeys.size)
val onlyMyNodeNum = contactKeys.values.all { it.myNodeNum == myNodeNum }
assertTrue(onlyMyNodeNum)
}
@Test
fun test_getMessageCount() = runBlocking {
testContactKeys.forEach { contactKey ->
val messageCount = packetDao.getMessageCount(contactKey)
assertEquals(SAMPLE_SIZE, messageCount)
}
}
@Test
fun test_getMessagesFrom() = runBlocking {
testContactKeys.forEach { contactKey ->
val messages = packetDao.getMessagesFrom(contactKey).first()
assertEquals(SAMPLE_SIZE, messages.size)
val onlyFromContactKey = messages.all { it.contact_key == contactKey }
assertTrue(onlyFromContactKey)
val onlyMyNodeNum = messages.all { it.myNodeNum == myNodeNum }
assertTrue(onlyMyNodeNum)
}
}
@Test
fun test_deleteContacts() = runBlocking {
packetDao.deleteContacts(testContactKeys)
testContactKeys.forEach { contactKey ->
val messages = packetDao.getMessagesFrom(contactKey).first()
assertTrue(messages.isEmpty())
}
}
companion object {
private const val SAMPLE_SIZE = 10
}
}

Wyświetl plik

@ -130,7 +130,8 @@ data class DeviceMetrics(
val batteryLevel: Int = 0,
val voltage: Float,
val channelUtilization: Float,
val airUtilTx: Float
val airUtilTx: Float,
val uptimeSeconds: Int,
) : Parcelable {
companion object {
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
@ -143,12 +144,9 @@ data class DeviceMetrics(
p.batteryLevel,
p.voltage,
p.channelUtilization,
p.airUtilTx
p.airUtilTx,
p.uptimeSeconds,
)
override fun toString(): String {
return "DeviceMetrics(time=${time}, batteryLevel=${batteryLevel}, voltage=${voltage}, channelUtilization=${channelUtilization}, airUtilTx=${airUtilTx})"
}
}
@Parcelize
@ -160,6 +158,7 @@ data class EnvironmentMetrics(
val gasResistance: Float,
val voltage: Float,
val current: Float,
val iaq: Int,
) : Parcelable {
companion object {
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
@ -174,13 +173,10 @@ data class EnvironmentMetrics(
t.barometricPressure,
t.gasResistance,
t.voltage,
t.current
t.current,
t.iaq,
)
override fun toString(): String {
return "EnvironmentMetrics(time=${time}, temperature=${temperature}, humidity=${relativeHumidity}, pressure=${barometricPressure}), resistance=${gasResistance}, voltage=${voltage}, current=${current}"
}
fun getDisplayString(inFahrenheit: Boolean = false): String {
val temp = if (temperature != 0f) {
if (inFahrenheit) {
@ -195,6 +191,7 @@ data class EnvironmentMetrics(
val gas = if (gasResistance != 0f) String.format("%.0fMΩ", gasResistance) else null
val voltage = if (voltage != 0f) String.format("%.2fV", voltage) else null
val current = if (current != 0f) String.format("%.1fmA", current) else null
val iaq = if (iaq != 0) "IAQ: $iaq" else null
return listOfNotNull(
temp,
@ -202,7 +199,8 @@ data class EnvironmentMetrics(
pressure,
gas,
voltage,
current
current,
iaq,
).joinToString(" ")
}

Wyświetl plik

@ -31,8 +31,9 @@ import com.geeksville.mesh.database.entity.QuickChatAction
AutoMigration (from = 4, to = 5),
AutoMigration (from = 5, to = 6),
AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8),
],
version = 7,
version = 8,
exportSchema = true,
)
@TypeConverters(Converters::class)

Wyświetl plik

@ -2,6 +2,7 @@ package com.geeksville.mesh.database
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
@ -15,12 +16,14 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
packetDaoLazy.get()
}
suspend fun getAllPackets(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
packetDao.getAllPackets()
}
fun getWaypoints(): Flow<List<Packet>> = packetDao.getAllPackets(PortNum.WAYPOINT_APP_VALUE)
fun getContacts(): Flow<Map<String, Packet>> = packetDao.getContactKeys()
suspend fun getMessageCount(contact: String): Int = withContext(Dispatchers.IO) {
packetDao.getMessageCount(contact)
}
suspend fun getQueuedPackets(): List<DataPacket>? = withContext(Dispatchers.IO) {
packetDao.getQueuedPackets()
}
@ -29,9 +32,7 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
packetDao.insert(packet)
}
suspend fun getMessagesFrom(contact: String) = withContext(Dispatchers.IO) {
packetDao.getMessagesFrom(contact)
}
fun getMessagesFrom(contact: String) = packetDao.getMessagesFrom(contact)
suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(Dispatchers.IO) {
packetDao.updateMessageStatus(d, m)
@ -45,16 +46,16 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
packetDao.getDataPacketById(requestId)
}
suspend fun deleteAllMessages() = withContext(Dispatchers.IO) {
packetDao.deleteAllMessages()
}
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
for (chunk in uuidList.chunked(500)) { // limit number of UUIDs per query
packetDao.deleteMessages(chunk)
}
}
suspend fun deleteContacts(contactList: List<String>) = withContext(Dispatchers.IO) {
packetDao.deleteContacts(contactList)
}
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) {
packetDao.deleteWaypoint(id)
}

Wyświetl plik

@ -16,28 +16,70 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface PacketDao {
@Query("Select * from packet order by received_time asc")
fun getAllPackets(): Flow<List<Packet>>
@Query(
"""
SELECT * FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND port_num = :portNum
ORDER BY received_time ASC
"""
)
fun getAllPackets(portNum: Int): Flow<List<Packet>>
@Query("Select * from packet where port_num = 1 order by received_time desc")
@Query(
"""
SELECT * FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND port_num = 1
ORDER BY received_time DESC
"""
)
fun getContactKeys(): Flow<Map<@MapColumn(columnName = "contact_key") String, Packet>>
@Query(
"""
SELECT COUNT(*) FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND port_num = 1 AND contact_key = :contact
"""
)
suspend fun getMessageCount(contact: String): Int
@Insert
fun insert(packet: Packet)
@Query("Select * from packet where port_num = 1 and contact_key = :contact order by received_time asc")
@Query(
"""
SELECT * FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND port_num = 1 AND contact_key = :contact
ORDER BY received_time ASC
"""
)
fun getMessagesFrom(contact: String): Flow<List<Packet>>
@Query("Select * from packet where data = :data")
@Query(
"""
SELECT * FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND data = :data
"""
)
fun findDataPacket(data: DataPacket): Packet?
@Query("Delete from packet where port_num = 1")
fun deleteAllMessages()
@Query("Delete from packet where uuid in (:uuidList)")
@Query("DELETE FROM packet WHERE uuid in (:uuidList)")
fun deleteMessages(uuidList: List<Long>)
@Query("Delete from packet where uuid=:uuid")
@Query(
"""
DELETE FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND contact_key IN (:contactList)
"""
)
fun deleteContacts(contactList: List<String>)
@Query("DELETE FROM packet WHERE uuid=:uuid")
fun _delete(uuid: Long)
@Transaction
@ -60,7 +102,13 @@ interface PacketDao {
findDataPacket(data)?.let { update(it.copy(data = new)) }
}
@Query("Select data from packet order by received_time asc")
@Query(
"""
SELECT data FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
ORDER BY received_time ASC
"""
)
fun getDataPackets(): List<DataPacket>
@Transaction
@ -72,7 +120,14 @@ interface PacketDao {
fun getQueuedPackets(): List<DataPacket>? =
getDataPackets().filter { it.status == MessageStatus.QUEUED }
@Query("Select * from packet where port_num = 8 order by received_time asc")
@Query(
"""
SELECT * FROM packet
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo))
AND port_num = 8
ORDER BY received_time ASC
"""
)
fun getAllWaypoints(): List<Packet>
@Transaction

Wyświetl plik

@ -2,15 +2,26 @@ package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.geeksville.mesh.DataPacket
@Entity(tableName = "packet")
@Entity(
tableName = "packet",
indices = [
Index(value = ["myNodeNum"]),
Index(value = ["port_num"]),
Index(value = ["contact_key"]),
]
)
data class Packet(
@PrimaryKey(autoGenerate = true) val uuid: Long,
@ColumnInfo(name = "myNodeNum", defaultValue = "0") val myNodeNum: Int,
@ColumnInfo(name = "port_num") val port_num: Int,
@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
)

Wyświetl plik

@ -36,11 +36,13 @@ fun Uri.toChannelSet(): ChannelSet {
val ChannelSet.subscribeList: List<String>
get() = settingsList.filter { it.downlinkEnabled }.map { Channel(it, loraConfig).name }
fun ChannelSet.getChannel(index: Int): Channel? =
if (settingsCount > index) Channel(getSettings(index), loraConfig) else null
/**
* Return the primary channel info
*/
val ChannelSet.primaryChannel: Channel?
get() = if (settingsCount > 0) Channel(getSettings(0), loraConfig) else null
val ChannelSet.primaryChannel: Channel? get() = getChannel(0)
/**
* Return a URL that represents the [ChannelSet]

Wyświetl plik

@ -0,0 +1,104 @@
package com.geeksville.mesh.model
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.repository.datastore.ChannelSetRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import java.text.DateFormat
import java.util.Date
import javax.inject.Inject
data class Contact(
val contactKey: String,
val shortName: String,
val longName: String,
val lastMessageTime: String?,
val lastMessageText: String?,
val unreadCount: Int,
val messageCount: Int,
val isMuted: Boolean,
)
// return time if within 24 hours, otherwise date/time
internal fun getShortDateTime(time: Long): String? {
val date = if (time != 0L) Date(time) else return null
val isWithin24Hours = System.currentTimeMillis() - date.time <= 24 * 60 * 60 * 1000L
return if (isWithin24Hours) {
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
} else {
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
}
}
@HiltViewModel
class ContactsViewModel @Inject constructor(
private val app: Application,
private val nodeDB: NodeDB,
channelSetRepository: ChannelSetRepository,
private val packetRepository: PacketRepository,
) : ViewModel(), Logging {
val contactList = combine(
nodeDB.myNodeInfo,
packetRepository.getContacts(),
channelSetRepository.channelSetFlow,
packetRepository.getContactSettings(),
) { myNodeInfo, contacts, channelSet, settings ->
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList()
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
val placeholder = (0 until channelSet.settingsCount).associate { ch ->
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data)
}
(placeholder + contacts).values.map { packet ->
val data = packet.data
val contactKey = packet.contact_key
// Determine if this is my message (originated on this device)
val fromLocal = data.from == DataPacket.ID_LOCAL
val toBroadcast = data.to == DataPacket.ID_BROADCAST
// grab usernames from NodeInfo
val node = nodeDB.nodes.value[if (fromLocal) data.to else data.from]
val shortName = node?.user?.shortName ?: app.getString(R.string.unknown_node_short_name)
val longName = if (toBroadcast) {
channelSet.getChannel(data.channel)?.name ?: app.getString(R.string.channel_name)
} else {
node?.user?.longName ?: app.getString(R.string.unknown_username)
}
Contact(
contactKey = contactKey,
shortName = if (toBroadcast) "${data.channel}" else shortName,
longName = longName,
lastMessageTime = getShortDateTime(data.time),
lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}",
unreadCount = 0,
messageCount = packetRepository.getMessageCount(contactKey),
isMuted = settings[contactKey]?.isMuted == true,
)
}
}.asLiveData()
fun setMuteUntil(contacts: List<String>, until: Long) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.setMuteUntil(contacts, until)
}
fun deleteContacts(contacts: List<String>) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteContacts(contacts)
}
}

Wyświetl plik

@ -18,7 +18,6 @@ import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
@ -127,9 +126,6 @@ class UIViewModel @Inject constructor(
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
val selectedBluetooth get() = radioInterfaceService.getDeviceAddress()?.getOrNull(0) == 'x'
private val _packets = MutableStateFlow<List<Packet>>(emptyList())
val packets: StateFlow<List<Packet>> = _packets
private val _localConfig = MutableStateFlow<LocalConfig>(LocalConfig.getDefaultInstance())
val localConfig: StateFlow<LocalConfig> = _localConfig
val config get() = _localConfig.value
@ -160,7 +156,7 @@ class UIViewModel @Inject constructor(
includeUnknown.value = !includeUnknown.value
}
val nodeViewState: StateFlow<NodesUiState> = combine(
val nodesUiState: StateFlow<NodesUiState> = combine(
nodeFilterText,
nodeSortOption,
includeUnknown,
@ -177,7 +173,7 @@ class UIViewModel @Inject constructor(
)
@OptIn(ExperimentalCoroutinesApi::class)
val filteredNodes: StateFlow<List<NodeInfo>> = nodeViewState.flatMapLatest { state ->
val nodeList: StateFlow<List<NodeInfo>> = nodesUiState.flatMapLatest { state ->
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
}.stateIn(
scope = viewModelScope,
@ -198,11 +194,6 @@ class UIViewModel @Inject constructor(
radioConfigRepository.clearErrorMessage()
}.launchIn(viewModelScope)
viewModelScope.launch {
packetRepository.getAllPackets().collect { packets ->
_packets.value = packets
}
}
radioConfigRepository.localConfigFlow.onEach { config ->
_localConfig.value = config
}.launchIn(viewModelScope)
@ -221,56 +212,13 @@ class UIViewModel @Inject constructor(
debug("ViewModel created")
}
private val _contactKey = MutableStateFlow("0${DataPacket.ID_BROADCAST}")
val contactKey: StateFlow<String> = _contactKey
fun setContactKey(contact: String) {
_contactKey.value = contact
}
fun getContactName(contactKey: String): String {
val (channel, dest) = contactKey[0].digitToIntOrNull() to contactKey.substring(1)
return if (channel == null || dest == DataPacket.ID_BROADCAST) {
// grab channel names from ChannelSet
val channelName = with(channelSet) {
if (channel != null && settingsCount > channel)
Channel(settingsList[channel], loraConfig).name else null
}
channelName ?: app.getString(R.string.channel_name)
} else {
// grab usernames from NodeInfo
val node = nodeDB.nodes.value[dest]
node?.user?.longName ?: app.getString(R.string.unknown_username)
}
}
fun getMessagesFrom(contactKey: String) = packetRepository.getMessagesFrom(contactKey)
@OptIn(ExperimentalCoroutinesApi::class)
val messages: LiveData<List<Packet>> = contactKey.flatMapLatest { contactKey ->
packetRepository.getMessagesFrom(contactKey)
}.asLiveData()
val contacts = combine(packetRepository.getContacts(), channels) { contacts, channelSet ->
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
val placeholder = (0 until channelSet.settingsCount).associate { ch ->
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
contactKey to Packet(0L, 1, contactKey, 0L, data)
}
contacts + (placeholder - contacts.keys)
}.asLiveData()
val contactSettings get() = packetRepository.getContactSettings()
fun setMuteUntil(contacts: List<String>, until: Long) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.setMuteUntil(contacts, until)
}
@OptIn(ExperimentalCoroutinesApi::class)
val waypoints: LiveData<Map<Int, Packet>> = _packets.mapLatest { list ->
list.filter { it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE }
.associateBy { packet -> packet.data.waypoint!!.id }
val waypoints = packetRepository.getWaypoints().mapLatest { list ->
list.associateBy { packet -> packet.data.waypoint!!.id }
.filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 }
}.asLiveData()
}
fun generatePacketId(): Int? {
return try {
@ -281,7 +229,7 @@ class UIViewModel @Inject constructor(
}
}
fun sendMessage(str: String, contactKey: String = this.contactKey.value) {
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
val dest = if (channel != null) contactKey.substring(1) else contactKey
@ -334,10 +282,6 @@ class UIViewModel @Inject constructor(
}
}
fun deleteAllMessages() = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteAllMessages()
}
fun deleteMessages(uuidList: List<Long>) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteMessages(uuidList)
}

Wyświetl plik

@ -595,9 +595,11 @@ class MeshService : Service(), Logging {
val packetToSave = Packet(
0L, // autoGenerated
myNodeNum,
dataPacket.dataType,
contactKey,
System.currentTimeMillis(),
true, // TODO isLocal
dataPacket
)
serviceScope.handledLaunch {

Wyświetl plik

@ -9,21 +9,16 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.AdapterContactLayoutBinding
import com.geeksville.mesh.databinding.FragmentContactsBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.ContactsViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.Date
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
@ -36,7 +31,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val model: UIViewModel by activityViewModels()
private val model: ContactsViewModel by activityViewModels()
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
@ -61,50 +56,31 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
return ViewHolder(contactsView)
}
var contacts = arrayOf<Packet>()
var contacts = arrayOf<Contact>()
var selectedList = ArrayList<String>()
var contactSettings = mapOf<String, ContactSettings>()
val isAllMuted get() = selectedList.all { contactSettings[it]?.isMuted == true }
private val selectedContacts get() = contacts.filter { it.contactKey in selectedList }
val isAllMuted get() = selectedContacts.all { it.isMuted }
val selectedCount get() = selectedContacts.sumOf { it.messageCount }
override fun getItemCount(): Int = contacts.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val packet = contacts[position]
val contact = packet.data
val contact = contacts[position]
// Determine if this is my message (originated on this device)
val fromLocal = contact.from == DataPacket.ID_LOCAL
val toBroadcast = contact.to == DataPacket.ID_BROADCAST
holder.shortName.text = contact.shortName
holder.longName.text = contact.longName
holder.lastMessageText.text = contact.lastMessageText
// grab usernames from NodeInfo
val nodes = model.nodeDB.nodes.value
val node = nodes[if (fromLocal) contact.to else contact.from]
//grab channel names from DeviceConfig
val channels = model.channelSet
val channelName = if (channels.settingsCount > contact.channel)
Channel(channels.settingsList[contact.channel], channels.loraConfig).name else null
val shortName = node?.user?.shortName ?: "???"
val longName = if (toBroadcast) channelName ?: getString(R.string.channel_name)
else node?.user?.longName ?: getString(R.string.unknown_username)
holder.shortName.text = if (toBroadcast) "${contact.channel}" else shortName
holder.longName.text = longName
val text = if (fromLocal) contact.text else "$shortName: ${contact.text}"
holder.lastMessageText.text = text
if (contact.time != 0L) {
if (contact.lastMessageTime != null) {
holder.lastMessageTime.visibility = View.VISIBLE
holder.lastMessageTime.text = getShortDateTime(Date(contact.time))
holder.lastMessageTime.text = contact.lastMessageTime
} else holder.lastMessageTime.visibility = View.INVISIBLE
holder.mutedIcon.isVisible = contactSettings[packet.contact_key]?.isMuted == true
holder.mutedIcon.isVisible = contact.isMuted
holder.itemView.setOnLongClickListener {
clickItem(holder, packet.contact_key)
clickItem(holder, contact.contactKey)
if (actionMode == null) {
actionMode =
(activity as AppCompatActivity).startSupportActionMode(actionModeCallback)
@ -112,18 +88,14 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
true
}
holder.itemView.setOnClickListener {
if (actionMode != null) clickItem(holder, packet.contact_key)
if (actionMode != null) clickItem(holder, contact.contactKey)
else {
debug("calling MessagesFragment filter:${packet.contact_key}")
model.setContactKey(packet.contact_key)
parentFragmentManager.beginTransaction()
.replace(R.id.mainActivityLayout, MessagesFragment())
.addToBackStack(null)
.commit()
debug("calling MessagesFragment filter:${contact.contactKey}")
parentFragmentManager.navigateToMessages(contact.contactKey, contact.longName)
}
}
if (selectedList.contains(packet.contact_key)) {
if (selectedList.contains(contact.contactKey)) {
holder.itemView.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 32f
@ -161,8 +133,8 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
notifyItemChanged(position)
}
fun onContactsChanged(contacts: Map<String, Packet>) {
this.contacts = contacts.values.toTypedArray()
fun onContactsChanged(contacts: List<Contact>) {
this.contacts = contacts.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
}
}
@ -186,19 +158,10 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
binding.contactsView.adapter = contactsAdapter
binding.contactsView.layoutManager = LinearLayoutManager(requireContext())
model.nodeDB.nodes.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.notifyDataSetChanged()
}
model.contacts.observe(viewLifecycleOwner) {
model.contactList.observe(viewLifecycleOwner) {
debug("New contacts received: ${it.size}")
contactsAdapter.onContactsChanged(it)
}
model.contactSettings.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.contactSettings = it
contactsAdapter.notifyDataSetChanged()
}
}
override fun onDestroyView() {
@ -261,28 +224,17 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
}
R.id.deleteButton -> {
val messagesTotal = model.packets.value.filter { it.port_num == 1 }
val selectedList = contactsAdapter.selectedList
val deleteList = ArrayList<Packet>()
// find messages for each contactId
selectedList.forEach { contact ->
deleteList += messagesTotal.filter { it.contact_key == contact }
}
val selectedCount = contactsAdapter.selectedCount
val deleteMessagesString = resources.getQuantityString(
R.plurals.delete_messages,
deleteList.size,
deleteList.size
selectedCount,
selectedCount
)
MaterialAlertDialogBuilder(requireContext())
.setMessage(deleteMessagesString)
.setPositiveButton(getString(R.string.delete)) { _, _ ->
debug("User clicked deleteButton")
// all items selected --> deleteAllMessages()
if (deleteList.size == messagesTotal.size) {
model.deleteAllMessages()
} else {
model.deleteMessages(deleteList.map { it.uuid })
}
model.deleteContacts(contactsAdapter.selectedList.toList())
mode.finish()
}
.setNeutralButton(R.string.cancel) { _, _ ->
@ -298,7 +250,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
// else --> select all
contactsAdapter.selectedList.clear()
contactsAdapter.contacts.forEach {
contactsAdapter.selectedList.add(it.contact_key)
contactsAdapter.selectedList.add(it.contactKey)
}
}
actionMode?.title = contactsAdapter.selectedList.size.toString()

Wyświetl plik

@ -9,8 +9,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.allViews
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
import androidx.recyclerview.widget.LinearLayoutManager
@ -43,6 +45,16 @@ internal fun getShortDateTime(date: Date): String {
}
}
internal fun FragmentManager.navigateToMessages(contactKey: String, contactName: String) {
val messagesFragment = MessagesFragment().apply {
arguments = bundleOf("contactKey" to contactKey, "contactName" to contactName)
}
beginTransaction()
.add(R.id.mainActivityLayout, messagesFragment)
.addToBackStack(null)
.commit()
}
@AndroidEntryPoint
class MessagesFragment : Fragment(), Logging {
@ -244,10 +256,14 @@ class MessagesFragment : Fragment(), Logging {
parentFragmentManager.popBackStack()
}
val contactKey = arguments?.getString("contactKey").toString()
val contactName = arguments?.getString("contactName").toString()
binding.messageTitle.text = contactName
fun sendMessageInputText() {
val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty()) {
model.sendMessage(str)
model.sendMessage(str, contactKey)
messagesAdapter.scrollToBottom()
}
binding.messageInputText.setText("") // blow away the string the user just entered
@ -267,8 +283,7 @@ class MessagesFragment : Fragment(), Logging {
layoutManager.stackFromEnd = true // We want the last rows to always be shown
binding.messageListView.layoutManager = layoutManager
model.messages.observe(viewLifecycleOwner) {
if (it.isNotEmpty() && it.first().contact_key != model.contactKey.value) return@observe
model.getMessagesFrom(contactKey).asLiveData().observe(viewLifecycleOwner) {
debug("New messages received: ${it.size}")
messagesAdapter.onMessagesChanged(it)
}
@ -286,10 +301,6 @@ class MessagesFragment : Fragment(), Logging {
}
}
model.contactKey.asLiveData().observe(viewLifecycleOwner) {
binding.messageTitle.text = model.getContactName(it)
}
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
actions?.let {
// This seems kinda hacky it might be better to replace with a recycler view
@ -313,7 +324,7 @@ class MessagesFragment : Fragment(), Logging {
binding.messageInputText.setText(newText)
binding.messageInputText.setSelection(newText.length)
} else {
model.sendMessage(action.message)
model.sendMessage(action.message, contactKey)
messagesAdapter.scrollToBottom()
}
}
@ -355,13 +366,7 @@ class MessagesFragment : Fragment(), Logging {
.setMessage(deleteMessagesString)
.setPositiveButton(getString(R.string.delete)) { _, _ ->
debug("User clicked deleteButton")
// all items selected --> deleteAllMessages()
val messagesTotal = model.packets.value.filter { it.port_num == 1 }
if (selectedList.size == messagesTotal.size) {
model.deleteAllMessages()
} else {
model.deleteMessages(selectedList.map { it.uuid })
}
model.deleteMessages(selectedList.map { it.uuid })
mode.finish()
}
.setNeutralButton(R.string.cancel) { _, _ ->

Wyświetl plik

@ -126,12 +126,9 @@ class UsersFragment : ScreenFragment("Users"), Logging {
popup.setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) {
R.id.direct_message -> {
debug("calling MessagesFragment filter: ${node.channel}${user.id}")
model.setContactKey("${node.channel}${user.id}")
parentFragmentManager.beginTransaction()
.replace(R.id.mainActivityLayout, MessagesFragment())
.addToBackStack(null)
.commit()
val contactKey = "${node.channel}${user.id}"
debug("calling MessagesFragment filter: $contactKey")
parentFragmentManager.navigateToMessages(contactKey, user.longName)
}
R.id.request_position -> {
debug("requesting position for '${user.longName}'")
@ -259,7 +256,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
binding.nodeFilter.initFilter()
model.filteredNodes.asLiveData().observe(viewLifecycleOwner) { nodeMap ->
model.nodeList.asLiveData().observe(viewLifecycleOwner) { nodeMap ->
nodesAdapter.onNodesChanged(nodeMap.toTypedArray())
}
@ -341,7 +338,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
private fun ComposeView.initFilter() {
this.setContent {
val nodeViewState by model.nodeViewState.collectAsStateWithLifecycle()
val nodeViewState by model.nodesUiState.collectAsStateWithLifecycle()
AppTheme {
Row(

Wyświetl plik

@ -18,7 +18,6 @@ import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -186,8 +185,8 @@ fun MapView(
requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}
val nodes by model.filteredNodes.collectAsStateWithLifecycle(emptyList())
val waypoints by model.waypoints.observeAsState(emptyMap())
val nodes by model.nodeList.collectAsStateWithLifecycle()
val waypoints by model.waypoints.collectAsStateWithLifecycle(emptyMap())
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
var showEditWaypointDialog by remember { mutableStateOf<Waypoint?>(null) }
@ -255,7 +254,7 @@ fun MapView(
fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback()
debug("marker long pressed id=${id}")
val waypoint = model.waypoints.value?.get(id)?.data?.waypoint ?: return
val waypoint = waypoints[id]?.data?.waypoint ?: return
// edit only when unlocked or lockedTo myNodeNum
if (waypoint.lockedTo in setOf(0, model.myNodeNum ?: 0) && model.isConnected())
showEditWaypointDialog = waypoint

Wyświetl plik

@ -28,7 +28,8 @@ class NodeInfoPreviewParameterProvider: PreviewParameterProvider<NodeInfo> {
channelUtilization = 2.4F,
airUtilTx = 3.5F,
batteryLevel = 85,
voltage = 3.7F
voltage = 3.7F,
uptimeSeconds = 3600,
),
user = MeshUser(
longName = "Micky Mouse",
@ -68,7 +69,8 @@ class NodeInfoPreviewParameterProvider: PreviewParameterProvider<NodeInfo> {
channelUtilization = 2.4F,
airUtilTx = 3.5F,
batteryLevel = 85,
voltage = 3.7F
voltage = 3.7F,
uptimeSeconds = 3600,
),
user = MeshUser(
longName = "Donald Duck, the Grand Duck of the Ducks",
@ -82,7 +84,8 @@ class NodeInfoPreviewParameterProvider: PreviewParameterProvider<NodeInfo> {
barometricPressure = 1013.25F,
gasResistance = 0.0F,
voltage = 3.7F,
current = 0.0F
current = 0.0F,
iaq = 100,
),
hopsAway = 2
)