diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutManager.kt index 2fc0dc083..db6fd37d4 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutManager.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutManager.kt @@ -158,6 +158,34 @@ class ConversationShortcutManager( setPackage(context.packageName) } + /** + * Ensures a long-lived conversation shortcut exists for [contactKey]. Called on demand when a notification is about + * to reference a shortcut id that may not have been pre-published (e.g., an incoming DM on a non-primary channel, + * or from a non-favorite node). Android Auto requires a matching published shortcut to project the notification as + * a messaging HUN. + */ + fun ensureConversationShortcut(contactKey: String, person: Person, label: String) { + val alreadyPublished = ShortcutManagerCompat.getDynamicShortcuts(context).any { it.id == contactKey } + if (alreadyPublished) return + val shortcut = + ShortcutInfoCompat.Builder(context, contactKey) + .setShortLabel(label) + .setLongLabel(label) + .setLocusId(LocusIdCompat(contactKey)) + .setPerson(person) + .setLongLived(true) + .setCategories(setOf(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION)) + .setIntent(conversationIntent(contactKey)) + .build() + try { + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } catch (e: IllegalArgumentException) { + Logger.e(e) { "Failed to publish on-demand shortcut $contactKey" } + } catch (e: IllegalStateException) { + Logger.e(e) { "Failed to publish on-demand shortcut $contactKey" } + } + } + private fun createPersonIcon(name: String, backgroundColor: Int, foregroundColor: Int): IconCompat { val size = ICON_SIZE val bitmap = createBitmap(size, size) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt index d3a6dc590..7fba7c087 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt @@ -110,6 +110,7 @@ class MeshServiceNotificationsImpl( private val context: Context, private val packetRepository: Lazy, private val nodeRepository: Lazy, + private val shortcutManager: Lazy, ) : MeshServiceNotifications { private val notificationManager = context.getSystemService()!! @@ -618,6 +619,8 @@ class MeshServiceNotificationsImpl( } val lastMessage = history.last() + ensureShortcutForNotification(contactKey, isBroadcast, channelName, lastMessage) + builder .setCategory(Notification.CATEGORY_MESSAGE) .setAutoCancel(true) @@ -773,6 +776,36 @@ class MeshServiceNotificationsImpl( } } + private fun ensureShortcutForNotification( + contactKey: String, + isBroadcast: Boolean, + channelName: String?, + lastMessage: Message, + ) { + val person = + if (isBroadcast) { + Person.Builder().setName(channelName ?: contactKey).setKey(contactKey).build() + } else { + Person.Builder() + .setName(lastMessage.node.user.long_name) + .setKey(lastMessage.node.user.id) + .setIcon( + createPersonIcon( + lastMessage.node.user.short_name, + lastMessage.node.colors.second, + lastMessage.node.colors.first, + ), + ) + .build() + } + val label = + when { + isBroadcast -> channelName ?: contactKey + else -> lastMessage.node.user.long_name.ifEmpty { lastMessage.node.user.short_name } + } + shortcutManager.value.ensureConversationShortcut(contactKey, person, label) + } + private fun createReplyAction(contactKey: String): NotificationCompat.Action { val replyLabel = getString(Res.string.reply) val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() @@ -792,6 +825,9 @@ class MeshServiceNotificationsImpl( return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_send, replyLabel, replyPendingIntent) .addRemoteInput(remoteInput) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + .setAllowGeneratedReplies(true) .build() } @@ -810,7 +846,10 @@ class MeshServiceNotificationsImpl( PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) - return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_view, label, pendingIntent).build() + return NotificationCompat.Action.Builder(android.R.drawable.ic_menu_view, label, pendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build() } private fun createReactionAction(