refactor: convert NodeDB to repository

This commit is contained in:
andrekir 2023-10-20 18:31:13 -03:00 committed by Andre K
parent d1d2c6cf3d
commit c489717ad1
13 changed files with 110 additions and 101 deletions

View file

@ -180,10 +180,6 @@ class MainActivity : AppCompatActivity(), Logging {
override fun createFragment(position: Int): Fragment = tabInfos[position].content
}
private val isInTestLab: Boolean by lazy {
(application as GeeksvilleApplication).isInTestLab
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
@ -364,7 +360,6 @@ class MainActivity : AppCompatActivity(), Logging {
debug("Getting latest DeviceConfig from service")
try {
val info: MyNodeInfo? = service.myNodeInfo // this can be null
model.setMyNodeInfo(info)
if (info != null) {
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
@ -381,9 +376,6 @@ class MainActivity : AppCompatActivity(), Logging {
else {
// If our app is too old/new, we probably don't understand the new DeviceConfig messages, so we don't read them until here
// model.setLocalConfig(LocalOnlyProtos.LocalConfig.parseFrom(service.deviceConfig))
// model.setChannels(ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels)))
model.updateNodesFromDevice()
// we have a connection to our device now, do the channel change
@ -526,8 +518,6 @@ class MainActivity : AppCompatActivity(), Logging {
// We don't start listening for packets until after we are connected to the service
registerMeshReceiver()
model.setMyNodeInfo(service.myNodeInfo) // Note: this could be NULL!
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())

View file

@ -14,7 +14,7 @@ interface MyNodeInfoDao {
fun getMyNodeInfo(): Flow<MyNodeInfo?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setMyNodeInfo(myInfo: MyNodeInfo?)
fun setMyNodeInfo(myInfo: MyNodeInfo)
@Query("DELETE FROM MyNodeInfo")
fun clearMyNodeInfo()

View file

@ -1,23 +1,50 @@
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.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NodeDB @Inject constructor(
private val myNodeInfoDao: MyNodeInfoDao,
private val nodeInfoDao: NodeInfoDao,
) {
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = myNodeInfoDao.getMyNodeInfo()
private suspend fun setMyNodeInfo(myInfo: MyNodeInfo) = withContext(Dispatchers.IO) {
myNodeInfoDao.setMyNodeInfo(myInfo)
}
fun nodeInfoFlow(): Flow<List<NodeInfo>> = nodeInfoDao.getNodes()
suspend fun upsert(node: NodeInfo) = withContext(Dispatchers.IO) {
nodeInfoDao.upsert(node)
}
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeInfo>) {
myNodeInfoDao.clearMyNodeInfo()
nodeInfoDao.clearNodeInfo()
nodeInfoDao.putAll(nodes)
setMyNodeInfo(mi) // set MyNodeInfo last
}
/// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service
class NodeDB(private val ui: UIViewModel) {
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35, 123), // dallas
Position(32.960758, -96.733521, 35, 456), // richardson
Position(32.912901, -96.781776, 35, 789) // north dallas
Position(32.912901, -96.781776, 35, 789), // north dallas
)
val testNodeNoPosition = NodeInfo(
private val testNodeNoPosition = NodeInfo(
8,
MeshUser(
"+16508765308".format(8),
@ -29,7 +56,7 @@ class NodeDB(private val ui: UIViewModel) {
null
)
private val testNodes = testPositions.mapIndexed { index, it ->
private val testNodes = (listOf(testNodeNoPosition) + testPositions.mapIndexed { index, it ->
NodeInfo(
9 + index,
MeshUser(
@ -41,22 +68,26 @@ class NodeDB(private val ui: UIViewModel) {
),
it
)
}.associateBy { it.user?.id!! }
}).associateBy { it.user?.id!! }
private val seedWithTestNodes = false
// The unique userId of our node
private val _myId = MutableLiveData<String?>(if (seedWithTestNodes) "+16508765309" else null)
val myId: LiveData<String?> get() = _myId
private val _myId = MutableStateFlow(if (seedWithTestNodes) "+16508765309" else null)
val myId: StateFlow<String?> get() = _myId
fun setMyId(myId: String?) {
_myId.value = myId
}
// A map from nodeNum to NodeInfo
private val _nodeDBbyNum = MutableStateFlow<Map<Int, NodeInfo>>(mapOf())
val nodeDBbyNum: StateFlow<Map<Int, NodeInfo>> get() = _nodeDBbyNum
val nodesByNum get() = nodeDBbyNum.value
// 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 }
val nodes: StateFlow<Map<String, NodeInfo>> get() = _nodes
fun setNodes(nodes: Map<String, NodeInfo>) {
_nodes.value = nodes
@ -64,5 +95,6 @@ class NodeDB(private val ui: UIViewModel) {
fun setNodes(list: List<NodeInfo>) {
setNodes(list.associateBy { it.user?.id!! })
_nodeDBbyNum.value = list.associateBy { it.num }
}
}

View file

@ -23,8 +23,6 @@ import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshService.ConnectionState
import com.geeksville.mesh.ui.ConfigRoute
import com.geeksville.mesh.ui.ResponseState
import com.google.protobuf.MessageLite
@ -59,7 +57,6 @@ data class RadioConfigState(
@HiltViewModel
class RadioConfigViewModel @Inject constructor(
private val app: Application,
radioInterfaceService: RadioInterfaceService,
private val radioConfigRepository: RadioConfigRepository,
meshLogRepository: MeshLogRepository,
) : ViewModel(), Logging {
@ -68,12 +65,10 @@ class RadioConfigViewModel @Inject constructor(
private val meshService: IMeshService? get() = radioConfigRepository.meshService
// Connection state to our radio device
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> get() = _connectionState
val connectionState get() = radioConfigRepository.connectionState
// A map from nodeNum to NodeInfo
private val _nodes = MutableStateFlow<Map<Int, NodeInfo>>(mapOf())
val nodes: StateFlow<Map<Int, NodeInfo>> = _nodes
val nodes: StateFlow<Map<Int, NodeInfo>> get() = radioConfigRepository.nodeDBbyNum
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
val myNodeInfo get() = _myNodeInfo
@ -86,21 +81,10 @@ class RadioConfigViewModel @Inject constructor(
val currentDeviceProfile get() = _currentDeviceProfile.value
init {
radioInterfaceService.connectionState.onEach { state ->
_connectionState.value = when {
state.isConnected -> ConnectionState.CONNECTED
else -> ConnectionState.DISCONNECTED
}
}.launchIn(viewModelScope)
radioConfigRepository.myNodeInfoFlow().onEach {
_myNodeInfo.value = it
}.launchIn(viewModelScope)
radioConfigRepository.nodeInfoFlow().onEach { list ->
_nodes.value = list.associateBy { it.num }
}.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach {
_currentDeviceProfile.value = it
}.launchIn(viewModelScope)
@ -114,8 +98,8 @@ class RadioConfigViewModel @Inject constructor(
debug("RadioConfigViewModel created")
}
val myNodeNum get() = _myNodeInfo.value?.myNodeNum
val maxChannels get() = _myNodeInfo.value?.maxChannels ?: 8
val myNodeNum get() = myNodeInfo.value?.myNodeNum
val maxChannels get() = myNodeInfo.value?.maxChannels ?: 8
private val ourNodeInfo: NodeInfo? get() = nodes.value[myNodeNum]
override fun onCleared() {

View file

@ -112,7 +112,7 @@ class UIViewModel @Inject constructor(
var actionBarMenu: Menu? = null
val meshService: IMeshService? get() = radioConfigRepository.meshService
val nodeDB = NodeDB(this)
val nodeDB = radioConfigRepository.nodeDB
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
val selectedBluetooth get() = radioInterfaceService.getDeviceAddress()?.getOrNull(0) == 'x'
@ -138,6 +138,10 @@ class UIViewModel @Inject constructor(
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
// hardware info about our local device (can be null)
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
val myNodeInfo: StateFlow<MyNodeInfo?> get() = _myNodeInfo
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
@ -152,6 +156,10 @@ class UIViewModel @Inject constructor(
radioInterfaceService.clearErrorMessage()
}.launchIn(viewModelScope)
radioConfigRepository.myNodeInfoFlow().onEach {
_myNodeInfo.value = it
}.launchIn(viewModelScope)
radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes)
.launchIn(viewModelScope)
@ -356,15 +364,8 @@ class UIViewModel @Inject constructor(
// managed mode disables all access to configuration
val isManaged: Boolean get() = config.device.isManaged
/// hardware info about our local device (can be null)
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
val myNodeNum get() = _myNodeInfo.value?.myNodeNum
val maxChannels = myNodeInfo.value?.maxChannels ?: 8
fun setMyNodeInfo(info: MyNodeInfo?) {
_myNodeInfo.value = info
}
val myNodeNum get() = myNodeInfo.value?.myNodeNum
val maxChannels get() = myNodeInfo.value?.maxChannels ?: 8
override fun onCleared() {
super.onCleared()
@ -445,7 +446,7 @@ class UIViewModel @Inject constructor(
val myNodeNum = myNodeNum ?: return@launch
// Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap()
val nodes = nodeDB.nodes.value
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
meshPosition?.let { Position(it) }.takeIf {
@ -602,7 +603,7 @@ class UIViewModel @Inject constructor(
if (data?.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) {
val parsed = MeshProtos.RouteDiscovery.parseFrom(data.payload)
fun nodeName(num: Int) = nodeDB.nodesByNum?.get(num)?.user?.longName
fun nodeName(num: Int) = nodeDB.nodesByNum[num]?.user?.longName
?: app.getString(R.string.unknown_username)
_tracerouteResponse.value = buildString {

View file

@ -11,66 +11,52 @@ 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 com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.getChannelUrl
import com.geeksville.mesh.service.MeshService.ConnectionState
import com.geeksville.mesh.service.ServiceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* Class responsible for radio configuration data.
* Combines access to [MyNodeInfo] & [NodeInfo] Room databases
* and [ChannelSet], [LocalConfig] & [LocalModuleConfig] data stores.
* Combines access to [nodeDB], [ChannelSet], [LocalConfig] & [LocalModuleConfig].
*/
class RadioConfigRepository @Inject constructor(
private val serviceRepository: ServiceRepository,
private val myNodeInfoDao: MyNodeInfoDao,
private val nodeInfoDao: NodeInfoDao,
val nodeDB: NodeDB,
private val channelSetRepository: ChannelSetRepository,
private val localConfigRepository: LocalConfigRepository,
private val moduleConfigRepository: ModuleConfigRepository,
) {
val meshService: IMeshService? get() = serviceRepository.meshService
suspend fun clearNodeDB() = withContext(Dispatchers.IO) {
myNodeInfoDao.clearMyNodeInfo()
nodeInfoDao.clearNodeInfo()
}
// Connection state to our radio device
val connectionState get() = serviceRepository.connectionState
fun setConnectionState(state: ConnectionState) = serviceRepository.setConnectionState(state)
/**
* Flow representing the [MyNodeInfo] database.
*/
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = myNodeInfoDao.getMyNodeInfo()
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = nodeDB.myNodeInfoFlow()
suspend fun getMyNodeInfo(): MyNodeInfo? = myNodeInfoFlow().firstOrNull()
suspend fun setMyNodeInfo(myInfo: MyNodeInfo?) = withContext(Dispatchers.IO) {
myNodeInfoDao.setMyNodeInfo(myInfo)
}
val nodeDBbyNum: StateFlow<Map<Int, NodeInfo>> get() = nodeDB.nodeDBbyNum
val nodeDBbyID: StateFlow<Map<String, NodeInfo>> get() = nodeDB.nodes
/**
* Flow representing the [NodeInfo] database.
*/
fun nodeInfoFlow(): Flow<List<NodeInfo>> = nodeInfoDao.getNodes()
fun nodeInfoFlow(): Flow<List<NodeInfo>> = nodeDB.nodeInfoFlow()
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
suspend fun upsert(node: NodeInfo) = nodeDB.upsert(node)
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeInfo>) {
nodeDB.installNodeDB(mi, nodes)
}
/**
@ -149,13 +135,13 @@ class RadioConfigRepository @Inject constructor(
*/
val deviceProfileFlow: Flow<DeviceProfile> = combine(
myNodeInfoFlow(),
nodeInfoFlow(),
nodeDBbyNum,
channelSetFlow,
localConfigFlow,
moduleConfigFlow,
) { myInfo, nodes, channels, localConfig, localModuleConfig ->
deviceProfile {
nodes.firstOrNull { it.num == myInfo?.myNodeNum }?.user?.let {
nodes[myInfo?.myNodeNum]?.user?.let {
longName = it.longName
shortName = it.shortName
}

View file

@ -144,7 +144,9 @@ class MeshService : Service(), Logging {
/// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf<String, String>()
private val serviceNotifications = MeshServiceNotifications(this)
private val serviceBroadcasts = MeshServiceBroadcasts(this, clientPackages) { connectionState }
private val serviceBroadcasts = MeshServiceBroadcasts(this, clientPackages) {
connectionState.also { radioConfigRepository.setConnectionState(it) }
}
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var connectionState = ConnectionState.DISCONNECTED
@ -1053,7 +1055,7 @@ class MeshService : Service(), Logging {
)
}
// Have our timeout fire in the approprate number of seconds
// Have our timeout fire in the appropriate number of seconds
sleepTimeout = serviceScope.handledLaunch {
try {
// If we have a valid timeout, wait that long (+30 seconds) otherwise, just wait 30 seconds
@ -1432,9 +1434,9 @@ class MeshService : Service(), Logging {
insertMeshLog(packetToSave)
// This was our config request
if (newMyNodeInfo == null || newNodes.isEmpty())
if (newMyNodeInfo == null || newNodes.isEmpty()) {
errormsg("Did not receive a valid config")
else {
} else {
discardNodeDB()
debug("Installing new node DB")
myNodeInfo = newMyNodeInfo // Install myNodeInfo as current
@ -1448,7 +1450,7 @@ class MeshService : Service(), Logging {
myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo
serviceScope.handledLaunch {
radioConfigRepository.installNodeDB(newMyNodeInfo, nodeDBbyID.values.toList())
radioConfigRepository.installNodeDB(newMyNodeInfo!!, nodeDBbyID.values.toList())
}
sendAnalytics()
@ -1460,8 +1462,9 @@ class MeshService : Service(), Logging {
}
onHasSettings()
}
} else
} else {
warn("Ignoring stale config complete")
}
}
private fun requestConfig(config: AdminProtos.AdminMessage.ConfigType) {

View file

@ -1,9 +1,14 @@
package com.geeksville.mesh.service
import com.geeksville.mesh.IMeshService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository class for managing the [IMeshService] instance and connection state
*/
@Singleton
class ServiceRepository @Inject constructor() {
var meshService: IMeshService? = null
@ -12,4 +17,12 @@ class ServiceRepository @Inject constructor() {
fun setMeshService(service: IMeshService?) {
meshService = service
}
// Connection state to our radio device
private val _connectionState = MutableStateFlow(MeshService.ConnectionState.DISCONNECTED)
val connectionState: StateFlow<MeshService.ConnectionState> get() = _connectionState
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState
}
}

View file

@ -87,7 +87,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
val toBroadcast = contact.to == DataPacket.ID_BROADCAST
// grab usernames from NodeInfo
val nodes = model.nodeDB.nodes.value!!
val nodes = model.nodeDB.nodes.value
val node = nodes[if (fromLocal) contact.to else contact.from]
//grab channel names from DeviceConfig
@ -212,7 +212,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
contactsAdapter.onChannelsChanged()
}
model.nodeDB.nodes.observe(viewLifecycleOwner) {
model.nodeDB.nodes.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.notifyDataSetChanged()
}

View file

@ -93,7 +93,7 @@ class MessagesFragment : Fragment(), Logging {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val packet = messages[position]
val msg = packet.data
val nodes = model.nodeDB.nodes.value!!
val nodes = model.nodeDB.nodes.value
val node = nodes[msg.from]
// Determine if this is my message (originated on this device)
val isLocal = msg.from == DataPacket.ID_LOCAL

View file

@ -321,7 +321,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
// Also watch myNodeInfo because it might change later
model.myNodeInfo.observe(viewLifecycleOwner) {
model.myNodeInfo.asLiveData().observe(viewLifecycleOwner) {
updateNodeInfo()
}

View file

@ -322,7 +322,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
values
}.toTypedArray()
model.nodeDB.nodes.observe(viewLifecycleOwner) {
model.nodeDB.nodes.asLiveData().observe(viewLifecycleOwner) {
nodesAdapter.onNodesChanged(it.perhapsReindexBy(model.myNodeNum))
}

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.DataPacket
@ -187,7 +188,7 @@ fun MapView(model: UIViewModel = viewModel()) {
requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}
val nodes by model.nodeDB.nodes.observeAsState(emptyMap())
val nodes by model.nodeDB.nodes.collectAsStateWithLifecycle()
val waypoints by model.waypoints.observeAsState(emptyMap())
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
@ -256,8 +257,7 @@ fun MapView(model: UIViewModel = viewModel()) {
}
fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL) context.getString(R.string.you)
else model.nodeDB.nodes.value?.get(id)?.user?.longName
?: context.getString(R.string.unknown_username)
else model.nodeDB.nodes.value[id]?.user?.longName ?: context.getString(R.string.unknown_username)
fun MapView.onWaypointChanged(waypoints: Collection<Packet>): List<MarkerWithLabel> {
return waypoints.mapNotNull { waypoint ->