feat(contact): add manually verified shared contact support (#3283)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
pull/3292/head
James Rich 2025-10-02 11:46:12 -05:00 zatwierdzone przez GitHub
rodzic 04991dbc5a
commit 24f0417b28
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 826 dodań i 9 usunięć

Wyświetl plik

@ -918,8 +918,17 @@ class MeshService : Service() {
sessionPasskey = a.sessionPasskey sessionPasskey = a.sessionPasskey
} }
private fun handleSharedContactImport(contact: AdminProtos.SharedContact) {
handleReceivedUser(contact.nodeNum, contact.user, manuallyVerified = true)
}
// Update our DB of users based on someone sending out a User subpacket // Update our DB of users based on someone sending out a User subpacket
private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User, channel: Int = 0) { private fun handleReceivedUser(
fromNum: Int,
p: MeshProtos.User,
channel: Int = 0,
manuallyVerified: Boolean = false,
) {
updateNodeInfo(fromNum) { updateNodeInfo(fromNum) {
val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET) val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET)
@ -936,6 +945,7 @@ class MeshService : Service() {
it.longName = p.longName it.longName = p.longName
it.shortName = p.shortName it.shortName = p.shortName
it.channel = channel it.channel = channel
it.manuallyVerified = manuallyVerified
if (newNode) { if (newNode) {
serviceNotifications.showNewNodeSeenNotification(it) serviceNotifications.showNewNodeSeenNotification(it)
} }
@ -1913,14 +1923,33 @@ class MeshService : Service() {
is ServiceAction.Favorite -> favoriteNode(action.node) is ServiceAction.Favorite -> favoriteNode(action.node)
is ServiceAction.Ignore -> ignoreNode(action.node) is ServiceAction.Ignore -> ignoreNode(action.node)
is ServiceAction.Reaction -> sendReaction(action) is ServiceAction.Reaction -> sendReaction(action)
is ServiceAction.AddSharedContact -> importContact(action.contact) is ServiceAction.ImportContact -> importContact(action.contact)
is ServiceAction.SendContact -> sendContact(action.contact)
} }
} }
} }
/**
* Imports a manually shared contact.
*
* This function takes a [AdminProtos.SharedContact] proto, marks it as manually verified, sends it for further
* processing, and then handles the import specific logic.
*
* @param contact The [AdminProtos.SharedContact] to be imported.
*/
private fun importContact(contact: AdminProtos.SharedContact) { private fun importContact(contact: AdminProtos.SharedContact) {
val verifiedContact = contact.copy { manuallyVerified = true }
sendContact(verifiedContact)
handleSharedContactImport(contact = verifiedContact)
}
/**
* Sends a shared contact to the radio via [AdminProtos.AdminMessage]
*
* @param contact The contact to send.
*/
private fun sendContact(contact: AdminProtos.SharedContact) {
packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { addContact = contact }) packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { addContact = contact })
handleReceivedUser(contact.nodeNum, contact.user)
} }
private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions { private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions {

Wyświetl plik

@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.channelSet import com.geeksville.mesh.channelSet
import com.geeksville.mesh.service.MeshServiceNotifications import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.sharedContact
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -40,12 +41,15 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Message import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val VERIFIED_CONTACT_FIRMWARE_CUTOFF = "2.7.12"
@HiltViewModel @HiltViewModel
class MessageViewModel class MessageViewModel
@Inject @Inject
@ -122,6 +126,20 @@ constructor(
fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
/**
* Sends a message to a contact or channel.
*
* If the message is a direct message (no channel specified), this function will:
* - If the device firmware version is older than 2.7.12, it will mark the destination node as a favorite to prevent
* it from being removed from the on-device node database.
* - If the device firmware version is 2.7.12 or newer, it will send a shared contact to the destination node.
*
* @param str The message content.
* @param contactKey The unique contact key, which is a combination of channel (optional) and node ID. Defaults to
* broadcasting on channel 0.
* @param replyId The ID of the message this is a reply to, if any.
*/
@Suppress("NestedBlockDepth")
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}", replyId: Int? = null) { fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}", replyId: Int? = null) {
// contactKey: unique contact key filter (channel)+(nodeId) // contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull() val channel = contactKey[0].digitToIntOrNull()
@ -130,9 +148,23 @@ constructor(
// if the destination is a node, we need to ensure it's a // if the destination is a node, we need to ensure it's a
// favorite so it does not get removed from the on-device node database. // favorite so it does not get removed from the on-device node database.
if (channel == null) { // no channel specified, so we assume it's a direct message if (channel == null) { // no channel specified, so we assume it's a direct message
val node = nodeRepository.getNode(dest) val fwVersion = ourNodeInfo.value?.metadata?.firmwareVersion
if (!node.isFavorite) { val destNode = nodeRepository.getNode(dest)
favoriteNode(nodeRepository.getNode(dest))
fwVersion?.let { fw ->
val ver = DeviceVersion(asString = fw)
val verifiedSharedContactsVersion =
DeviceVersion(
asString = VERIFIED_CONTACT_FIRMWARE_CUTOFF,
) // Version cutover to verified shared contacts
if (ver >= verifiedSharedContactsVersion) {
sendSharedContact(destNode)
} else {
if (!destNode.isFavorite) {
favoriteNode(destNode)
}
}
} }
} }
val p = DataPacket(dest, channel ?: 0, str, replyId) val p = DataPacket(dest, channel ?: 0, str, replyId)
@ -159,6 +191,19 @@ constructor(
} }
} }
private fun sendSharedContact(node: Node) = viewModelScope.launch {
try {
val contact = sharedContact {
nodeNum = node.num
user = node.user
manuallyVerified = node.manuallyVerified
}
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
} catch (ex: RemoteException) {
Timber.e(ex, "Send shared contact error")
}
}
private fun sendDataPacket(p: DataPacket) { private fun sendDataPacket(p: DataPacket) {
try { try {
serviceRepository.meshService?.send(p) serviceRepository.meshService?.send(p)

Wyświetl plik

@ -0,0 +1,735 @@
{
"formatVersion": 1,
"database": {
"version": 21,
"identityHash": "e490e68006ac5578aea2ce5c4c8a1fb5",
"entities": [
{
"tableName": "my_node",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, PRIMARY KEY(`myNodeNum`))",
"fields": [
{
"fieldPath": "myNodeNum",
"columnName": "myNodeNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "model",
"columnName": "model",
"affinity": "TEXT"
},
{
"fieldPath": "firmwareVersion",
"columnName": "firmwareVersion",
"affinity": "TEXT"
},
{
"fieldPath": "couldUpdate",
"columnName": "couldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shouldUpdate",
"columnName": "shouldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentPacketId",
"columnName": "currentPacketId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageTimeoutMsec",
"columnName": "messageTimeoutMsec",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minAppVersion",
"columnName": "minAppVersion",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxChannels",
"columnName": "maxChannels",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasWifi",
"columnName": "hasWifi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deviceId",
"columnName": "deviceId",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"myNodeNum"
]
}
},
{
"tableName": "nodes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`num`))",
"fields": [
{
"fieldPath": "num",
"columnName": "num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "longName",
"columnName": "long_name",
"affinity": "TEXT"
},
{
"fieldPath": "shortName",
"columnName": "short_name",
"affinity": "TEXT"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "snr",
"columnName": "snr",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "rssi",
"columnName": "rssi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastHeard",
"columnName": "last_heard",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deviceTelemetry",
"columnName": "device_metrics",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "channel",
"columnName": "channel",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "viaMqtt",
"columnName": "via_mqtt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hopsAway",
"columnName": "hops_away",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isFavorite",
"columnName": "is_favorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isIgnored",
"columnName": "is_ignored",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "environmentTelemetry",
"columnName": "environment_metrics",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "powerTelemetry",
"columnName": "power_metrics",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "paxcounter",
"columnName": "paxcounter",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "publicKey",
"columnName": "public_key",
"affinity": "BLOB"
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "manuallyVerified",
"columnName": "manually_verified",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"num"
]
}
},
{
"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, `reply_id` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` 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"
},
{
"fieldPath": "replyId",
"columnName": "reply_id",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "snr",
"columnName": "snr",
"affinity": "REAL",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "rssi",
"columnName": "rssi",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hopsAway",
"columnName": "hopsAway",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
}
],
"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`)"
}
]
},
{
"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"
]
}
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message_type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_date",
"columnName": "received_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw_message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fromNum",
"columnName": "from_num",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "portNum",
"columnName": "port_num",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "fromRadio",
"columnName": "from_radio",
"affinity": "BLOB",
"notNull": true,
"defaultValue": "x''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
},
"indices": [
{
"name": "index_log_from_num",
"unique": false,
"columnNames": [
"from_num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)"
},
{
"name": "index_log_port_num",
"unique": false,
"columnNames": [
"port_num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)"
}
]
},
{
"tableName": "quick_chat",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
}
},
{
"tableName": "reactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`reply_id`, `user_id`, `emoji`))",
"fields": [
{
"fieldPath": "replyId",
"columnName": "reply_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emoji",
"columnName": "emoji",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"reply_id",
"user_id",
"emoji"
]
},
"indices": [
{
"name": "index_reactions_reply_id",
"unique": false,
"columnNames": [
"reply_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)"
}
]
},
{
"tableName": "metadata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))",
"fields": [
{
"fieldPath": "num",
"columnName": "num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "proto",
"columnName": "proto",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"num"
]
},
"indices": [
{
"name": "index_metadata_num",
"unique": false,
"columnNames": [
"num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)"
}
]
},
{
"tableName": "device_hardware",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`hwModel`))",
"fields": [
{
"fieldPath": "activelySupported",
"columnName": "actively_supported",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "architecture",
"columnName": "architecture",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasInkHud",
"columnName": "has_ink_hud",
"affinity": "INTEGER"
},
{
"fieldPath": "hasMui",
"columnName": "has_mui",
"affinity": "INTEGER"
},
{
"fieldPath": "hwModel",
"columnName": "hwModel",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hwModelSlug",
"columnName": "hw_model_slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "images",
"columnName": "images",
"affinity": "TEXT"
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "partitionScheme",
"columnName": "partition_scheme",
"affinity": "TEXT"
},
{
"fieldPath": "platformioTarget",
"columnName": "platformio_target",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "requiresDfu",
"columnName": "requires_dfu",
"affinity": "INTEGER"
},
{
"fieldPath": "supportLevel",
"columnName": "support_level",
"affinity": "INTEGER"
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"hwModel"
]
}
},
{
"tableName": "firmware_release",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pageUrl",
"columnName": "page_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "releaseNotes",
"columnName": "release_notes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "zipUrl",
"columnName": "zip_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "releaseType",
"columnName": "release_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"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, 'e490e68006ac5578aea2ce5c4c8a1fb5')"
]
}
}

