From 38942ec557920513f54e172602e32550018ad4c6 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sun, 13 Oct 2024 06:10:28 -0500 Subject: [PATCH] Add local device stats to the service notification (#1307) This commit adds the local device stats to the service notification. This information includes the number of online and total nodes, as well as other local stats. It also updates the notification summary and adds local stats telemetry handling. --- .../geeksville/mesh/service/MeshService.kt | 48 ++++++++++-- .../mesh/service/MeshServiceNotifications.kt | 76 +++++++++++++++---- 2 files changed, 105 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 231c5bf97..c255986c6 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -9,16 +9,17 @@ import android.os.IBinder import android.os.RemoteException import androidx.core.app.ServiceCompat import androidx.core.location.LocationCompat -import com.geeksville.mesh.analytics.DataPair -import com.geeksville.mesh.android.GeeksvilleApplication -import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.* import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.ToRadio +import com.geeksville.mesh.TelemetryProtos.LocalStats +import com.geeksville.mesh.analytics.DataPair +import com.geeksville.mesh.android.GeeksvilleApplication +import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.hasLocationPermission +import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.MeshLog @@ -138,6 +139,7 @@ class MeshService : Service(), Logging { } private var previousSummary: String? = null + private var previousStats: LocalStats? = null /// A mapping of receiver class name to package name - used for explicit broadcasts private val clientPackages = mutableMapOf() @@ -167,6 +169,10 @@ class MeshService : Service(), Logging { 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) */ @@ -823,11 +829,21 @@ class MeshService : Service(), Logging { } } + + private fun handleLocalStats(stats: TelemetryProtos.Telemetry) { + localStatsTelemetry = stats + maybeUpdateServiceStatusNotification() + } + + /// Update our DB of users based on someone sending out a Telemetry subpacket private fun handleReceivedTelemetry( fromNum: Int, t: TelemetryProtos.Telemetry, ) { + if (t.hasLocalStats()) { + handleLocalStats(t) + } updateNodeInfo(fromNum) { when { t.hasDeviceMetrics() -> it.deviceTelemetry = t @@ -1272,10 +1288,30 @@ class MeshService : Service(), Logging { } private fun maybeUpdateServiceStatusNotification() { + var update = false val currentSummary = notificationSummary - if (previousSummary == null || !previousSummary.equals(currentSummary)) { - serviceNotifications.updateServiceStateNotification(currentSummary) + val currentStats = localStats + val currentStatsUpdatedAtMillis = localStatsUpdatedAtMillis + if ( + !currentSummary.isNullOrBlank() && + (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 + ) } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt index 922890ad4..af4e18280 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt @@ -17,14 +17,22 @@ import androidx.core.app.NotificationCompat import androidx.core.graphics.drawable.toBitmapOrNull import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R +import com.geeksville.mesh.TelemetryProtos.LocalStats import com.geeksville.mesh.android.notificationManager import com.geeksville.mesh.util.PendingIntentCompat import java.io.Closeable - +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class MeshServiceNotifications( private val context: Context ) : Closeable { + + companion object { + private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000 + } + private val notificationManager: NotificationManager get() = context.notificationManager // We have two notification channels: one for general service status and another one for messages @@ -93,11 +101,37 @@ class MeshServiceNotifications( } } - fun updateServiceStateNotification(summaryString: String) = + private fun formatStatsString(stats: LocalStats?, currentStatsUpdatedAtMillis: Long?): String { + val updatedAt = "Next update at: ${ + currentStatsUpdatedAtMillis?.let { + val date = Date(it + FIFTEEN_MINUTES_IN_MILLIS) // Add 15 minutes in milliseconds + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + dateFormat.format(date) + } ?: "???" + }" + val statsJoined = stats?.allFields?.mapNotNull { (k, v) -> + if (k.name == "num_online_nodes" || k.name == "num_total_nodes") { + return@mapNotNull null + } + "${ + k.name.replace('_', ' ').split(" ") + .joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } } + }=$v" + }?.joinToString("\n") ?: "No Local Stats" + return "$updatedAt\n$statsJoined" + } + + fun updateServiceStateNotification( + summaryString: String? = null, + localStats: LocalStats? = null, + currentStatsUpdatedAtMillis: Long? = null, + ) { + val statsString = formatStatsString(localStats, currentStatsUpdatedAtMillis) notificationManager.notify( notifyId, - createServiceStateNotification(summaryString) + createServiceStateNotification(summaryString.orEmpty(), statsString) ) + } fun updateMessageNotification(name: String, message: String) = notificationManager.notify( @@ -139,28 +173,44 @@ class MeshServiceNotifications( builder.setSmallIcon( // vector form icons don't work reliably on older androids - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) R.drawable.app_icon_novect - else R.drawable.app_icon + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + R.drawable.app_icon_novect + } else { + R.drawable.app_icon + } ) .setLargeIcon(largeIcon) } return builder } - fun createServiceStateNotification(summaryString: String): Notification { - val builder = commonBuilder(channelId) - with(builder) { + lateinit var serviceNotificationBuilder: NotificationCompat.Builder + fun createServiceStateNotification(name: String, message: String? = null): Notification { + if (!::serviceNotificationBuilder.isInitialized) { + serviceNotificationBuilder = commonBuilder(channelId) + } + with(serviceNotificationBuilder) { priority = NotificationCompat.PRIORITY_MIN setCategory(Notification.CATEGORY_SERVICE) setOngoing(true) - setContentTitle(summaryString) // leave this off for now so our notification looks smaller + setContentTitle(name) + message?.let { + setContentText(it) + setStyle( + NotificationCompat.BigTextStyle() + .bigText(message), + ) + } } - return builder.build() + return serviceNotificationBuilder.build() } + lateinit var messageNotificationBuilder: NotificationCompat.Builder private fun createMessageNotification(name: String, message: String): Notification { - val builder = commonBuilder(messageChannelId) - with(builder) { + if (!::messageNotificationBuilder.isInitialized) { + messageNotificationBuilder = commonBuilder(messageChannelId) + } + with(messageNotificationBuilder) { priority = NotificationCompat.PRIORITY_DEFAULT setCategory(Notification.CATEGORY_MESSAGE) setAutoCancel(true) @@ -171,7 +221,7 @@ class MeshServiceNotifications( .bigText(message), ) } - return builder.build() + return messageNotificationBuilder.build() } override fun close() {