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

359 lines
14 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
import androidx.lifecycle.AndroidViewModel
2020-09-23 22:47:45 -04:00
import androidx.lifecycle.LiveData
2020-04-07 17:42:31 -07:00
import androidx.lifecycle.MutableLiveData
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.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService
2022-02-03 18:15:06 -08:00
import com.geeksville.mesh.ui.positionToMeter
2020-09-23 22:47:45 -04:00
import kotlinx.coroutines.Dispatchers
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
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
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) {
in 0..minchars - 1 -> {
2020-09-27 14:05:10 -07:00
val nm = if (name.length >= 1)
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
2021-02-21 12:07:53 +08:00
class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging {
2020-09-23 22:47:45 -04:00
private val repository: PacketRepository
val allPackets: LiveData<List<Packet>>
2020-04-07 17:42:31 -07:00
init {
2020-09-23 22:47:45 -04:00
val packetsDao = MeshtasticDatabase.getDatabase(app).packetDao()
repository = PacketRepository(packetsDao)
allPackets = repository.allPackets
2020-04-07 17:42:31 -07:00
debug("ViewModel created")
}
2020-09-23 22:47:45 -04:00
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet)
}
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
2020-04-07 17:42:31 -07:00
companion object {
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
2021-02-21 12:07:53 +08:00
private val context: Context get() = app.applicationContext
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)
2020-04-07 17:42:31 -07:00
/// Are we connected to our radio device
2020-04-08 08:16:06 -07:00
val isConnected =
object :
MutableLiveData<MeshService.ConnectionState>(MeshService.ConnectionState.DISCONNECTED) {
2020-04-08 08:16:06 -07:00
}
2020-04-07 17:42:31 -07:00
/// various radio settings (including the channel)
2021-02-27 13:43:55 +08:00
val radioConfig = object : MutableLiveData<RadioConfigProtos.RadioConfig?>(null) {
2020-04-08 08:16:06 -07:00
}
2020-04-07 17:42:31 -07:00
2021-02-27 13:43:55 +08:00
val channels = object : MutableLiveData<ChannelSet?>(null) {
2021-02-27 11:44:05 +08:00
}
2021-02-05 21:29:28 -08:00
var positionBroadcastSecs: Int?
get() {
radioConfig.value?.preferences?.let {
if (it.positionBroadcastSecs > 0) return it.positionBroadcastSecs
// These default values are borrowed from the device code.
if (it.isRouter) return 60 * 60
return 15 * 60
}
return null
}
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
val builder = config.toBuilder()
2022-01-24 18:23:09 -03:00
builder.preferencesBuilder.positionBroadcastSecs = value
setRadioConfig(builder.build())
}
}
var lsSleepSecs: Int?
get() = radioConfig.value?.preferences?.lsSecs
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
val builder = config.toBuilder()
builder.preferencesBuilder.lsSecs = value
setRadioConfig(builder.build())
}
}
var locationShare: Boolean?
get() {
return radioConfig.value?.preferences?.locationShare == RadioConfigProtos.LocationSharing.LocEnabled
|| radioConfig.value?.preferences?.locationShare == RadioConfigProtos.LocationSharing.LocUnset
2022-01-24 18:23:09 -03:00
}
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
val builder = config.toBuilder()
if (value == true) {
2021-02-05 21:29:28 -08:00
builder.preferencesBuilder.locationShare =
RadioConfigProtos.LocationSharing.LocUnset
2021-02-06 22:08:49 -08:00
} else {
2021-02-05 21:29:28 -08:00
builder.preferencesBuilder.locationShare =
2021-02-27 13:43:55 +08:00
RadioConfigProtos.LocationSharing.LocDisabled
2021-02-06 22:08:49 -08:00
}
2021-02-05 21:29:28 -08:00
setRadioConfig(builder.build())
}
}
2022-01-24 18:23:09 -03:00
var isPowerSaving: Boolean?
get() = radioConfig.value?.preferences?.isPowerSaving
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
val builder = config.toBuilder()
2022-01-24 18:23:09 -03:00
builder.preferencesBuilder.isPowerSaving = value
setRadioConfig(builder.build())
2021-02-05 21:29:28 -08:00
}
}
2021-12-21 16:28:57 -03:00
var isAlwaysPowered: Boolean?
get() = radioConfig.value?.preferences?.isAlwaysPowered
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
val builder = config.toBuilder()
builder.preferencesBuilder.isAlwaysPowered = value
setRadioConfig(builder.build())
}
}
var region: RadioConfigProtos.RegionCode
2021-03-29 20:33:06 +08:00
get() = meshService?.region?.let { RadioConfigProtos.RegionCode.forNumber(it) }
?: RadioConfigProtos.RegionCode.Unset
2021-02-05 21:29:28 -08:00
set(value) {
meshService?.region = value.number
2021-02-05 21:29:28 -08:00
}
2021-03-04 09:08:29 +08:00
/// hardware info about our local device (can be null)
val myNodeInfo = object : MutableLiveData<MyNodeInfo?>(null) {}
2020-05-13 17:00:23 -07:00
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
}
2021-02-27 11:44:05 +08:00
/**
* Return the primary channel info
*/
2021-02-27 13:43:55 +08:00
val primaryChannel: Channel? get() = channels.value?.primaryChannel
///
// Set the radio config (also updates our saved copy in preferences)
private fun setRadioConfig(c: RadioConfigProtos.RadioConfig) {
2020-04-07 17:42:31 -07:00
debug("Setting new radio config!")
meshService?.radioConfig = c.toByteArray()
radioConfig.value =
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)
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) {
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()
2021-02-27 11:44:05 +08:00
channels.value =
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
getPreferences(context).edit(commit = true) {
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
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
2020-03-15 18:44:10 -07:00
}
2020-03-15 16:30:12 -07:00
2020-02-18 10:40:02 -08:00
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
}
2021-06-23 12:41:44 -07:00
val provideLocation = object : MutableLiveData<Boolean>(getPreferences(context).getBoolean(MyPreferences.provideLocationKey, false)) {
override fun setValue(value: Boolean) {
super.setValue(value)
getPreferences(context).edit(commit = true) {
this.putBoolean(MyPreferences.provideLocationKey, value)
}
}
}
/// If the app was launched because we received a new channel intent, the Url will be here
var requestedChannelUrl: Uri? = null
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) {
ownerName.value = s
2020-02-18 12:22:45 -08:00
// note: we allow an empty userstring to be written to prefs
2020-03-02 08:41:16 -08:00
getPreferences(context).edit(commit = true) {
2020-02-18 10:40:02 -08:00
putString("owner", s)
}
}
// Note: we are careful to not set a new unique ID
if (ownerName.value!!.isNotEmpty())
2020-02-18 12:22:45 -08:00
try {
meshService?.setOwner(
null,
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
/**
* 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()
writeToUri(file_uri) { writer ->
// Create a map of nodes keyed by their ID
val nodesById = nodes.values.associateBy { it.num }
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.
var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
repository.allPacketsInReceiveOrder.first().forEach { packet ->
packet.proto?.let { proto ->
packet.position?.let { position ->
if (proto.from == myNodeNum) {
localNodePosition = position
} else {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPos = packet.position
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPos = localNodePosition
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
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) {
""
} else {
positionToMeter(
localNodePosition!!,
position
).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 -> ""
}
// 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")
}
}
}
}
}
}
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
withContext(Dispatchers.IO) {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
BufferedWriter(fileWriter).use { writer ->
block.invoke(writer)
}
}
}
}
}
2020-02-17 13:34:52 -08:00
}