From a31232b37a819879f8a8b8bb30dde737361d23b3 Mon Sep 17 00:00:00 2001 From: andrekir Date: Tue, 5 Sep 2023 07:43:34 -0300 Subject: [PATCH] refactor: migrate `nodeDB` to Room database --- .../3.json | 140 ++++++ .../4.json | 402 ++++++++++++++++++ .../java/com/geeksville/mesh/MainActivity.kt | 12 - .../java/com/geeksville/mesh/MyNodeInfo.kt | 64 +-- .../main/java/com/geeksville/mesh/NodeInfo.kt | 20 +- .../mesh/database/DatabaseModule.kt | 12 + .../mesh/database/MeshtasticDatabase.kt | 24 +- .../mesh/database/dao/MyNodeInfoDao.kt | 21 + .../mesh/database/dao/NodeInfoDao.kt | 42 ++ .../java/com/geeksville/mesh/model/NodeDB.kt | 20 +- .../java/com/geeksville/mesh/model/UIState.kt | 3 + .../datastore/RadioConfigRepository.kt | 47 +- .../geeksville/mesh/service/MeshService.kt | 53 +-- .../mesh/service/MeshServiceSettingsData.kt | 30 -- 14 files changed, 742 insertions(+), 148 deletions(-) create mode 100644 app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/3.json create mode 100644 app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/4.json create mode 100644 app/src/main/java/com/geeksville/mesh/database/dao/MyNodeInfoDao.kt create mode 100644 app/src/main/java/com/geeksville/mesh/database/dao/NodeInfoDao.kt delete mode 100644 app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/3.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/3.json new file mode 100644 index 000000000..a7a946beb --- /dev/null +++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/3.json @@ -0,0 +1,140 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "5a779656cf00ccb63e4140845e25be09", + "entities": [ + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "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, '5a779656cf00ccb63e4140845e25be09')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/4.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/4.json new file mode 100644 index 000000000..1d8cee636 --- /dev/null +++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/4.json @@ -0,0 +1,402 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "935bed00304e3ea7c0398f8e1c19992a", + "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, `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, `devMetrics_time` INTEGER, `devMetrics_batteryLevel` INTEGER, `devMetrics_voltage` REAL, `devMetrics_channelUtilization` REAL, `devMetrics_airUtilTx` REAL, `envMetrics_time` INTEGER, `envMetrics_temperature` REAL, `envMetrics_relativeHumidity` REAL, `envMetrics_barometricPressure` REAL, `envMetrics_gasResistance` REAL, `envMetrics_voltage` REAL, `envMetrics_current` REAL, 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": "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": "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": "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 + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "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, '935bed00304e3ea7c0398f8e1c19992a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 9f6d9bcc3..2eb496187 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -311,7 +311,6 @@ class MainActivity : AppCompatActivity(), Logging { unregisterMeshReceiver() val filter = IntentFilter() filter.addAction(MeshService.ACTION_MESH_CONNECTED) - filter.addAction(MeshService.ACTION_NODE_CHANGE) registerReceiver(meshServiceReceiver, filter) receiverRegistered = true } @@ -489,17 +488,6 @@ class MainActivity : AppCompatActivity(), Logging { debug("Received from mesh service $intent") when (intent.action) { - MeshService.ACTION_NODE_CHANGE -> { - val info: NodeInfo? = intent.getParcelableExtraCompat(EXTRA_NODEINFO) - debug("UI nodechange $info") - - // We only care about nodes that have user info - info?.user?.id?.let { - val nodes = model.nodeDB.nodes.value!! + Pair(it, info) - model.nodeDB.setNodes(nodes) - } - } - MeshService.ACTION_MESH_CONNECTED -> { val extra = intent.getStringExtra(EXTRA_CONNECTED) if (extra != null) { diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt index 1c1efd218..d675d72a5 100644 --- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt @@ -1,12 +1,20 @@ package com.geeksville.mesh -import android.os.Parcel import android.os.Parcelable -import kotlinx.serialization.Serializable +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +/** + * Room [Entity] and [PrimaryKey] annotations and imports can be removed when only using the API. + * For details check the AIDL interface in [com.geeksville.mesh.IMeshService] + */ // MyNodeInfo sent via special protobuf from radio -@Serializable +@Parcelize +@Entity(tableName = "MyNodeInfo") data class MyNodeInfo( + @PrimaryKey(autoGenerate = false) val myNodeNum: Int, val hasGPS: Boolean, val model: String?, @@ -23,52 +31,4 @@ data class MyNodeInfo( ) : Parcelable { /** A human readable description of the software/hardware version */ val firmwareString: String get() = "$model $firmwareVersion" - - constructor(parcel: Parcel) : this( - parcel.readInt(), - parcel.readByte() != 0.toByte(), - parcel.readString(), - parcel.readString(), - parcel.readByte() != 0.toByte(), - parcel.readByte() != 0.toByte(), - parcel.readLong(), - parcel.readInt(), - parcel.readInt(), - parcel.readInt(), - parcel.readByte() != 0.toByte(), - parcel.readFloat(), - parcel.readFloat() - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeInt(myNodeNum) - parcel.writeByte(if (hasGPS) 1 else 0) - parcel.writeString(model) - parcel.writeString(firmwareVersion) - parcel.writeByte(if (couldUpdate) 1 else 0) - parcel.writeByte(if (shouldUpdate) 1 else 0) - parcel.writeLong(currentPacketId) - parcel.writeInt(messageTimeoutMsec) - parcel.writeInt(minAppVersion) - parcel.writeInt(maxChannels) - parcel.writeByte(if (hasWifi) 1 else 0) - parcel.writeFloat(channelUtilization) - parcel.writeFloat(airUtilTx) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): MyNodeInfo { - return MyNodeInfo(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index e1b864d79..7c222e5b0 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -2,20 +2,25 @@ package com.geeksville.mesh import android.graphics.Color import android.os.Parcelable +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey import com.geeksville.mesh.MeshProtos.User import com.geeksville.mesh.util.GPSFormat import com.geeksville.mesh.util.bearing import com.geeksville.mesh.util.latLongToMeter import com.geeksville.mesh.util.anonymize import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable +/** + * Room [Embedded], [Entity] and [PrimaryKey] annotations and imports can be removed when only using the API. + * For details check the AIDL interface in [com.geeksville.mesh.IMeshService] + */ // // model objects that directly map to the corresponding protobufs // -@Serializable @Parcelize data class MeshUser( val id: String, @@ -41,7 +46,6 @@ data class MeshUser( else hwModel.name.replace('_', '-').replace('p', '.').lowercase() } -@Serializable @Parcelize data class Position( val latitude: Double, @@ -94,7 +98,6 @@ data class Position( } -@Serializable @Parcelize data class DeviceMetrics( val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) @@ -122,7 +125,6 @@ data class DeviceMetrics( } } -@Serializable @Parcelize data class EnvironmentMetrics( val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) @@ -154,16 +156,22 @@ data class EnvironmentMetrics( } } -@Serializable @Parcelize +@Entity(tableName = "NodeInfo") data class NodeInfo( + @PrimaryKey(autoGenerate = false) val num: Int, // This is immutable, and used as a key + @Embedded(prefix = "user_") var user: MeshUser? = null, + @Embedded(prefix = "position_") var position: Position? = null, var snr: Float = Float.MAX_VALUE, var rssi: Int = Int.MAX_VALUE, var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 + @Embedded(prefix = "devMetrics_") var deviceMetrics: DeviceMetrics? = null, + val channel: Int = 0, + @Embedded(prefix = "envMetrics_") var environmentMetrics: EnvironmentMetrics? = null, ) : Parcelable { diff --git a/app/src/main/java/com/geeksville/mesh/database/DatabaseModule.kt b/app/src/main/java/com/geeksville/mesh/database/DatabaseModule.kt index d7412e61c..3552093f1 100644 --- a/app/src/main/java/com/geeksville/mesh/database/DatabaseModule.kt +++ b/app/src/main/java/com/geeksville/mesh/database/DatabaseModule.kt @@ -2,6 +2,8 @@ package com.geeksville.mesh.database import android.app.Application import com.geeksville.mesh.database.dao.MeshLogDao +import com.geeksville.mesh.database.dao.MyNodeInfoDao +import com.geeksville.mesh.database.dao.NodeInfoDao import com.geeksville.mesh.database.dao.PacketDao import com.geeksville.mesh.database.dao.QuickChatActionDao import dagger.Module @@ -18,6 +20,16 @@ class DatabaseModule { fun provideDatabase(app: Application): MeshtasticDatabase = MeshtasticDatabase.getDatabase(app) + @Provides + fun provideMyNodeInfoDao(database: MeshtasticDatabase): MyNodeInfoDao { + return database.myNodeInfoDao() + } + + @Provides + fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao { + return database.nodeInfoDao() + } + @Provides fun providePacketDao(database: MeshtasticDatabase): PacketDao { return database.packetDao() diff --git a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt index 8b736f0b9..8b7eb289c 100644 --- a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt +++ b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt @@ -1,20 +1,40 @@ package com.geeksville.mesh.database import android.content.Context +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.geeksville.mesh.MyNodeInfo +import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.database.dao.PacketDao import com.geeksville.mesh.database.dao.MeshLogDao +import com.geeksville.mesh.database.dao.MyNodeInfoDao +import com.geeksville.mesh.database.dao.NodeInfoDao import com.geeksville.mesh.database.dao.QuickChatActionDao import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction -@Database(entities = [Packet::class, MeshLog::class, QuickChatAction::class], version = 3, exportSchema = false) +@Database( + entities = [ + MyNodeInfo::class, + NodeInfo::class, + Packet::class, + MeshLog::class, + QuickChatAction::class + ], + autoMigrations = [ + AutoMigration (from = 3, to = 4), + ], + version = 4, + exportSchema = true, +) @TypeConverters(Converters::class) abstract class MeshtasticDatabase : RoomDatabase() { + abstract fun myNodeInfoDao(): MyNodeInfoDao + abstract fun nodeInfoDao(): NodeInfoDao abstract fun packetDao(): PacketDao abstract fun meshLogDao(): MeshLogDao abstract fun quickChatActionDao(): QuickChatActionDao @@ -31,4 +51,4 @@ abstract class MeshtasticDatabase : RoomDatabase() { .build() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/MyNodeInfoDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/MyNodeInfoDao.kt new file mode 100644 index 000000000..ddd444b7d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/database/dao/MyNodeInfoDao.kt @@ -0,0 +1,21 @@ +package com.geeksville.mesh.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.geeksville.mesh.MyNodeInfo +import kotlinx.coroutines.flow.Flow + +@Dao +interface MyNodeInfoDao { + + @Query("SELECT * FROM MyNodeInfo") + fun getMyNodeInfo(): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun setMyNodeInfo(myInfo: MyNodeInfo?) + + @Query("DELETE FROM MyNodeInfo") + fun clearMyNodeInfo() +} diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/NodeInfoDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/NodeInfoDao.kt new file mode 100644 index 000000000..7db5c6f8d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/database/dao/NodeInfoDao.kt @@ -0,0 +1,42 @@ +package com.geeksville.mesh.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Upsert +import com.geeksville.mesh.NodeInfo +import kotlinx.coroutines.flow.Flow + +@Dao +interface NodeInfoDao { + + @Query("SELECT * FROM NodeInfo") + fun getNodes(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(node: NodeInfo) + + @Upsert + fun upsert(node: NodeInfo) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun putAll(nodes: List) + + @Query("DELETE FROM NodeInfo") + fun clearNodeInfo() + + @Query("SELECT * FROM NodeInfo WHERE num=:num") + fun getNodeInfo(num: Int): NodeInfo? + +// @Transaction +// suspend fun updateUser(num: Int, updatedUser: MeshUser) { +// getNodeInfo(num)?.let { +// val updatedNodeInfo = it.copy(user = updatedUser) +// upsert(updatedNodeInfo) +// } +// } + +// @Query("Update node_info set position=:position WHERE num=:num") +// fun updatePosition(num: Int, position: MeshProtos.Position) +} diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index 2bf0cbcfb..fc930d39a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -2,11 +2,12 @@ package com.geeksville.mesh.model import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshUser import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.Position - +import kotlinx.coroutines.flow.MutableStateFlow /// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service class NodeDB(private val ui: UIViewModel) { @@ -40,11 +41,11 @@ class NodeDB(private val ui: UIViewModel) { ), it ) - } + }.associateBy { it.user?.id!! } private val seedWithTestNodes = false - /// The unique ID of our node + // The unique userId of our node private val _myId = MutableLiveData(if (seedWithTestNodes) "+16508765309" else null) val myId: LiveData get() = _myId @@ -52,13 +53,16 @@ class NodeDB(private val ui: UIViewModel) { _myId.value = myId } - /// A map from nodeid to to nodeinfo - private val _nodes = MutableLiveData>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it } - .toTypedArray())) - val nodes: LiveData> get() = _nodes + // A map from userId to NodeInfo + private val _nodes = MutableStateFlow(if (seedWithTestNodes) testNodes else mapOf()) + val nodes: LiveData> = _nodes.asLiveData() val nodesByNum get() = nodes.value?.values?.associateBy { it.num } fun setNodes(nodes: Map) { _nodes.value = nodes } -} \ No newline at end of file + + fun setNodes(list: List) { + setNodes(list.associateBy { it.user?.id!! }) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index ee963959c..5674aa8fc 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -123,6 +123,9 @@ class UIViewModel @Inject constructor( val packetResponse: StateFlow = _packetResponse init { + radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes) + .launchIn(viewModelScope) + viewModelScope.launch { meshLogRepository.getAllLogs().collect { logs -> _meshLog.value = logs diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt index f8131754e..37432052f 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt @@ -7,18 +7,63 @@ import com.geeksville.mesh.ConfigProtos.Config import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig +import com.geeksville.mesh.MyNodeInfo +import com.geeksville.mesh.NodeInfo +import com.geeksville.mesh.database.dao.MyNodeInfoDao +import com.geeksville.mesh.database.dao.NodeInfoDao +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext import javax.inject.Inject /** * Class responsible for radio configuration data. - * Combines access to [ChannelSet], [LocalConfig] & [LocalModuleConfig] data stores. + * Combines access to [MyNodeInfo] & [NodeInfo] Room databases + * and [ChannelSet], [LocalConfig] & [LocalModuleConfig] data stores. */ class RadioConfigRepository @Inject constructor( + private val myNodeInfoDao: MyNodeInfoDao, + private val nodeInfoDao: NodeInfoDao, private val channelSetRepository: ChannelSetRepository, private val localConfigRepository: LocalConfigRepository, private val moduleConfigRepository: ModuleConfigRepository, ) { + suspend fun clearNodeDB() = withContext(Dispatchers.IO) { + myNodeInfoDao.clearMyNodeInfo() + nodeInfoDao.clearNodeInfo() + } + + /** + * Flow representing the [MyNodeInfo] database. + */ + fun myNodeInfoFlow(): Flow = myNodeInfoDao.getMyNodeInfo() + suspend fun getMyNodeInfo(): MyNodeInfo? = myNodeInfoFlow().firstOrNull() + + suspend fun setMyNodeInfo(myInfo: MyNodeInfo?) = withContext(Dispatchers.IO) { + myNodeInfoDao.setMyNodeInfo(myInfo) + } + + /** + * Flow representing the [NodeInfo] database. + */ + fun nodeInfoFlow(): Flow> = nodeInfoDao.getNodes() + suspend fun getNodes(): List? = nodeInfoFlow().firstOrNull() + + suspend fun upsert(node: NodeInfo) = withContext(Dispatchers.IO) { + nodeInfoDao.upsert(node) + } + + suspend fun putAll(nodes: List) = withContext(Dispatchers.IO) { + nodeInfoDao.putAll(nodes) + } + + suspend fun installNodeDB(mi: MyNodeInfo?, nodes: List) { + clearNodeDB() + putAll(nodes) + setMyNodeInfo(mi) // set MyNodeInfo last + } + /** * Flow representing the [ChannelSet] data store. */ diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 9eed66e89..8298e72c7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -7,7 +7,6 @@ import android.content.pm.ServiceInfo import android.os.IBinder import android.os.RemoteException import androidx.core.app.ServiceCompat -import androidx.core.content.edit import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging @@ -41,7 +40,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.Json import java.util.* import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.TimeUnit @@ -242,9 +240,6 @@ class MeshService : Service(), Logging { // Switch to the IO thread serviceScope.handledLaunch { - loadSettings() // Load our last known node DB - - // We in turn need to use the radiointerface service radioInterfaceService.connect() } radioInterfaceService.connectionState.onEach(::onRadioConnectionState) @@ -256,6 +251,8 @@ class MeshService : Service(), Logging { radioConfigRepository.channelSetFlow.onEach { channelSet = it } .launchIn(serviceScope) + loadSettings() // Load our last known node DB + // the rest of our init will happen once we are in radioConnection.onServiceConnected } @@ -299,8 +296,6 @@ class MeshService : Service(), Logging { override fun onDestroy() { info("Destroying mesh service") - saveSettings() - // Make sure we aren't using the notification first ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) serviceNotifications.close() @@ -313,26 +308,8 @@ class MeshService : Service(), Logging { /// BEGINNING OF MODEL - FIXME, move elsewhere /// - private fun getPrefs() = getSharedPreferences("service-prefs", Context.MODE_PRIVATE) + private fun installNewNodeDB(ni: MyNodeInfo, nodes: List) { - /// Save information about our mesh to disk, so we will have it when we next start the service (even before we hear from our device) - private fun saveSettings() = synchronized(nodeDBbyNodeNum) { - myNodeInfo?.let { myInfo -> - val settings = MeshServiceSettingsData( - myInfo = myInfo, - nodeDB = nodeDBbyNodeNum.values.toTypedArray(), - ) - val json = Json { isLenient = true; allowSpecialFloatingPointValues = true } - val asString = json.encodeToString(MeshServiceSettingsData.serializer(), settings) - debug("Saving settings") - getPrefs().edit { - // FIXME, not really ideal to store this bigish blob in preferences - putString("json", asString) - } - } - } - - private fun installNewNodeDB(ni: MyNodeInfo, nodes: Array) { discardNodeDB() // Get rid of any old state myNodeInfo = ni @@ -347,11 +324,11 @@ class MeshService : Service(), Logging { private fun loadSettings() { try { - getPrefs().getString("json", null)?.let { asString -> + serviceScope.handledLaunch { - val json = Json { isLenient = true; allowSpecialFloatingPointValues = true } - val settings = json.decodeFromString(MeshServiceSettingsData.serializer(), asString) - installNewNodeDB(settings.myInfo, settings.nodeDB) + val myInfo = radioConfigRepository.getMyNodeInfo() + val nodeDB = radioConfigRepository.getNodes() + if (myInfo != null && nodeDB != null) installNewNodeDB(myInfo, nodeDB) // Note: we do not haveNodeDB = true because that means we've got a valid db from a real device (rather than this possibly stale hint) } @@ -455,8 +432,12 @@ class MeshService : Service(), Logging { // This might have been the first time we know an ID for this node, so also update the by ID map val userId = info.user?.id.orEmpty() - if (userId.isNotEmpty()) + if (userId.isNotEmpty()) { nodeDBbyID[userId] = info + if (haveNodeDB) serviceScope.handledLaunch { + radioConfigRepository.upsert(info) + } + } // parcelable is busted if (withBroadcast) @@ -1027,9 +1008,6 @@ class MeshService : Service(), Logging { /// Perform all the steps needed once we start waiting for device sleep to complete fun startDeviceSleep() { - // Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy) - saveSettings() - stopPacketQueue() stopLocationRequests() @@ -1063,9 +1041,6 @@ class MeshService : Service(), Logging { } fun startDisconnect() { - // Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy) - saveSettings() - stopPacketQueue() stopLocationRequests() @@ -1405,6 +1380,10 @@ class MeshService : Service(), Logging { regenMyNodeInfo() // we have a node db now, so can possibly find a better hwmodel myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo + serviceScope.handledLaunch { + radioConfigRepository.installNodeDB(newMyNodeInfo, nodeDBbyID.values.toList()) + } + sendAnalytics() if (deviceVersion < minDeviceVersion || appVersion < minAppVersion) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt deleted file mode 100644 index 7b7fb7cc7..000000000 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.geeksville.mesh.service - -import com.geeksville.mesh.MyNodeInfo -import com.geeksville.mesh.NodeInfo -import kotlinx.serialization.Serializable - -/// Our saved preferences as stored on disk -@Serializable -data class MeshServiceSettingsData( - val nodeDB: Array, - val myInfo: MyNodeInfo, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MeshServiceSettingsData - - if (!nodeDB.contentEquals(other.nodeDB)) return false - if (myInfo != other.myInfo) return false - - return true - } - - override fun hashCode(): Int { - var result = nodeDB.contentHashCode() - result = 31 * result + myInfo.hashCode() - return result - } -}