kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: migrate `nodeDB` to Room database (#717)
rodzic
99d7147efe
commit
83722159be
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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<MyNodeInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): MyNodeInfo {
|
||||
return MyNodeInfo(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<MyNodeInfo?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MyNodeInfo?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun setMyNodeInfo(myInfo: MyNodeInfo?)
|
||||
|
||||
@Query("DELETE FROM MyNodeInfo")
|
||||
fun clearMyNodeInfo()
|
||||
}
|
|
@ -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<List<NodeInfo>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(node: NodeInfo)
|
||||
|
||||
@Upsert
|
||||
fun upsert(node: NodeInfo)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun putAll(nodes: List<NodeInfo>)
|
||||
|
||||
@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)
|
||||
}
|
|
@ -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<String?>(if (seedWithTestNodes) "+16508765309" else null)
|
||||
val myId: LiveData<String?> 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<Map<String, NodeInfo>>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it }
|
||||
.toTypedArray()))
|
||||
val nodes: LiveData<Map<String, NodeInfo>> get() = _nodes
|
||||
// A map from userId to NodeInfo
|
||||
private val _nodes = MutableStateFlow(if (seedWithTestNodes) testNodes else mapOf())
|
||||
val nodes: LiveData<Map<String, NodeInfo>> = _nodes.asLiveData()
|
||||
val nodesByNum get() = nodes.value?.values?.associateBy { it.num }
|
||||
|
||||
fun setNodes(nodes: Map<String, NodeInfo>) {
|
||||
_nodes.value = nodes
|
||||
}
|
||||
}
|
||||
|
||||
fun setNodes(list: List<NodeInfo>) {
|
||||
setNodes(list.associateBy { it.user?.id!! })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,9 @@ class UIViewModel @Inject constructor(
|
|||
val packetResponse: StateFlow<MeshLog?> = _packetResponse
|
||||
|
||||
init {
|
||||
radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
meshLogRepository.getAllLogs().collect { logs ->
|
||||
_meshLog.value = logs
|
||||
|
|
|
@ -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<MyNodeInfo?> = 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<List<NodeInfo>> = nodeInfoDao.getNodes()
|
||||
suspend fun getNodes(): List<NodeInfo>? = nodeInfoFlow().firstOrNull()
|
||||
|
||||
suspend fun upsert(node: NodeInfo) = withContext(Dispatchers.IO) {
|
||||
nodeInfoDao.upsert(node)
|
||||
}
|
||||
|
||||
suspend fun putAll(nodes: List<NodeInfo>) = withContext(Dispatchers.IO) {
|
||||
nodeInfoDao.putAll(nodes)
|
||||
}
|
||||
|
||||
suspend fun installNodeDB(mi: MyNodeInfo?, nodes: List<NodeInfo>) {
|
||||
clearNodeDB()
|
||||
putAll(nodes)
|
||||
setMyNodeInfo(mi) // set MyNodeInfo last
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow representing the [ChannelSet] data store.
|
||||
*/
|
||||
|
|
|
@ -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<NodeInfo>) {
|
||||
|
||||
/// 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<NodeInfo>) {
|
||||
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) {
|
||||
|
|
|
@ -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<NodeInfo>,
|
||||
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
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue