mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Feat/node notes (#3014)
This commit is contained in:
parent
363764c5ce
commit
f2680d37ad
9 changed files with 889 additions and 107 deletions
|
|
@ -1,106 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MyNodeEntity::class,
|
||||
NodeEntity::class,
|
||||
Packet::class,
|
||||
ContactSettings::class,
|
||||
MeshLog::class,
|
||||
QuickChatAction::class,
|
||||
ReactionEntity::class,
|
||||
MetadataEntity::class,
|
||||
DeviceHardwareEntity::class,
|
||||
FirmwareReleaseEntity::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12),
|
||||
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19),
|
||||
],
|
||||
version = 19,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MeshtasticDatabase : RoomDatabase() {
|
||||
abstract fun nodeInfoDao(): NodeInfoDao
|
||||
abstract fun packetDao(): PacketDao
|
||||
abstract fun meshLogDao(): MeshLogDao
|
||||
abstract fun quickChatActionDao(): QuickChatActionDao
|
||||
abstract fun deviceHardwareDao(): DeviceHardwareDao
|
||||
abstract fun firmwareReleaseDao(): FirmwareReleaseDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase {
|
||||
|
||||
return Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
MeshtasticDatabase::class.java,
|
||||
"meshtastic_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteTable.Entries(
|
||||
DeleteTable(tableName = "NodeInfo"),
|
||||
DeleteTable(tableName = "MyNodeInfo")
|
||||
)
|
||||
class AutoMigration12to13 : AutoMigrationSpec
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
|
||||
@Database(
|
||||
entities =
|
||||
[
|
||||
MyNodeEntity::class,
|
||||
NodeEntity::class,
|
||||
Packet::class,
|
||||
ContactSettings::class,
|
||||
MeshLog::class,
|
||||
QuickChatAction::class,
|
||||
ReactionEntity::class,
|
||||
MetadataEntity::class,
|
||||
DeviceHardwareEntity::class,
|
||||
FirmwareReleaseEntity::class,
|
||||
],
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12),
|
||||
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19),
|
||||
AutoMigration(from = 19, to = 20),
|
||||
],
|
||||
version = 20,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MeshtasticDatabase : RoomDatabase() {
|
||||
abstract fun nodeInfoDao(): NodeInfoDao
|
||||
|
||||
abstract fun packetDao(): PacketDao
|
||||
|
||||
abstract fun meshLogDao(): MeshLogDao
|
||||
|
||||
abstract fun quickChatActionDao(): QuickChatActionDao
|
||||
|
||||
abstract fun deviceHardwareDao(): DeviceHardwareDao
|
||||
|
||||
abstract fun firmwareReleaseDao(): FirmwareReleaseDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase =
|
||||
Room.databaseBuilder(context.applicationContext, MeshtasticDatabase::class.java, "meshtastic_database")
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteTable.Entries(DeleteTable(tableName = "NodeInfo"), DeleteTable(tableName = "MyNodeInfo"))
|
||||
class AutoMigration12to13 : AutoMigrationSpec
|
||||
|
|
|
|||
|
|
@ -157,4 +157,7 @@ constructor(
|
|||
|
||||
val totalNodeCount: Flow<Int> =
|
||||
nodeInfoDao.nodeDBbyNum().mapLatest { map -> map.values.count() }.flowOn(dispatchers.io).conflate()
|
||||
|
||||
suspend fun setNodeNotes(num: Int, notes: String) =
|
||||
withContext(dispatchers.io) { nodeInfoDao.setNodeNotes(num, notes) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ interface NodeInfoDao {
|
|||
return if (isPublicKeyMatchingOrExistingIsEmpty) {
|
||||
// Keys match or existing key was empty: trust the incoming node data completely.
|
||||
// This allows for legitimate updates to user info and other fields.
|
||||
incomingNode
|
||||
val resolvedNotes = if (incomingNode.notes.isBlank()) existingNode.notes else incomingNode.notes
|
||||
incomingNode.copy(notes = resolvedNotes)
|
||||
} else {
|
||||
existingNode.copy(
|
||||
lastHeard = incomingNode.lastHeard,
|
||||
|
|
@ -93,6 +94,7 @@ interface NodeInfoDao {
|
|||
// to reflect the conflict state.
|
||||
user = existingNode.user.toBuilder().setPublicKey(ByteString.EMPTY).build(),
|
||||
publicKey = ByteString.EMPTY,
|
||||
notes = existingNode.notes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -216,4 +218,7 @@ interface NodeInfoDao {
|
|||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun putAll(nodes: List<NodeEntity>)
|
||||
|
||||
@Query("UPDATE nodes SET notes = :notes WHERE num = :num")
|
||||
fun setNodeNotes(num: Int, notes: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ data class NodeWithRelations(
|
|||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
paxcounter = paxcounter,
|
||||
notes = notes,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ data class NodeWithRelations(
|
|||
environmentTelemetry = environmentTelemetry,
|
||||
powerTelemetry = powerTelemetry,
|
||||
paxcounter = paxcounter,
|
||||
notes = notes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +121,7 @@ data class NodeEntity(
|
|||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||
@ColumnInfo(name = "public_key") var publicKey: ByteString? = null,
|
||||
@ColumnInfo(name = "notes", defaultValue = "") var notes: String = "",
|
||||
) {
|
||||
val deviceMetrics: TelemetryProtos.DeviceMetrics
|
||||
get() = deviceTelemetry.deviceMetrics
|
||||
|
|
@ -172,6 +175,7 @@ data class NodeEntity(
|
|||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
paxcounter = paxcounter,
|
||||
publicKey = publicKey ?: user.publicKey,
|
||||
notes = notes,
|
||||
)
|
||||
|
||||
fun toNodeInfo() = NodeInfo(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ data class Node(
|
|||
val powerMetrics: PowerMetrics = PowerMetrics.getDefaultInstance(),
|
||||
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||
val publicKey: ByteString? = null,
|
||||
val notes: String = "",
|
||||
) {
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
|
|
|
|||
|
|
@ -762,6 +762,16 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setNodeNotes(nodeNum: Int, notes: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
nodeDB.setNodeNotes(nodeNum, notes)
|
||||
} catch (ex: java.io.IOException) {
|
||||
errormsg("Set node notes IO error: ${ex.message}")
|
||||
} catch (ex: java.sql.SQLException) {
|
||||
errormsg("Set node notes SQL error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// managed mode disables all access to configuration
|
||||
val isManaged: Boolean
|
||||
get() = config.device.isManaged || config.security.isManaged
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ import androidx.compose.material3.LocalContentColor
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -331,9 +332,35 @@ private fun NodeDetailContent(
|
|||
},
|
||||
modifier = modifier,
|
||||
availableLogs = availableLogs,
|
||||
onSaveNotes = { num, notes -> uiViewModel.setNodeNotes(num, notes) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun notesSection(node: Node, onSaveNotes: (Int, String) -> Unit) {
|
||||
if (node.isFavorite) {
|
||||
TitledCard(title = stringResource(R.string.notes)) {
|
||||
val originalNotes = node.notes
|
||||
var notes by remember(node.notes) { mutableStateOf(node.notes) }
|
||||
OutlinedTextField(
|
||||
value = notes,
|
||||
onValueChange = { notes = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(stringResource(id = R.string.add_a_note)) },
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val edited = notes.trim() != originalNotes.trim()
|
||||
if (edited) {
|
||||
NodeActionButton(
|
||||
title = stringResource(id = R.string.save),
|
||||
enabled = true,
|
||||
onClick = { onSaveNotes(node.num, notes.trim()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NodeDetailList(
|
||||
|
|
@ -344,6 +371,7 @@ private fun NodeDetailList(
|
|||
metricsState: MetricsState,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
availableLogs: Set<LogsType>,
|
||||
onSaveNotes: (Int, String) -> Unit,
|
||||
) {
|
||||
var showFirmwareSheet by remember { mutableStateOf(false) }
|
||||
var selectedFirmware by remember { mutableStateOf<FirmwareRelease?>(null) }
|
||||
|
|
@ -369,6 +397,7 @@ private fun NodeDetailList(
|
|||
TitledCard(title = stringResource(R.string.details)) {
|
||||
NodeDetailsContent(node, ourNode, metricsState.displayUnits)
|
||||
}
|
||||
notesSection(node = node, onSaveNotes = onSaveNotes)
|
||||
|
||||
DeviceActions(
|
||||
isLocal = metricsState.isLocal,
|
||||
|
|
@ -1067,6 +1096,7 @@ private fun NodeDetailsPreview(@PreviewParameter(NodePreviewParameterProvider::c
|
|||
metricsState = MetricsState.Empty,
|
||||
availableLogs = emptySet(),
|
||||
onAction = {},
|
||||
onSaveNotes = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue