kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
				
				
				
			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
							rodzic
							
								
									04991dbc5a
								
							
						
					
					
						commit
						24f0417b28
					
				|  | @ -918,8 +918,17 @@ class MeshService : Service() { | |||
|         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 | ||||
|     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) { | ||||
|             val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET) | ||||
| 
 | ||||
|  | @ -936,6 +945,7 @@ class MeshService : Service() { | |||
|             it.longName = p.longName | ||||
|             it.shortName = p.shortName | ||||
|             it.channel = channel | ||||
|             it.manuallyVerified = manuallyVerified | ||||
|             if (newNode) { | ||||
|                 serviceNotifications.showNewNodeSeenNotification(it) | ||||
|             } | ||||
|  | @ -1913,14 +1923,33 @@ class MeshService : Service() { | |||
|                 is ServiceAction.Favorite -> favoriteNode(action.node) | ||||
|                 is ServiceAction.Ignore -> ignoreNode(action.node) | ||||
|                 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) { | ||||
|         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 }) | ||||
|         handleReceivedUser(contact.nodeNum, contact.user) | ||||
|     } | ||||
| 
 | ||||
|     private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions { | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel | |||
| import androidx.lifecycle.viewModelScope | ||||
| import com.geeksville.mesh.channelSet | ||||
| import com.geeksville.mesh.service.MeshServiceNotifications | ||||
| import com.geeksville.mesh.sharedContact | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| 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.Node | ||||
| import org.meshtastic.core.model.DataPacket | ||||
| import org.meshtastic.core.model.DeviceVersion | ||||
| import org.meshtastic.core.prefs.ui.UiPrefs | ||||
| import org.meshtastic.core.service.ServiceAction | ||||
| import org.meshtastic.core.service.ServiceRepository | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| private const val VERIFIED_CONTACT_FIRMWARE_CUTOFF = "2.7.12" | ||||
| 
 | ||||
| @HiltViewModel | ||||
| class MessageViewModel | ||||
| @Inject | ||||
|  | @ -122,6 +126,20 @@ constructor( | |||
| 
 | ||||
|     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) { | ||||
|         // contactKey: unique contact key filter (channel)+(nodeId) | ||||
|         val channel = contactKey[0].digitToIntOrNull() | ||||
|  | @ -130,9 +148,23 @@ constructor( | |||
|         // 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. | ||||
|         if (channel == null) { // no channel specified, so we assume it's a direct message | ||||
|             val node = nodeRepository.getNode(dest) | ||||
|             if (!node.isFavorite) { | ||||
|                 favoriteNode(nodeRepository.getNode(dest)) | ||||
|             val fwVersion = ourNodeInfo.value?.metadata?.firmwareVersion | ||||
|             val destNode = 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) | ||||
|  | @ -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) { | ||||
|         try { | ||||
|             serviceRepository.meshService?.send(p) | ||||
|  |  | |||
|  | @ -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')" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|  | @ -75,8 +75,9 @@ import org.meshtastic.core.database.entity.ReactionEntity | |||
|         AutoMigration(from = 17, to = 18), | ||||
|         AutoMigration(from = 18, to = 19), | ||||
|         AutoMigration(from = 19, to = 20), | ||||
|         AutoMigration(from = 20, to = 21), | ||||
|     ], | ||||
|     version = 20, | ||||
|     version = 21, | ||||
|     exportSchema = true, | ||||
| ) | ||||
| @TypeConverters(Converters::class) | ||||
|  |  | |||
|  | @ -61,6 +61,7 @@ data class NodeWithRelations( | |||
|             powerMetrics = powerTelemetry.powerMetrics, | ||||
|             paxcounter = paxcounter, | ||||
|             notes = notes, | ||||
|             manuallyVerified = manuallyVerified, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -82,6 +83,7 @@ data class NodeWithRelations( | |||
|             powerTelemetry = powerTelemetry, | ||||
|             paxcounter = paxcounter, | ||||
|             notes = notes, | ||||
|             manuallyVerified = manuallyVerified, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -122,6 +124,8 @@ data class NodeEntity( | |||
|     var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(), | ||||
|     @ColumnInfo(name = "public_key") var publicKey: ByteString? = null, | ||||
|     @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 | ||||
|         get() = deviceTelemetry.deviceMetrics | ||||
|  |  | |||
|  | @ -53,6 +53,7 @@ data class Node( | |||
|     val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(), | ||||
|     val publicKey: ByteString? = null, | ||||
|     val notes: String = "", | ||||
|     val manuallyVerified: Boolean = false, | ||||
| ) { | ||||
|     val colors: Pair<Int, Int> | ||||
|         get() { // returns foreground and background @ColorInt for each 'num' | ||||
|  |  | |||
|  | @ -29,5 +29,7 @@ sealed class 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() | ||||
| } | ||||
|  |  | |||
|  | @ -162,7 +162,7 @@ constructor( | |||
|     } | ||||
| 
 | ||||
|     fun addSharedContact(sharedContact: AdminProtos.SharedContact) = | ||||
|         viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) } | ||||
|         viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) } | ||||
| 
 | ||||
|     fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) { | ||||
|         _sharedContactRequested.value = sharedContact | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 James Rich
						James Rich