Meshtastic-Android/app/src/main/java/com/geeksville/mesh/model/UIState.kt

507 lines
20 KiB
Kotlin
Raw Normal View History

2020-02-17 13:34:52 -08:00
package com.geeksville.mesh.model
import android.app.Application
2020-02-18 10:40:02 -08:00
import android.content.Context
2020-03-02 08:41:16 -08:00
import android.content.SharedPreferences
2020-03-17 11:35:19 -07:00
import android.net.Uri
2020-02-18 12:22:45 -08:00
import android.os.RemoteException
2020-07-17 17:06:29 -04:00
import android.view.Menu
2020-02-18 10:40:02 -08:00
import androidx.core.content.edit
2022-04-22 17:22:06 -03:00
import androidx.lifecycle.LiveData
2020-04-07 17:42:31 -07:00
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
2020-09-23 22:47:45 -04:00
import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging
2022-02-03 18:15:06 -08:00
import com.geeksville.mesh.*
2020-09-23 22:47:45 -04:00
import com.geeksville.mesh.database.PacketRepository
2022-08-11 16:43:26 +01:00
import com.geeksville.mesh.database.QuickChatActionRepository
2020-09-23 22:47:45 -04:00
import com.geeksville.mesh.database.entity.Packet
2022-08-11 16:43:26 +01:00
import com.geeksville.mesh.database.entity.QuickChatAction
2022-06-11 18:36:57 -03:00
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
import com.geeksville.mesh.service.MeshService
2022-05-20 11:20:13 -03:00
import com.geeksville.mesh.util.positionToMeter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineStart
2020-09-23 22:47:45 -04:00
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
2022-02-03 18:15:06 -08:00
import kotlinx.coroutines.flow.first
2020-09-23 22:47:45 -04:00
import kotlinx.coroutines.launch
2022-02-03 18:15:06 -08:00
import kotlinx.coroutines.withContext
import java.io.BufferedWriter
2022-02-13 08:09:26 -03:00
import java.io.FileNotFoundException
2022-02-03 18:15:06 -08:00
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
2022-02-03 18:15:06 -08:00
import kotlin.math.roundToInt
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user. If the original name is only one word, strip vowels from the original name and if the result is
/// 3 or more characters, use the first three characters. If not, just take the first 3 characters of the
/// original name.
fun getInitials(nameIn: String): String {
val nchars = 3
val minchars = 2
val name = nameIn.trim()
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }
val initials = when (words.size) {
2022-05-26 16:23:47 -03:00
in 0 until minchars -> {
val nm = if (name.isNotEmpty())
2022-01-24 18:23:09 -03:00
name.first() + name.drop(1).filterNot { c -> c.lowercase() in "aeiou" }
2020-09-27 14:05:10 -07:00
else
""
if (nm.length >= nchars) nm else name
}
else -> words.map { it.first() }.joinToString("")
}
return initials.take(nchars)
}
2020-03-15 16:30:12 -07:00
@HiltViewModel
class UIViewModel @Inject constructor(
private val app: Application,
2022-06-10 21:55:26 -03:00
private val packetRepository: PacketRepository,
2022-06-11 18:36:57 -03:00
private val localConfigRepository: LocalConfigRepository,
2022-08-11 16:43:26 +01:00
private val quickChatActionRepository: QuickChatActionRepository,
private val preferences: SharedPreferences
) : ViewModel(), Logging {
2020-09-23 22:47:45 -04:00
private val _allPacketState = MutableStateFlow<List<Packet>>(emptyList())
val allPackets: StateFlow<List<Packet>> = _allPacketState
2020-09-23 22:47:45 -04:00
2022-08-23 21:39:08 -03:00
private val _localConfig = MutableLiveData<LocalOnlyProtos.LocalConfig>()
val localConfig: LiveData<LocalOnlyProtos.LocalConfig> get() = _localConfig
2022-06-20 22:46:45 -03:00
2022-08-23 21:39:08 -03:00
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
2022-08-11 16:43:26 +01:00
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
2020-04-07 17:42:31 -07:00
init {
viewModelScope.launch {
2022-06-17 02:00:18 -03:00
packetRepository.getAllPackets().collect { packets ->
_allPacketState.value = packets
2022-06-11 18:36:57 -03:00
}
2022-06-17 02:00:18 -03:00
}
2022-06-20 22:46:45 -03:00
viewModelScope.launch {
2022-06-17 02:00:18 -03:00
localConfigRepository.localConfigFlow.collect { config ->
2022-06-20 22:46:45 -03:00
_localConfig.value = config
}
}
2022-08-11 16:43:26 +01:00
viewModelScope.launch {
quickChatActionRepository.getAllActions().collect { actions ->
_quickChatActions.value = actions
}
}
2020-04-07 17:42:31 -07:00
debug("ViewModel created")
}
2020-09-23 22:47:45 -04:00
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
2022-06-10 21:55:26 -03:00
packetRepository.deleteAll()
2020-09-23 22:47:45 -04:00
}
2020-04-07 17:42:31 -07:00
companion object {
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
2020-07-17 17:06:29 -04:00
var actionBarMenu: Menu? = null
2020-04-07 17:42:31 -07:00
var meshService: IMeshService? = null
val nodeDB = NodeDB(this)
val messagesState = MessagesState(this)
2022-04-22 17:22:06 -03:00
/// Connection state to our radio device
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
2022-04-24 12:12:13 -03:00
val connectionState: LiveData<MeshService.ConnectionState> get() = _connectionState
2020-04-07 17:42:31 -07:00
2022-04-24 12:12:13 -03:00
fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
2020-04-07 17:42:31 -07:00
2022-04-22 17:22:06 -03:00
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState
2021-02-27 11:44:05 +08:00
}
2022-04-22 17:22:06 -03:00
private val _channels = MutableLiveData<ChannelSet?>()
val channels: LiveData<ChannelSet?> get() = _channels
2022-05-17 17:29:21 -03:00
private val _requestChannelUrl = MutableLiveData<Uri?>(null)
val requestChannelUrl: LiveData<Uri?> get() = _requestChannelUrl
fun setRequestChannelUrl(channelUrl: Uri) {
_requestChannelUrl.value = channelUrl
}
/**
* Called immediately after activity observes requestChannelUrl
*/
fun clearRequestChannelUrl() {
_requestChannelUrl.value = null
}
2021-02-05 21:29:28 -08:00
var positionBroadcastSecs: Int?
get() {
2022-06-10 21:55:26 -03:00
_localConfig.value?.position?.positionBroadcastSecs?.let {
2022-06-17 02:00:18 -03:00
return if (it > 0) it else defaultPositionBroadcastSecs
2021-02-05 21:29:28 -08:00
}
return null
}
set(value) {
2022-06-10 21:55:26 -03:00
val config = _localConfig.value
2021-02-05 21:29:28 -08:00
if (value != null && config != null) {
2022-06-10 21:55:26 -03:00
val builder = config.position.toBuilder()
2022-06-17 02:00:18 -03:00
builder.positionBroadcastSecs =
if (value == defaultPositionBroadcastSecs) 0 else value
2022-06-10 21:55:26 -03:00
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.position = builder.build()
setDeviceConfig(newConfig.build())
2022-01-24 18:23:09 -03:00
}
}
var lsSleepSecs: Int?
get() {
2022-06-10 21:55:26 -03:00
_localConfig.value?.power?.lsSecs?.let {
2022-06-17 02:00:18 -03:00
return if (it > 0) it else defaultLsSecs
}
return null
}
2022-01-24 18:23:09 -03:00
set(value) {
2022-06-10 21:55:26 -03:00
val config = _localConfig.value
2022-01-24 18:23:09 -03:00
if (value != null && config != null) {
2022-06-10 21:55:26 -03:00
val builder = config.power.toBuilder()
2022-06-17 02:00:18 -03:00
builder.lsSecs = if (value == defaultLsSecs) 0 else value
2022-06-10 21:55:26 -03:00
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.power = builder.build()
setDeviceConfig(newConfig.build())
2022-01-24 18:23:09 -03:00
}
}
2022-05-26 16:23:47 -03:00
var gpsDisabled: Boolean
2022-06-10 21:55:26 -03:00
get() = _localConfig.value?.position?.gpsDisabled ?: false
2022-03-26 17:09:05 -03:00
set(value) {
2022-06-10 21:55:26 -03:00
val config = _localConfig.value
2022-03-26 17:09:05 -03:00
if (config != null) {
2022-06-10 21:55:26 -03:00
val builder = config.position.toBuilder()
builder.gpsDisabled = value
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.position = builder.build()
setDeviceConfig(newConfig.build())
2021-02-05 21:29:28 -08:00
}
}
2022-01-24 18:23:09 -03:00
var isPowerSaving: Boolean?
2022-06-10 21:55:26 -03:00
get() = _localConfig.value?.power?.isPowerSaving
set(value) {
2022-06-10 21:55:26 -03:00
val config = _localConfig.value
if (value != null && config != null) {
2022-06-10 21:55:26 -03:00
val builder = config.power.toBuilder()
builder.isPowerSaving = value
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.power = builder.build()
setDeviceConfig(newConfig.build())
2021-02-05 21:29:28 -08:00
}
}
2022-05-26 16:23:47 -03:00
var region: ConfigProtos.Config.LoRaConfig.RegionCode
2022-07-26 23:01:28 -03:00
get() = localConfig.value?.lora?.region ?: ConfigProtos.Config.LoRaConfig.RegionCode.Unset
2021-02-05 21:29:28 -08:00
set(value) {
2022-07-26 23:01:28 -03:00
val config = _localConfig.value
if (config != null) {
val builder = config.lora.toBuilder()
builder.region = value
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.lora = builder.build()
setDeviceConfig(newConfig.build())
}
2021-02-05 21:29:28 -08:00
}
2022-07-26 23:01:28 -03:00
@Suppress("MemberVisibilityCanBePrivate")
2022-06-17 02:00:18 -03:00
val isRouter: Boolean =
localConfig.value?.device?.role == ConfigProtos.Config.DeviceConfig.Role.Router
// These default values are borrowed from the device code.
private val defaultPositionBroadcastSecs = if (isRouter) 12 * 60 * 60 else 15 * 60
private val defaultLsSecs = if (isRouter) 24 * 60 * 60 else 5 * 60
2022-08-28 07:54:47 -03:00
// We consider hasWifi = ESP32
fun isESP32() = myNodeInfo.value?.hasWifi == true
2022-06-13 18:13:47 -03:00
fun hasAXP(): Boolean {
val hasAXP = listOf(4, 7, 9) // mesh.proto 'HardwareModel' enums with AXP192 chip
return hasAXP.contains(nodeDB.ourNodeInfo?.user?.hwModel?.number)
}
2021-03-04 09:08:29 +08:00
/// hardware info about our local device (can be null)
2022-04-22 17:22:06 -03:00
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
fun setMyNodeInfo(info: MyNodeInfo?) {
_myNodeInfo.value = info
}
2020-05-13 17:00:23 -07:00
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
}
2022-04-22 17:22:06 -03:00
/// Pull our latest node db from the device
fun updateNodesFromDevice() {
meshService?.let { service ->
// Update our nodeinfos based on data from the device
val nodes = service.nodes.associateBy { it.user?.id!! }
nodeDB.setNodes(nodes)
try {
// Pull down our real node ID - This must be done AFTER reading the nodedb because we need the DB to find our nodeinof object
nodeDB.setMyId(service.myId)
val ownerName = nodeDB.ourNodeInfo?.user?.longName
_ownerName.value = ownerName
} catch (ex: Exception) {
warn("Ignoring failure to get myId, service is probably just uninited... ${ex.message}")
}
}
}
2021-02-27 11:44:05 +08:00
/**
* Return the primary channel info
*/
2022-04-22 17:22:06 -03:00
val primaryChannel: Channel? get() = _channels.value?.primaryChannel
2021-02-27 13:43:55 +08:00
// Set the radio config (also updates our saved copy in preferences)
2022-06-10 21:55:26 -03:00
private fun setDeviceConfig(config: ConfigProtos.Config) {
meshService?.deviceConfig = config.toByteArray()
}
fun setLocalConfig(localConfig: LocalOnlyProtos.LocalConfig) {
2022-06-17 02:00:18 -03:00
if (_localConfig.value == localConfig) return
2022-06-10 21:55:26 -03:00
_localConfig.value = localConfig
2021-02-27 11:44:05 +08:00
}
/// Set the radio config (also updates our saved copy in preferences)
2021-02-27 13:43:55 +08:00
fun setChannels(c: ChannelSet) {
2022-06-17 02:00:18 -03:00
if (_channels.value == c) return
2021-02-27 11:44:05 +08:00
debug("Setting new channels!")
2021-02-27 13:43:55 +08:00
meshService?.channels = c.protobuf.toByteArray()
2022-04-22 17:22:06 -03:00
_channels.value =
2021-02-27 11:44:05 +08:00
c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings)
2020-04-07 17:42:31 -07:00
2022-04-22 17:22:06 -03:00
preferences.edit {
2021-02-27 13:43:55 +08:00
this.putString("channel-url", c.getChannelUrl().toString())
}
}
2020-02-17 13:34:52 -08:00
/// Kinda ugly - created in the activity but used from Compose - figure out if there is a cleaner way GIXME
// lateinit var googleSignInClient: GoogleSignInClient
/// our name in hte radio
/// Note, we generate owner initials automatically for now
2020-02-18 12:22:45 -08:00
/// our activity will read this from prefs or set it to the empty string
2022-04-22 17:22:06 -03:00
private val _ownerName = MutableLiveData<String?>()
val ownerName: LiveData<String?> get() = _ownerName
2020-03-15 16:30:12 -07:00
2022-06-22 22:07:55 -03:00
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {
2021-06-23 12:41:44 -07:00
override fun setValue(value: Boolean) {
super.setValue(value)
2022-04-22 17:22:06 -03:00
preferences.edit {
2022-06-22 22:07:55 -03:00
this.putBoolean("provide-location", value)
2021-06-23 12:41:44 -07:00
}
}
}
2020-02-18 10:40:02 -08:00
// clean up all this nasty owner state management FIXME
fun setOwner(s: String? = null) {
2020-02-18 12:22:45 -08:00
2020-02-18 10:40:02 -08:00
if (s != null) {
2022-04-22 17:22:06 -03:00
_ownerName.value = s
2020-02-18 12:22:45 -08:00
// note: we allow an empty userstring to be written to prefs
2022-04-22 17:22:06 -03:00
preferences.edit {
2020-02-18 10:40:02 -08:00
putString("owner", s)
}
}
// Note: we are careful to not set a new unique ID
2022-04-22 17:22:06 -03:00
if (_ownerName.value!!.isNotEmpty())
2020-02-18 12:22:45 -08:00
try {
meshService?.setOwner(
null,
2022-04-22 17:22:06 -03:00
_ownerName.value,
getInitials(_ownerName.value!!)
2020-02-18 12:22:45 -08:00
) // Note: we use ?. here because we might be running in the emulator
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")
2020-02-18 12:22:45 -08:00
}
2020-02-18 10:40:02 -08:00
}
2022-02-03 18:15:06 -08:00
2022-06-06 17:29:09 -03:00
fun requestShutdown() {
meshService?.requestShutdown(DataPacket.ID_LOCAL)
}
fun requestReboot() {
meshService?.requestReboot(DataPacket.ID_LOCAL)
}
2022-08-23 11:13:47 +01:00
fun requestFactoryReset() {
val config = _localConfig.value
if (config != null) {
val builder = config.device.toBuilder()
builder.factoryReset = true
val newConfig = ConfigProtos.Config.newBuilder()
newConfig.device = builder.build()
setDeviceConfig(newConfig.build())
}
}
2022-02-03 18:15:06 -08:00
/**
* Write the persisted packet data out to a CSV file in the specified location.
*/
fun saveMessagesCSV(file_uri: Uri) {
viewModelScope.launch(Dispatchers.Main) {
// Extract distances to this device from position messages and put (node,SNR,distance) in
// the file_uri
val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch
// Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap()
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
meshPosition?.let { Position(it) }.takeIf {
it?.isValid() == true
}
}
2022-02-03 18:15:06 -08:00
writeToUri(file_uri) { writer ->
// Create a map of nodes keyed by their ID
val nodesById = nodes.values.associateBy { it.num }.toMutableMap()
val nodePositions = mutableMapOf<Int, MeshProtos.Position?>()
2022-02-03 18:15:06 -08:00
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
// Packets are ordered by time, we keep most recent position of
// our device in localNodePosition.
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
2022-06-10 21:55:26 -03:00
packetRepository.getAllPacketsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
// If we get a NodeInfo packet, use it to update our position data (if valid)
packet.nodeInfo?.let { nodeInfo ->
positionToPos.invoke(nodeInfo.position)?.let { _ ->
nodePositions[nodeInfo.num] = nodeInfo.position
}
}
packet.meshPacket?.let { proto ->
// If the packet contains position data then use it to update, if valid
2022-02-03 18:15:06 -08:00
packet.position?.let { position ->
positionToPos.invoke(position)?.let { _ ->
nodePositions[proto.from] = position
}
}
// Filter out of our results any packet that doesn't report SNR. This
// is primarily ADMIN_APP.
if (proto.rxSnr > 0.0f) {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPosition = nodePositions[proto.from]
val senderPos = positionToPos.invoke(senderPosition)
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPosition = nodePositions[myNodeNum]
val rxPos = positionToPos.invoke(rxPosition)
val rxLat = rxPos?.latitude ?: ""
val rxLong = rxPos?.longitude ?: ""
val rxAlt = rxPos?.altitude ?: ""
val rxSnr = "%f".format(proto.rxSnr)
// Calculate the distance if both positions are valid
val dist = if (senderPos == null || rxPos == null) {
""
2022-02-03 18:15:06 -08:00
} else {
positionToMeter(
rxPosition!!, // Use rxPosition but only if rxPos was valid
senderPosition!! // Use senderPosition but only if senderPos was valid
).roundToInt().toString()
}
val hopLimit = proto.hopLimit
val payload = when {
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
.replace("\"", "\\\"") + "\""
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
else -> ""
2022-02-03 18:15:06 -08:00
}
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
2022-02-03 18:15:06 -08:00
}
}
}
}
}
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
withContext(Dispatchers.IO) {
2022-02-13 08:09:26 -03:00
try {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
BufferedWriter(fileWriter).use { writer ->
block.invoke(writer)
}
2022-02-03 18:15:06 -08:00
}
}
2022-02-13 08:09:26 -03:00
} catch (ex: FileNotFoundException) {
errormsg("Can't write file error: ${ex.message}")
2022-02-03 18:15:06 -08:00
}
}
}
2022-08-10 17:29:17 +01:00
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
2022-08-11 16:43:26 +01:00
viewModelScope.launch(Dispatchers.Main) {
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
2022-08-11 16:43:26 +01:00
quickChatActionRepository.insert(action)
}
}
fun deleteQuickChatAction(action: QuickChatAction) {
viewModelScope.launch(Dispatchers.Main) {
quickChatActionRepository.delete(action)
2022-08-11 16:43:26 +01:00
}
}
fun updateQuickChatAction(
action: QuickChatAction,
name: String?,
message: String?,
mode: QuickChatAction.Mode?
) {
viewModelScope.launch(Dispatchers.Main) {
val newAction = QuickChatAction(
action.uuid,
name ?: action.name,
message ?: action.message,
mode ?: action.mode,
action.position
2022-08-11 16:43:26 +01:00
)
quickChatActionRepository.update(newAction)
}
2022-08-10 17:29:17 +01:00
}
fun updateActionPositions(actions: List<QuickChatAction>) {
viewModelScope.launch(Dispatchers.Main) {
2022-08-16 12:25:40 +01:00
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
}
}
2020-02-17 13:34:52 -08:00
}