feat(notifications): align message notifications with Android best practices#6371
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughConversation shortcuts are managed by the core service, while notifications use conversation metadata, stable identifiers, cached icons, historic message context, and refreshed inline replies. Android Auto’s dedicated shortcut manager was removed. ChangesConversation notifications
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Sequence Diagram(s)sequenceDiagram
participant ReplyReceiver
participant SendMessageUseCase
participant MeshNotificationManagerImpl
participant ConversationShortcutPublisher
participant ShortcutManager
ReplyReceiver->>SendMessageUseCase: Send inline reply
SendMessageUseCase-->>ReplyReceiver: Return send result
ReplyReceiver->>MeshNotificationManagerImpl: Refresh conversation notification
MeshNotificationManagerImpl->>ConversationShortcutPublisher: Ensure conversation shortcut
ConversationShortcutPublisher->>ShortcutManager: Publish shortcut if absent
MeshNotificationManagerImpl-->>ReplyReceiver: Repost conversation notification
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
aebd344 to
a538cee
Compare
…actices
Message notifications rebuilt the full recent conversation from the database on
every arrival and presented already-read messages as new content, shared a
single integer ID space across all notification types, and never linked to the
conversation shortcuts that feature/car was publishing.
- Read "context" messages now use MessagingStyle.addHistoricMessage() instead of
addMessage(), so they provide context to Auto/Wear/accessibility without being
re-presented as freshly-arrived. Unread messages and reactions remain the
alerting content; a guard keeps the latest message alerting when all are read.
- Every notification type posts via notify(tag, id)/cancel(tag, id), closing
cross-type ID collisions (e.g. a node whose num == 101 clobbering the
foreground-service notification, or num == 1 the group summary).
- Conversation-shortcut publishing moves from feature/car (Auto-session only)
into core/service ConversationShortcutPublisher, owned by the MeshService
lifecycle so shortcuts exist on phones too. DM and broadcast notifications now
set setShortcutId/setLocusId for Conversations-section treatment (Android 11+).
- Conversation shortcuts carry a rendered icon that distinguishes types instead
of a generic head: a wide node-colored pill with the short name for DMs
(mirroring the in-app node chip; shortLabel=short name, longLabel=long name),
and a black rounded-square badge with the channel number for channels. Channel
labels use the effective name so an empty primary shows its modem-preset name
("LongFast"), matching the in-app conversation list. The icon is set on the
shortcut itself (not just the Person), which fixes the generic-head rendering.
- Shortcuts are ranked by recency: DMs and channels are merged and sorted by
last-activity time and setRank is applied, so launchers/Android Auto (which
show only a small capped set) surface the recently-active conversations rather
than an arbitrary slice of stale channels.
- MessagingStyle sender avatars use the same node-colored pill and hold each
participant's full short name (e.g. "WOLF", "2c3d") rather than a single
initial, so senders are identifiable in a busy channel conversation.
- An inline reply now re-posts the conversation silently with the sent reply
appended (refreshConversationAfterReply, default falls back to dismissal), so
the RemoteInput spinner resolves with visible confirmation instead of the
notification vanishing; a failed send still dismisses.
- The group summary alerts via its children only (GROUP_ALERT_CHILDREN), the
service notification shows immediately on Android 12+ instead of being
deferred (FOREGROUND_SERVICE_IMMEDIATE), favorite nodes mark their Person
important to feed conversation-priority ranking, and notifications use the
Meshtastic brand accent color instead of raw blue.
- Avatars are cached per node instead of re-rasterizing a bitmap per message;
PersonIconFactory is shared in core/service (dedupes the car copy) and gained a
shape-aware, auto-sizing label renderer.
Also fixes an orphaned group summary that lingered after mark-as-read/reply:
activeNotifications does not reflect a cancel() issued moments earlier, so
showGroupSummary() rebuilt the summary from stale state. It now excludes the
just-cancelled id so the summary is dropped once the last conversation is gone.
Adds Robolectric regression coverage: the historic-vs-new split, per-type
(tag, id) namespacing, summary alert behavior and cleanup, both post-reply
refresh paths, shortcut recency ranking/labels, and stale-shortcut pruning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a538cee to
fdf099a
Compare
|
@copilot review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt (1)
70-83: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winShortcut publishing runs IPC + bitmap rendering on the Main dispatcher.
startObservingis invoked withMeshService.serviceScope, which usesDispatchers.Main.immediate. Everycollectemission callspublishShortcutssynchronously, which performsShortcutManagerCompatIPC (getDynamicShortcuts,pushDynamicShortcut,removeDynamicShortcuts) and re-rasterizes bitmaps for every shortcut on the Main thread. Since the observed flow re-emits on every recency-affecting DB change, this repeats for the life of the foreground service and can add avoidable jank to the shared Main thread.Move the collection to a background dispatcher.
♻️ Proposed fix
+import org.meshtastic.core.common.util.ioDispatcher ... fun startObserving(scope: CoroutineScope) { observeJob?.cancel() observeJob = - scope.launch { + scope.launch(ioDispatcher) { combine(packetRepository.getContacts(), radioConfigRepository.channelSetFlow) { contacts, channelSet -> rankConversationsByRecency(contacts, channelSet) } .distinctUntilChanged() .collect { conversations -> publishShortcuts(conversations) } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt` around lines 70 - 83, Update startObserving so the launched observation coroutine collects the combined flow on a background dispatcher, rather than inheriting MeshService.serviceScope’s Main dispatcher. Keep the existing ranking, distinctUntilChanged, and publishShortcuts flow unchanged while ensuring publishShortcuts and its shortcut IPC/bitmap work execute off Main.core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt (1)
98-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
ConcurrentHashMapshould be replaced per project convention forandroidMain.
personIconCacheusesjava.util.concurrent.ConcurrentHashMap. The repo's own guideline forandroidMain(not justcommonMain) explicitly calls for replacing this.A Mutex-guarded
mutableMapOf()(oratomicfu) would require makingcachedPersonIconsuspend or wrapping access insynchronized/Mutex.withLock, since call sites here are non-suspend. Given the low contention (single service-owned cache), aMutex-guarded plain map, or simplyCollections.synchronizedMap-style guarding, would satisfy the convention without much churn.
As per coding guidelines, "ReplaceConcurrentHashMapwithatomicfuor a Mutex-guardedmutableMapOf(),java.util.concurrent.locks.*withMutex, andjava.io.*with OkioBufferedSource/BufferedSink."Also applies to: 153-159
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt` at line 98, Replace the ConcurrentHashMap-based personIconCache in MeshNotificationManagerImpl with a Mutex-guarded mutableMapOf, preserving thread-safe access in cachedPersonIcon and the related cache operations around the second referenced block. Since these call sites are non-suspend, use appropriate locking or adjust the access path so synchronization remains correct without retaining java.util.concurrent.ConcurrentHashMap.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt`:
- Around line 226-259: Remove the raw contactKey interpolation from both
exception messages in ensureConversationShortcut. Keep the failure context while
logging only a generic on-demand shortcut publication message, consistent with
the aggregate logging in publishShortcuts.
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt`:
- Around line 77-97: Update the coroutine error handling in ReplyReceiver’s
reply-send flow to use kotlinx.coroutines.CancellationException and safeCatching
for the refreshConversationAfterReply and business-logic cancellation paths,
ensuring cancellation is rethrown rather than logged or swallowed. Retain
runCatching only for the notification cancellation cleanup/teardown calls.
---
Nitpick comments:
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt`:
- Around line 70-83: Update startObserving so the launched observation coroutine
collects the combined flow on a background dispatcher, rather than inheriting
MeshService.serviceScope’s Main dispatcher. Keep the existing ranking,
distinctUntilChanged, and publishShortcuts flow unchanged while ensuring
publishShortcuts and its shortcut IPC/bitmap work execute off Main.
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt`:
- Line 98: Replace the ConcurrentHashMap-based personIconCache in
MeshNotificationManagerImpl with a Mutex-guarded mutableMapOf, preserving
thread-safe access in cachedPersonIcon and the related cache operations around
the second referenced block. Since these call sites are non-suspend, use
appropriate locking or adjust the access path so synchronization remains correct
without retaining java.util.concurrent.ConcurrentHashMap.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6062284f-c817-4e2a-9246-1849dfdc3c0f
📒 Files selected for processing (13)
core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshNotificationManager.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ReplyReceiverTest.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/PersonIconFactory.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.ktfeature/car/src/main/kotlin/org/meshtastic/feature/car/service/ConversationShortcutManager.ktfeature/car/src/main/kotlin/org/meshtastic/feature/car/service/MeshtasticCarSession.ktfeature/car/src/main/kotlin/org/meshtastic/feature/car/util/PersonIconFactory.kt
💤 Files with no reviewable changes (3)
- feature/car/src/main/kotlin/org/meshtastic/feature/car/util/PersonIconFactory.kt
- feature/car/src/main/kotlin/org/meshtastic/feature/car/service/ConversationShortcutManager.kt
- feature/car/src/main/kotlin/org/meshtastic/feature/car/service/MeshtasticCarSession.kt
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt (1)
227-237: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert the DM shortcut label, not only the missing conversation title.
This test claims to validate DM peer-name resolution for the shortcut label, but only checks that a group conversation title is absent. A regression publishing an empty or incorrect DM shortcut label would still pass. Assert the published shortcut label is
"Hawk Ridge"or rename the test to cover only title behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt` around lines 227 - 237, Update the test `refreshConversationAfterReply resolves a dm peer name for the shortcut label` to assert that the published notification shortcut label equals "Hawk Ridge", in addition to or instead of the existing conversation-title assertion. Ensure the assertion targets the notification shortcut metadata produced by `refreshConversationAfterReply`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt`:
- Around line 227-237: Update the test `refreshConversationAfterReply resolves a
dm peer name for the shortcut label` to assert that the published notification
shortcut label equals "Hawk Ridge", in addition to or instead of the existing
conversation-title assertion. Ensure the assertion targets the notification
shortcut metadata produced by `refreshConversationAfterReply`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c1ee78c-09db-4afd-878c-60cbe3f58cac
📒 Files selected for processing (2)
core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
…d cancellation - ConversationShortcutPublisher observes on the IO dispatcher: the caller's scope is Main-bound (MeshService), and publishing does ShortcutManager IPC plus avatar bitmap rasterization on every recency-affecting DB change. - Drop the contactKey from the on-demand shortcut failure logs; node/channel identifiers are user-traceable (privacy-first convention). - ReplyReceiver rethrows CancellationException instead of treating it as a failed send, and uses safeCatching (which preserves cancellation) rather than runCatching for the notification calls. - Strengthen the DM refresh test to assert the resolved peer name on the published shortcut label, not just the absent conversation title. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…MessagingStyle The summary previously reconstructed each line from EXTRA_TITLE/EXTRA_TEXT, so it showed the conversation title as the "sender". Extract the child's MessagingStyle (public compat API) and reuse its latest message — real sender Person and timestamp — falling back to the extras for children without an extractable style. Reimplements the useful half of an errored Copilot session's partial commit (dropped from the branch): that version used the restricted Message.getMessagesFromBundleArray API and also demoted fresh reactions on already-read messages to historic context, which would have buried the very event a reaction notification exists to surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
220ccc8 to
d7cff2d
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt (3)
156-160: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winDo not expose
contactKeyas the fallback display label.When node metadata is unavailable, the raw identifier is rendered as the shortcut’s short and long label. Use a localized generic fallback; keep
contactKeyonly in internal IDs and deep-link metadata.As per coding guidelines, “Never log or expose personally identifiable information, location data, or cryptographic keys.” As per path instructions, “Flag leftover // ... existing code ... placeholders, and any logging of PII, location, or cryptographic keys.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt` around lines 156 - 160, Update the label fallback logic in ConversationShortcutPublisher so shortLabel and longLabel use the existing localized generic fallback when node metadata is missing, rather than dm.contactKey. Keep contactKey restricted to internal identifiers and deep-link metadata, and flag any leftover placeholders or logging that exposes it.Sources: Coding guidelines, Path instructions
193-193: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winLocalize the fallback channel label.
"Channel ${channel.index}"is user-facing shortcut text and should come fromcore:resources, using the project’s nonblocking resource-loading path.As per coding guidelines, “Do not hardcode user-facing strings. Use core:resources…”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt` at line 193, Replace the hardcoded fallback in the channelName assignment with the localized channel-label resource from core:resources, loading it through the project’s established nonblocking resource-access path and passing channel.index as the format argument. Preserve channel.name when non-empty.Source: Coding guidelines
137-140: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftDo not prune on-demand shortcuts from an incomplete snapshot.
publishShortcutscan remove the shortcut created byensureConversationShortcutbefore the contacts flow includes that conversation, breaking the immediate notification-linking contract. Scope cleanup to publisher-owned IDs and preserve pending on-demand shortcuts until the observed snapshot catches up.Also applies to: 224-232
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt` around lines 137 - 140, Update publishShortcuts cleanup to avoid removing on-demand shortcuts while the contacts snapshot is incomplete. Restrict stale-ID pruning to shortcuts owned by the publisher, and preserve shortcuts created by ensureConversationShortcut until the observed contacts flow includes their conversation; apply the same ownership and preservation logic to the cleanup path around the additional referenced block.
🧹 Nitpick comments (1)
core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt (1)
81-85: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winObserve node metadata changes as well as message recency.
buildDmShortcutderives labels, icons, and favorite priority fromnodeRepository.nodeDBbyNum.value, but this flow only combines contacts and channel configuration. UnlessgetContacts()emits for node renames/favorite changes, shortcuts remain stale. Add a node-database/version trigger and regression tests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt` around lines 81 - 85, Update the shortcut-publishing flow around rankConversationsByRecency to also collect a node-database or version trigger from nodeRepository, so buildDmShortcut-derived labels, icons, and favorite priority refresh when node metadata changes. Preserve the existing contacts and channel-set inputs, and add regression coverage for node rename and favorite-priority updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt`:
- Around line 156-160: Update the label fallback logic in
ConversationShortcutPublisher so shortLabel and longLabel use the existing
localized generic fallback when node metadata is missing, rather than
dm.contactKey. Keep contactKey restricted to internal identifiers and deep-link
metadata, and flag any leftover placeholders or logging that exposes it.
- Line 193: Replace the hardcoded fallback in the channelName assignment with
the localized channel-label resource from core:resources, loading it through the
project’s established nonblocking resource-access path and passing channel.index
as the format argument. Preserve channel.name when non-empty.
- Around line 137-140: Update publishShortcuts cleanup to avoid removing
on-demand shortcuts while the contacts snapshot is incomplete. Restrict stale-ID
pruning to shortcuts owned by the publisher, and preserve shortcuts created by
ensureConversationShortcut until the observed contacts flow includes their
conversation; apply the same ownership and preservation logic to the cleanup
path around the additional referenced block.
---
Nitpick comments:
In
`@core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt`:
- Around line 81-85: Update the shortcut-publishing flow around
rankConversationsByRecency to also collect a node-database or version trigger
from nodeRepository, so buildDmShortcut-derived labels, icons, and favorite
priority refresh when node metadata changes. Preserve the existing contacts and
channel-set inputs, and add regression coverage for node rename and
favorite-priority updates.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a163356b-a04f-41e2-b8e4-7c068a8ea60e
📒 Files selected for processing (5)
core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt
- core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt
- core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
- core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
…review - Never expose the raw contactKey as a user-facing label: DM shortcuts fall back to the localized unknown-username string, and the post-reply refresh uses localized fallbacks too. contactKey stays confined to internal ids and deep-link metadata (privacy-first convention). - Drop the dead "Channel N" fallback: Channel.name resolves an empty settings.name to the modem-preset display name (or "Custom"), so the effective name is never empty. - Protect on-demand shortcuts from the observer's pruning until a contacts snapshot includes their conversation, so a brand-new conversation's first notification cannot have its shortcut link severed by an in-flight publish. Adds regression tests for the pruning protection and reseeds the stale-removal test via a raw push (the in-memory protection intentionally doesn't survive a process restart, so the observer still owns cross-session cleanup). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Why
A review of
MeshNotificationManagerImplagainst Android's messaging-notification guidance surfaced three gaps:MessagingStyleis designed to accumulate, and it has a first-class mechanism for context that shouldn't re-alert — we weren't using it.contactKey.hashCode(),name.hashCode(), and rawnode.num, alongside fixedSUMMARY_ID = 1/SERVICE_NOTIFY_ID = 101. A node whosenum == 101would overwrite the foreground-service notification;num == 1would collide with the group summary; an alert and a message could collide via unrelated hashes.feature/carpublished long-lived conversation shortcuts (Person+LocusId+SHORTCUT_CATEGORY_CONVERSATION), but only during an Android Auto session, and no notification ever setsetShortcutId/setLocusId— so DMs and channels never received the Conversations-section treatment (Android 11+), and theensureConversationShortcuthelper was dead code. The two halves were built and never connected.What
🌟 Conversations-section support on phones
Conversation-shortcut publishing moves out of
feature/carinto a newConversationShortcutPublisherincore:service, owned by theMeshServicelifecycle (startObservinginonCreate,stopObservinginonDestroy) so shortcuts exist whenever the service runs — not only in Android Auto. Conversation notifications nowensureConversationShortcut(...)then setsetShortcutId+setLocusId, so Android ranks them in the shade's Conversations section and surfaces them to Auto/Wear. The car-onlyConversationShortcutManageris retired (Auto keeps working — the same shortcuts are now published for the whole service lifetime). The shortcut deep-link is aligned to the canonical$DEEP_LINK_BASE_URI/messages/...the notification tap already uses.🛠️ Read messages become historic context, not new content
Already-read messages are added via
MessagingStyle.addHistoricMessage()instead ofaddMessage(), so they provide conversation context to accessibility services and Auto/Wear without being re-presented as freshly-arrived. Unread messages — and reactions, which are themselves new events — remain the alerting content. A guard keeps the most-recent message alerting when everything in the window is already read (e.g. a reaction on a read message).🐛 Per-type notification ID namespacing
Every type now posts through
notify(tag, id, …)/cancel(tag, id), eliminating the cross-type ID collisions above. The group summary is excluded from the active-conversation count by itsFLAG_GROUP_SUMMARYflag rather than a fragileid != SUMMARY_IDcheck.🐛 Orphaned group summary after mark-as-read / reply
Found during on-emulator testing: after the last conversation was dismissed via mark-as-read or reply, an empty group summary lingered in the shade (and Android Auto).
NotificationManager.activeNotificationsdoesn't reflect acancel()issued moments earlier, soshowGroupSummary()rebuilt the summary from stale state instead of dropping it — defeating the existingactiveNotifications.isEmpty()guard.cancelMessageNotificationnow passes the just-cancelled id toshowGroupSummary, which excludes it from the stale snapshot so the summary is correctly cleared.Device-switch state reset — verified, no code change needed
switchDevice()clears notifications (cancelAll); conversation shortcuts reset through the publisher's existing flows: DM shortcuts via thecurrentDb-reactive contacts flow, and channel shortcuts viachannelSetFlow+ the publisher's stale-shortcut removal. Both were confirmed on-emulator (see Testing). An earlier draft added an identity-keyed wipe for this; testing showed it redundant (the existing stale-removal already resets channels when a device connects) so it was dropped.🌟 Styled conversation shortcuts (channels vs DMs)
Previously every conversation shortcut had
icon=null, so launchers / Android Auto / the People space rendered them all as identical generic head silhouettes with only a text label. Now each shortcut carries a rendered icon that distinguishes the two conversation types at a glance:shortLabel= short name,longLabel= long name.LongFast) to match the in-app conversation list rather than a placeholder.The icon is set on the shortcut itself (not just the
Person), which is what fixes the generic-head rendering.PersonIconFactorygained a shape-aware, auto-sizing label renderer shared by both paths (and the on-demandensureConversationShortcut, so there's no generic-head flash before the observer republishes).In-notification sender avatars (the
MessagingStylePersonicons) also now hold each participant's full short name — e.g.WOLF,HAWK,2c3d— rather than just its first character, and use the same wide-pill shape, so senders are identifiable at a glance in a busy channel conversation.Shortcuts are ranked by recency: DMs and channels are merged and sorted by last-activity time (from the message DB), and
setRankis applied so the most recently active conversations occupy the low ranks. Launchers / Android Auto show only a small capped number of dynamic shortcuts, so without this the surfaced set was an arbitrary slice of stale channels; now it's the conversations the user actually touched most recently.🛠️ Modern-API review fixes
A holistic pass against current notification guidance (Android 12–16 behaviors) landed five more refinements:
refreshConversationAfterReply, new interface method with a dismiss fallback default), instead of the notification vanishing. This is the MessagingStyle confirmation pattern and resolves the RemoteInput spinner with visible feedback; on Android Auto the conversation shows the reply rather than disappearing. A failed send still dismisses so the spinner never hangs.GROUP_ALERT_CHILDREN; previously it was posted on a HIGH-importance channel with default alert behavior, so it could buzz a second time for every message.FOREGROUND_SERVICE_IMMEDIATE— Android 12+ can defer FGS notifications ~10s; the connection-state notification now shows immediately (the "Connecting…" window is exactly when the user is watching).Person.setImportantfor favorites — favorite nodes feed the system's conversation-priority ranking (shade ordering, suggestions), on both notification senders and DM shortcut persons.#67EA94, per design standards) instead of rawColor.BLUE.Conversations surface — scope note
Message notifications now get the full Conversations treatment on Android 11+: long-lived
SHORTCUT_CATEGORY_CONVERSATIONshortcuts +setShortcutId/setLocusId, verifiedfound valid? truefor both DMs (my_messages) and broadcasts (my_broadcasts). Bubbles / chat-heads are intentionally not enabled here — they need an embeddable bubble Activity plusBubbleMetadataand per-channelsetAllowBubbles, which is a separate feature. This PR lays the required shortcut/LocusId foundation that bubbles build on; enabling them can follow.🧹 Avatar caching + dedupe
Generated avatars are cached per node instead of re-rasterizing a 128px bitmap for every message on every rebuild.
PersonIconFactorymoves tocore:serviceand now backs both the shortcut publisher and the notification avatars, removing the duplicate that lived infeature/car.Testing performed
./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests— full baseline, both flavors.:androidApp:testFdroidDebugUnitTest --tests "*KoinVerificationTest*"— verifies the newConversationShortcutPublisherbinding andMeshNotificationManagerImpl's added dependencies resolve across the DI graph.androidHostTest):MeshNotificationManagerImplConversationTest— read messages become historic / unread stay alerting; per-type (tag, id) namespacing (anum == 101node no longer clobbers the FGS notification); summary usesGROUP_ALERT_CHILDRENand is dropped with the last conversation;refreshConversationAfterReplyre-posts with the effective channel name (broadcast) and without a conversation title (DM); brand accent color.ConversationShortcutPublisherTest— recency ranking across DMs and channels (setRank), DM short/long labels, empty-primary → preset name ("LongFast"), stale-shortcut pruning.ReplyReceiverTestupdated: a successful reply refreshes the conversation in place (and never cancels); a failed send still dismisses.MeshNotificationManagerImplTestfor the new constructor parameters.Live E2E on emulator (fdroid debug) driven by meshtastic-mcp replay
Connected the app (TCP) to a simulated 800-node mesh and injected messages, inspecting
dumpsys notification:shortcut=0^all found valid? trueonmy_broadcasts; a direct message reportsshortcut=0!<hex> found valid? truewithlocusIdonmy_messages. Both DM and channel conversation shortcuts are published (SHORTCUT_CATEGORY_CONVERSATION) and accepted by the system.message(id 47226035),message_summary(id 1), and the foreground service (id 101, untagged) coexist as distinct records; none clobbers another.android.messages=Parcelable[] (1)(the new message) andandroid.messages.historic=Parcelable[] (2)(the read context), withandroid.text= the new message. Read history is context, not new content.Published 0 conversation shortcutsthen repopulated (DM reset via the contacts flow); reconnecting to a different mesh scenario flipped the channel shortcuts from one channel set to the other (MeshCon/Talks/Swap/Hax/Staff→Everyone/BRC/Playa/Rangers) viachannelSetFlow+ stale-removal, with stale channels dropped. Notifications cleared viacancelAll.Remaining (non-blocking): worth a real-device eyeball for the shade's Conversations-section presentation. Note the replay tooling presents a single fixed local node identity regardless of seed/scenario, so a true two-identity device switch (vs. the address/DB and channel-set switches exercised here) still wants a two-radio check.
Summary by CodeRabbit