From 07772917c36e5148a815af15dd10b7f9fdab7a7c Mon Sep 17 00:00:00 2001 From: James Rich Date: Fri, 17 Apr 2026 08:48:38 -0500 Subject: [PATCH] fix(auto): project messaging notifications to Android Auto Gearhead's MsgNotifParser was rejecting Meshtastic notifications with: - 'No semantic reply action found' - 'No semantic mark-as-read action found' - 'added an invalid shortcut' Fixes: - Tag reply action with SEMANTIC_ACTION_REPLY + setShowsUserInterface(false) + setAllowGeneratedReplies(true) so Gearhead/Assistant can surface it. - Tag mark-as-read action with SEMANTIC_ACTION_MARK_AS_READ. - Publish an on-demand long-lived conversation shortcut whose id matches the notification's setShortcutId(contactKey). Previously only favorites + channels at index 0 had shortcuts, so DMs received on a non-zero channel referenced an unpublished shortcut and Android Auto refused to project them. Verified on Pixel 6a + DHU 2.0: notifications now carry matching long-lived shortcuts and project as messaging HUNs with reply, mark-read and reaction actions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../service/ConversationShortcutManager.kt | 28 +++++++++++++ .../service/MeshServiceNotificationsImpl.kt | 41 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) 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(