Wyświetl plik

@ -75,8 +75,9 @@ import org.meshtastic.core.database.entity.ReactionEntity
AutoMigration(from = 17, to = 18), AutoMigration(from = 17, to = 18),
AutoMigration(from = 18, to = 19), AutoMigration(from = 18, to = 19),
AutoMigration(from = 19, to = 20), AutoMigration(from = 19, to = 20),
AutoMigration(from = 20, to = 21),
], ],
version = 20, version = 21,
exportSchema = true, exportSchema = true,
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

Wyświetl plik

@ -61,6 +61,7 @@ data class NodeWithRelations(
powerMetrics = powerTelemetry.powerMetrics, powerMetrics = powerTelemetry.powerMetrics,
paxcounter = paxcounter, paxcounter = paxcounter,
notes = notes, notes = notes,
manuallyVerified = manuallyVerified,
) )
} }
@ -82,6 +83,7 @@ data class NodeWithRelations(
powerTelemetry = powerTelemetry, powerTelemetry = powerTelemetry,
paxcounter = paxcounter, paxcounter = paxcounter,
notes = notes, notes = notes,
manuallyVerified = manuallyVerified,
) )
} }
} }
@ -122,6 +124,8 @@ data class NodeEntity(
var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(), var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
@ColumnInfo(name = "public_key") var publicKey: ByteString? = null, @ColumnInfo(name = "public_key") var publicKey: ByteString? = null,
@ColumnInfo(name = "notes", defaultValue = "") var notes: String = "", @ColumnInfo(name = "notes", defaultValue = "") var notes: String = "",
@ColumnInfo(name = "manually_verified", defaultValue = "0")
var manuallyVerified: Boolean = false, // ONLY set true when scanned/imported manually
) { ) {
val deviceMetrics: TelemetryProtos.DeviceMetrics val deviceMetrics: TelemetryProtos.DeviceMetrics
get() = deviceTelemetry.deviceMetrics get() = deviceTelemetry.deviceMetrics

Wyświetl plik

@ -53,6 +53,7 @@ data class Node(
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(), val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
val publicKey: ByteString? = null, val publicKey: ByteString? = null,
val notes: String = "", val notes: String = "",
val manuallyVerified: Boolean = false,
) { ) {
val colors: Pair<Int, Int> val colors: Pair<Int, Int>
get() { // returns foreground and background @ColorInt for each 'num' get() { // returns foreground and background @ColorInt for each 'num'

Wyświetl plik

@ -29,5 +29,7 @@ sealed class ServiceAction {
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction() data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
data class AddSharedContact(val contact: AdminProtos.SharedContact) : ServiceAction() data class ImportContact(val contact: AdminProtos.SharedContact) : ServiceAction()
data class SendContact(val contact: AdminProtos.SharedContact) : ServiceAction()
} }

Wyświetl plik

@ -162,7 +162,7 @@ constructor(
} }
fun addSharedContact(sharedContact: AdminProtos.SharedContact) = fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) } viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) { fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact _sharedContactRequested.value = sharedContact