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() {