mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix: service status notification refactor (#3386)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
da65dfdd15
commit
38c50799cb
3 changed files with 109 additions and 102 deletions
|
|
@ -18,6 +18,7 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -45,7 +46,6 @@ import com.geeksville.mesh.PaxcountProtos
|
|||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.StoreAndForwardProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.XmodemProtos
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.copy
|
||||
|
|
@ -207,9 +207,6 @@ class MeshService : Service() {
|
|||
private var configNonce = 1
|
||||
}
|
||||
|
||||
private var previousSummary: String? = null
|
||||
private var previousStats: LocalStats? = null
|
||||
|
||||
private val serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
|
||||
|
|
@ -228,22 +225,6 @@ class MeshService : Service() {
|
|||
return name ?: getString(R.string.unknown_username)
|
||||
}
|
||||
|
||||
private val notificationSummary
|
||||
get() =
|
||||
when (connectionStateHolder.getState()) {
|
||||
ConnectionState.CONNECTED -> getString(R.string.connected_count).format(numOnlineNodes)
|
||||
|
||||
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
||||
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
||||
}
|
||||
|
||||
private var localStatsTelemetry: TelemetryProtos.Telemetry? = null
|
||||
private val localStats: LocalStats?
|
||||
get() = localStatsTelemetry?.localStats
|
||||
|
||||
private val localStatsUpdatedAtMillis: Long?
|
||||
get() = localStatsTelemetry?.time?.let { it * 1000L }
|
||||
|
||||
/** start our location requests (if they weren't already running) */
|
||||
private fun startLocationRequests() {
|
||||
// If we're already observing updates, don't register again
|
||||
|
|
@ -368,12 +349,7 @@ class MeshService : Service() {
|
|||
val wantForeground = a != null && a != NO_DEVICE_SELECTED
|
||||
|
||||
Timber.i("Requesting foreground service=$wantForeground")
|
||||
|
||||
// We always start foreground because that's how our service is always started (if we didn't
|
||||
// then android would
|
||||
// kill us)
|
||||
// but if we don't really need foreground we immediately stop it.
|
||||
val notification = serviceNotifications.updateServiceStateNotification(notificationSummary)
|
||||
val notification = updateServiceStatusNotification()
|
||||
|
||||
try {
|
||||
ServiceCompat.startForeground(
|
||||
|
|
@ -976,35 +952,34 @@ class MeshService : Service() {
|
|||
}
|
||||
|
||||
// Update our DB of users based on someone sending out a Telemetry subpacket
|
||||
private fun handleReceivedTelemetry(fromNum: Int, t: TelemetryProtos.Telemetry) {
|
||||
private fun handleReceivedTelemetry(fromNum: Int, telemetry: TelemetryProtos.Telemetry) {
|
||||
val isRemote = (fromNum != myNodeNum)
|
||||
if (!isRemote && t.hasLocalStats()) {
|
||||
localStatsTelemetry = t
|
||||
maybeUpdateServiceStatusNotification()
|
||||
if (!isRemote) {
|
||||
updateServiceStatusNotification(telemetry = telemetry)
|
||||
}
|
||||
updateNodeInfo(fromNum) {
|
||||
updateNodeInfo(fromNum) { nodeEntity ->
|
||||
when {
|
||||
t.hasDeviceMetrics() -> {
|
||||
it.deviceTelemetry = t
|
||||
if (fromNum == myNodeNum || (isRemote && it.isFavorite)) {
|
||||
telemetry.hasDeviceMetrics() -> {
|
||||
nodeEntity.deviceTelemetry = telemetry
|
||||
if (fromNum == myNodeNum || (isRemote && nodeEntity.isFavorite)) {
|
||||
if (
|
||||
t.deviceMetrics.voltage > batteryPercentUnsupported &&
|
||||
t.deviceMetrics.batteryLevel <= batteryPercentLowThreshold
|
||||
telemetry.deviceMetrics.voltage > batteryPercentUnsupported &&
|
||||
telemetry.deviceMetrics.batteryLevel <= batteryPercentLowThreshold
|
||||
) {
|
||||
if (shouldBatteryNotificationShow(fromNum, t)) {
|
||||
serviceNotifications.showOrUpdateLowBatteryNotification(it, isRemote)
|
||||
if (shouldBatteryNotificationShow(fromNum, telemetry)) {
|
||||
serviceNotifications.showOrUpdateLowBatteryNotification(nodeEntity, isRemote)
|
||||
}
|
||||
} else {
|
||||
if (batteryPercentCooldowns.containsKey(fromNum)) {
|
||||
batteryPercentCooldowns.remove(fromNum)
|
||||
}
|
||||
serviceNotifications.cancelLowBatteryNotification(it)
|
||||
serviceNotifications.cancelLowBatteryNotification(nodeEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.hasEnvironmentMetrics() -> it.environmentTelemetry = t
|
||||
t.hasPowerMetrics() -> it.powerTelemetry = t
|
||||
telemetry.hasEnvironmentMetrics() -> nodeEntity.environmentTelemetry = telemetry
|
||||
telemetry.hasPowerMetrics() -> nodeEntity.powerTelemetry = telemetry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1095,7 +1070,6 @@ class MeshService : Service() {
|
|||
}
|
||||
.build(),
|
||||
)
|
||||
onNodeDBChanged()
|
||||
} else {
|
||||
Timber.w("Ignoring early received packet: ${packet.toOneLineString()}")
|
||||
// earlyReceivedPackets.add(packet)
|
||||
|
|
@ -1228,11 +1202,6 @@ class MeshService : Service() {
|
|||
|
||||
private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt()
|
||||
|
||||
// If we just changed our nodedb, we might want to do somethings
|
||||
private fun onNodeDBChanged() {
|
||||
maybeUpdateServiceStatusNotification()
|
||||
}
|
||||
|
||||
/** Send in analytics about mesh connection */
|
||||
private fun reportConnection() {
|
||||
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
|
||||
|
|
@ -1337,30 +1306,21 @@ class MeshService : Service() {
|
|||
ConnectionState.DISCONNECTED -> startDisconnect()
|
||||
}
|
||||
|
||||
// Update the android notification in the status bar
|
||||
maybeUpdateServiceStatusNotification()
|
||||
updateServiceStatusNotification()
|
||||
}
|
||||
|
||||
private fun maybeUpdateServiceStatusNotification() {
|
||||
var update = false
|
||||
val currentSummary = notificationSummary
|
||||
val currentStats = localStats
|
||||
val currentStatsUpdatedAtMillis = localStatsUpdatedAtMillis
|
||||
if (currentSummary.isNotBlank() && (previousSummary == null || !previousSummary.equals(currentSummary))) {
|
||||
previousSummary = currentSummary
|
||||
update = true
|
||||
}
|
||||
if (currentStats != null && (previousStats == null || !(previousStats?.equals(currentStats) ?: false))) {
|
||||
previousStats = currentStats
|
||||
update = true
|
||||
}
|
||||
if (update) {
|
||||
serviceNotifications.updateServiceStateNotification(
|
||||
summaryString = currentSummary,
|
||||
localStats = currentStats,
|
||||
currentStatsUpdatedAtMillis = currentStatsUpdatedAtMillis,
|
||||
)
|
||||
}
|
||||
private fun updateServiceStatusNotification(telemetry: TelemetryProtos.Telemetry? = null): Notification {
|
||||
val notificationSummary =
|
||||
when (connectionStateHolder.getState()) {
|
||||
ConnectionState.CONNECTED -> getString(R.string.connected_count).format(numOnlineNodes)
|
||||
|
||||
ConnectionState.DISCONNECTED -> getString(R.string.disconnected)
|
||||
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
|
||||
}
|
||||
return serviceNotifications.updateServiceStateNotification(
|
||||
summaryString = notificationSummary,
|
||||
telemetry = telemetry,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRadioConnectionState(newState: ConnectionState) {
|
||||
|
|
@ -1573,7 +1533,7 @@ class MeshService : Service() {
|
|||
* Regenerate the myNodeInfo model. We call this twice. Once after we receive myNodeInfo from the device and again
|
||||
* after we have the node DB (which might allow us a better notion of our HwModel.
|
||||
*/
|
||||
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata) {
|
||||
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata? = MeshProtos.DeviceMetadata.getDefaultInstance()) {
|
||||
val myInfo = rawMyNodeInfo
|
||||
if (myInfo != null) {
|
||||
val mi =
|
||||
|
|
@ -1581,25 +1541,27 @@ class MeshService : Service() {
|
|||
MyNodeEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
model =
|
||||
when (val hwModel = metadata.hwModel) {
|
||||
when (val hwModel = metadata?.hwModel) {
|
||||
null,
|
||||
MeshProtos.HardwareModel.UNSET,
|
||||
-> null
|
||||
|
||||
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
},
|
||||
firmwareVersion = metadata.firmwareVersion,
|
||||
firmwareVersion = metadata?.firmwareVersion,
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false, // TODO add check after re-implementing firmware updates
|
||||
currentPacketId = currentPacketId and 0xffffffffL,
|
||||
messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code
|
||||
minAppVersion = minAppVersion,
|
||||
maxChannels = 8,
|
||||
hasWifi = metadata.hasWifi,
|
||||
hasWifi = metadata?.hasWifi == true,
|
||||
deviceId = deviceId.toStringUtf8(),
|
||||
)
|
||||
}
|
||||
serviceScope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) }
|
||||
if (metadata != null && metadata != MeshProtos.DeviceMetadata.getDefaultInstance()) {
|
||||
serviceScope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) }
|
||||
}
|
||||
newMyNodeInfo = mi
|
||||
}
|
||||
}
|
||||
|
|
@ -1626,6 +1588,7 @@ class MeshService : Service() {
|
|||
insertMeshLog(packetToSave)
|
||||
|
||||
rawMyNodeInfo = myInfo
|
||||
regenMyNodeInfo()
|
||||
|
||||
// We'll need to get a new set of channels and settings now
|
||||
serviceScope.handledLaunch {
|
||||
|
|
@ -1809,7 +1772,6 @@ class MeshService : Service() {
|
|||
haveNodeDB = true // we now have nodes from real hardware
|
||||
sendAnalytics()
|
||||
onHasSettings()
|
||||
onNodeDBChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.core.net.toUri
|
|||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R.raw
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
|
|
@ -164,6 +165,7 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
NotificationType.ServiceState -> {
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
|
||||
NotificationType.DirectMessage,
|
||||
NotificationType.BroadcastMessage,
|
||||
NotificationType.NewNode,
|
||||
|
|
@ -180,6 +182,7 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
)
|
||||
if (type == NotificationType.LowBatteryRemote) enableVibration(true)
|
||||
}
|
||||
|
||||
NotificationType.Alert -> {
|
||||
setShowBadge(true)
|
||||
enableLights(true)
|
||||
|
|
@ -194,6 +197,7 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
NotificationType.Client -> {
|
||||
setShowBadge(true)
|
||||
}
|
||||
|
|
@ -202,17 +206,44 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
var cachedTelemetry: TelemetryProtos.Telemetry? = null
|
||||
var cachedLocalStats: LocalStats? = null
|
||||
var nextStatsUpdateMillis: Long = 0
|
||||
var cachedMessage: String? = null
|
||||
|
||||
// region Public Notification Methods
|
||||
fun updateServiceStateNotification(
|
||||
summaryString: String?,
|
||||
localStats: LocalStats? = null,
|
||||
currentStatsUpdatedAtMillis: Long? = System.currentTimeMillis(),
|
||||
telemetry: TelemetryProtos.Telemetry? = cachedTelemetry,
|
||||
): Notification {
|
||||
val hasLocalStats = telemetry?.hasLocalStats() == true
|
||||
val hasDeviceMetrics = telemetry?.hasDeviceMetrics() == true
|
||||
val message =
|
||||
if (hasLocalStats) {
|
||||
val localStats = telemetry.localStats
|
||||
val localStatsMessage = localStats?.formatToString()
|
||||
cachedTelemetry = telemetry
|
||||
nextStatsUpdateMillis = System.currentTimeMillis() + FIFTEEN_MINUTES_IN_MILLIS
|
||||
localStatsMessage
|
||||
} else if (cachedTelemetry == null && hasDeviceMetrics) {
|
||||
val deviceMetrics = telemetry.deviceMetrics
|
||||
val deviceMetricsMessage = deviceMetrics.formatToString()
|
||||
if (cachedLocalStats == null) {
|
||||
cachedTelemetry = telemetry
|
||||
}
|
||||
nextStatsUpdateMillis = System.currentTimeMillis()
|
||||
deviceMetricsMessage
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
cachedMessage = message ?: cachedMessage ?: context.getString(R.string.no_local_stats)
|
||||
|
||||
val notification =
|
||||
createServiceStateNotification(
|
||||
name = summaryString.orEmpty(),
|
||||
message = localStats.formatToString(),
|
||||
nextUpdateAt = currentStatsUpdatedAtMillis?.plus(FIFTEEN_MINUTES_IN_MILLIS),
|
||||
message = cachedMessage,
|
||||
nextUpdateAt = nextStatsUpdateMillis,
|
||||
)
|
||||
notificationManager.notify(SERVICE_NOTIFY_ID, notification)
|
||||
return notification
|
||||
|
|
@ -270,11 +301,13 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
builder.setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
||||
}
|
||||
|
||||
nextUpdateAt?.let {
|
||||
builder.setWhen(it)
|
||||
builder.setUsesChronometer(true)
|
||||
builder.setChronometerCountDown(true)
|
||||
}
|
||||
nextUpdateAt
|
||||
?.takeIf { it > System.currentTimeMillis() }
|
||||
?.let {
|
||||
builder.setWhen(it)
|
||||
builder.setUsesChronometer(true)
|
||||
builder.setChronometerCountDown(true)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
|
@ -427,23 +460,34 @@ class MeshServiceNotifications(private val context: Context) {
|
|||
}
|
||||
|
||||
// Extension function to format LocalStats into a readable string.
|
||||
private fun LocalStats?.formatToString(): String {
|
||||
if (this == null) return "No Local Stats"
|
||||
|
||||
return this.allFields
|
||||
.mapNotNull { (k, v) ->
|
||||
when (k.name) {
|
||||
"num_online_nodes",
|
||||
"num_total_nodes",
|
||||
-> null // Exclude these fields
|
||||
"uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}"
|
||||
"channel_utilization" -> "ChUtil: %.2f%%".format(v)
|
||||
"air_util_tx" -> "AirUtilTX: %.2f%%".format(v)
|
||||
else -> {
|
||||
val formattedKey = k.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
"$formattedKey: $v"
|
||||
}
|
||||
private fun LocalStats?.formatToString(): String? = this?.allFields
|
||||
?.mapNotNull { (k, v) ->
|
||||
when (k.name) {
|
||||
"num_online_nodes",
|
||||
"num_total_nodes",
|
||||
-> null // Exclude these fields
|
||||
"uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}"
|
||||
"channel_utilization" -> "ChUtil: %.2f%%".format(v)
|
||||
"air_util_tx" -> "AirUtilTX: %.2f%%".format(v)
|
||||
else -> {
|
||||
val formattedKey = k.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
"$formattedKey: $v"
|
||||
}
|
||||
}
|
||||
.joinToString("\n")
|
||||
}
|
||||
}
|
||||
?.joinToString("\n")
|
||||
|
||||
private fun TelemetryProtos.DeviceMetrics?.formatToString(): String? = this?.allFields
|
||||
?.mapNotNull { (k, v) ->
|
||||
when (k.name) {
|
||||
"battery_level" -> "Battery Level: $v"
|
||||
"uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}"
|
||||
"channel_utilization" -> "ChUtil: %.2f%%".format(v)
|
||||
"air_util_tx" -> "AirUtilTX: %.2f%%".format(v)
|
||||
else -> {
|
||||
val formattedKey = k.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
"$formattedKey: $v"
|
||||
}
|
||||
}
|
||||
}
|
||||
?.joinToString("\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue