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 19d1f18e9..8d1106565 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -98,6 +98,8 @@ class MeshService : Service(), Logging { DEVICE_SLEEP // device is in LS sleep state, it will reconnected to us over bluetooth once it has data } + private var previousSummary: String? = null + /// A mapping of receiver class name to package name - used for explicit broadcasts private val clientPackages = mutableMapOf() private val serviceNotifications = MeshServiceNotifications(this) @@ -124,19 +126,11 @@ class MeshService : Service(), Logging { getNodeNum = { myNodeNum } ) - private fun getSenderName(): String { - val recentFrom = recentReceivedTextPacket?.from // safe, immutable copy - return if (recentFrom != null) { - nodeDBbyID[recentFrom]?.user?.longName - ?: recentFrom - } else { - getString(R.string.unknown_username) - } + private fun getSenderName(packet : DataPacket?): String { + val name = nodeDBbyID[packet?.from]?.user?.longName + return name ?: "Unknown username" } - /// A text message that has a arrived since the last notification update - private var recentReceivedTextPacket: DataPacket? = null - private val notificationSummary get() = when (connectionState) { ConnectionState.CONNECTED -> getString(R.string.connected_count).format( @@ -259,11 +253,9 @@ class MeshService : Service(), Logging { }) } - private fun updateNotification() = serviceNotifications.updateNotification( - recentReceivedTextPacket, - notificationSummary, - getSenderName() - ) + private fun updateMessageNotification(message: DataPacket) = + serviceNotifications.updateMessageNotification( + getSenderName(message), message.bytes!!.toString(utf8)) /** * tell android not to kill us @@ -276,11 +268,9 @@ class MeshService : Service(), Logging { // 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.createNotification( - recentReceivedTextPacket, - notificationSummary, - getSenderName() - ) + val notification = serviceNotifications.createServiceStateNotification( + notificationSummary) + startForeground(serviceNotifications.notifyId, notification) if (!wantForeground) { stopForeground(true) @@ -634,9 +624,7 @@ class MeshService : Service(), Logging { when (data.portnumValue) { Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { debug("Received CLEAR_TEXT from $fromId") - - recentReceivedTextPacket = dataPacket - updateNotification() + updateMessageNotification(dataPacket) } // Handle new style position info @@ -814,7 +802,7 @@ class MeshService : Service(), Logging { /// If we just changed our nodedb, we might want to do somethings private fun onNodeDBChanged() { - updateNotification() + maybeUpdateServiceStatusNotification() // we don't ask for GPS locations from android if our device has a built in GPS // Note: myNodeInfo can go away if we lose connections, so it might be null @@ -956,7 +944,15 @@ class MeshService : Service(), Logging { } // Update the android notification in the status bar - updateNotification() + maybeUpdateServiceStatusNotification() + } + + private fun maybeUpdateServiceStatusNotification() { + val currentSummary = notificationSummary + if (previousSummary == null || !previousSummary.equals(currentSummary)) { + serviceNotifications.updateServiceStateNotification(currentSummary) + previousSummary = currentSummary + } } /** 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 67b4903aa..c7b315535 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt @@ -9,6 +9,8 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color +import android.media.AudioAttributes +import android.media.RingtoneManager import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat @@ -18,6 +20,7 @@ import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.android.notificationManager +import com.geeksville.mesh.ui.SLogging import com.geeksville.mesh.utf8 import java.io.Closeable @@ -26,21 +29,47 @@ class MeshServiceNotifications( private val context: Context ) : Closeable { private val notificationManager: NotificationManager get() = context.notificationManager + // We have two notification channels: one for general service status and another one for messages val notifyId = 101 + private val messageNotifyId = 102 + private var largeIcon: Bitmap? = null @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(): String { val channelId = "my_service" val channelName = context.getString(R.string.meshtastic_service_notifications) + val channel = NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_MIN + ).apply { + lightColor = Color.BLUE + lockscreenVisibility = Notification.VISIBILITY_PRIVATE + } + notificationManager.createNotificationChannel(channel) + return channelId + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createMessageNotificationChannel(): String { + val channelId = "my_messages" + val channelName = context.getString(R.string.meshtastic_messages_notifications) val channel = NotificationChannel( channelId, channelName, NotificationManager.IMPORTANCE_HIGH ).apply { lightColor = Color.BLUE - importance = NotificationManager.IMPORTANCE_NONE - lockscreenVisibility = Notification.VISIBILITY_PRIVATE + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(true) + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) } notificationManager.createNotificationChannel(channel) return channelId @@ -56,18 +85,24 @@ class MeshServiceNotifications( } } - /** - * Update our notification with latest data - */ - fun updateNotification( - recentReceivedText: DataPacket?, - summaryString: String, - senderName: String - ) { - val notification = createNotification(recentReceivedText, summaryString, senderName) - notificationManager.notify(notifyId, notification) + private val messageChannelId: String by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createMessageNotificationChannel() + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } } + fun updateServiceStateNotification(summaryString: String) = + notificationManager.notify(notifyId, + createServiceStateNotification(summaryString)) + + fun updateMessageNotification(name: String, message: String) = + notificationManager.notify(messageNotifyId, + createMessageNotifcation(name, message)) + private val openAppIntent: PendingIntent by lazy { PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0) } @@ -91,20 +126,8 @@ class MeshServiceNotifications( return bitmap } - /** - * Generate a new version of our notification - reflecting current app state - */ - fun createNotification( - recentReceivedText: DataPacket?, - summaryString: String, - senderName: String - ): Notification { - val category = - if (recentReceivedText != null) Notification.CATEGORY_SERVICE else Notification.CATEGORY_MESSAGE - val builder = NotificationCompat.Builder(context, channelId).setOngoing(true) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setCategory(category) - .setContentTitle(summaryString) // leave this off for now so our notification looks smaller + fun commonBuilder(channel: String) : NotificationCompat.Builder { + val builder = NotificationCompat.Builder(context, channel) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(openAppIntent) @@ -123,20 +146,32 @@ class MeshServiceNotifications( builder.setSmallIcon(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) R.drawable.app_icon_novect else R.drawable.app_icon) // vector form icons don't work reliably on older androids .setLargeIcon(largeIcon) } + return builder + } - // FIXME, show information about the nearest node - // if(shortContent != null) builder.setContentText(shortContent) + fun createServiceStateNotification(summaryString: String): Notification { + val builder = commonBuilder(channelId) + with(builder) { + setPriority(NotificationCompat.PRIORITY_MIN) + setCategory(Notification.CATEGORY_SERVICE) + setOngoing(true) + setContentTitle(summaryString) // leave this off for now so our notification looks smaller + } + return builder.build() + } - // If a text message arrived include it with our notification - recentReceivedText?.let { packet -> - // Try to show the human name of the sender if possible - builder.setContentText("Message from $senderName") - builder.setStyle( + fun createMessageNotifcation(name: String, message: String): Notification { + val builder = commonBuilder(messageChannelId) + with(builder) { + setPriority(NotificationCompat.PRIORITY_DEFAULT) + setCategory(Notification.CATEGORY_MESSAGE) + setAutoCancel(true) + setContentTitle(name) + setStyle( NotificationCompat.BigTextStyle() - .bigText(packet.bytes!!.toString(utf8)) + .bigText(message), ) } - return builder.build() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b96c0e298..11e7483c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,4 +85,5 @@ Message delivery status Broadcast position period (in seconds), 0 - disable Device sleep period (in seconds) + Notifications about messages