Skip to content

feat(car): Android Car App Library integration#5633

Draft
jamesarich wants to merge 28 commits into
mainfrom
feature/20260521-153452-car-app-library-integration
Draft

feat(car): Android Car App Library integration#5633
jamesarich wants to merge 28 commits into
mainfrom
feature/20260521-153452-car-app-library-integration

Conversation

@jamesarich
Copy link
Copy Markdown
Collaborator

Summary

Full Android Car App Library integration for Meshtastic — delivering a distraction-optimized, safety-first mesh radio interface for Android Auto and AAOS.

Features

Messaging (ConversationItem API — official required pattern)

  • Native ConversationItem template with built-in TTS readout, voice reply, and mark-as-read
  • ConversationCallback handles reply via SendMessageUseCase (homoglyph encoding, message queuing, persistence)
  • Channel conversations + DM conversations ordered by recency
  • Group conversation detection for channels (3+ participants)

Node Dashboard

  • All mesh nodes with signal quality coloring (Excellent/Good/Fair/Bad/None)
  • Per-node colored icons via nodeColorsFromNum()
  • Battery percentage, last-heard time, signal indicators
  • Node detail drill-down

Device Status Tab

  • Battery level, channel utilization, air utilization
  • Online/total node count, uptime, TX/RX packet counts
  • Mirrors the home-screen Local Stats widget metrics

Emergency Alerts

  • Alert.Builder modal alerts (API 8+) with 10s duration
  • Emergency spotlight section in messaging list
  • Graceful fallback on API 7 hosts (notification-only)

Notification Integration

  • MessagingStyle notifications with proper SEMANTIC_ACTION_REPLY / SEMANTIC_ACTION_MARK_AS_READ
  • ConversationShortcutManager publishes dynamic shortcuts for favorites + channels
  • PersonIconFactory renders circular avatars with node-colored initials
  • LocusIdCompat links notifications ↔ template conversations
  • CarReplyReceiver handles inline reply and mark-as-read

Safety & Security

  • minCarApiLevel = 7 with graceful API 8 feature fallbacks
  • android:permission="androidx.car.app.CarAppService" on service
  • MessageFilter.validateOutgoing() enforces 237-byte Meshtastic packet limit
  • CoroutineExceptionHandler on all scopes to prevent silent failures
  • PII stripped from logs (char count only, not content)
  • Explicit CarReplyReceiver (android:exported=false) for notification actions

Architecture

  • Module: feature/car — zero modifications to existing core/feature modules
  • DI: Koin annotations (@Factory, @Single) with proper module
  • Data flow: Reactive StateFlow collection → debounced invalidate()
  • Testability: CarScreenDataBuilder — pure functions with 533-line unit test suite
  • Coordinator pattern: CarStateCoordinator bridges repositories to car UI state

Comparison with PR #5162

This implementation supersedes #5162 (feat(auto)) with:

  • ✅ Official ConversationItem API (required for MESSAGING category)
  • ✅ 3-tab layout (Messages / Nodes / Status) vs 3-tab (Status / Favorites / Messages)
  • ✅ Emergency alert system (Alert API + EmergencyHandler)
  • ✅ Signal quality coloring + node-colored icons
  • ConversationShortcutManager + PersonIconFactory
  • ✅ API 7 minimum with API 8 graceful fallbacks
  • ✅ Full spec/plan/tasks/checklists documentation (65/65 items)
  • ✅ 533-line unit test suite for business logic

Technical Details

  • Min Car API: 7 (ConversationItem)
  • CAL Version: 1.9.0-alpha01
  • Target: Android Auto (phone projection) + AAOS
  • Categories: MESSAGING (template + notification)
  • Templated messaging: Beta (internal/closed testing tracks)

Verification

spotlessCheck ✓ | detekt ✓ | compileFdroidDebugKotlin ✓ | testFdroidDebugUnitTest ✓

Files (22 Kotlin + resources)

Module structure
feature/car/
├── src/main/kotlin/org/meshtastic/feature/car/
│   ├── alerts/EmergencyHandler.kt
│   ├── di/FeatureCarModule.kt
│   ├── model/CarUiModels.kt
│   ├── panels/MeshStatusPanel.kt
│   ├── panels/MeshStatusSessionWiring.kt
│   ├── screens/
│   │   ├── ChannelChipBuilder.kt
│   │   ├── EmergencySpotlightBuilder.kt
│   │   ├── HomeScreen.kt
│   │   ├── MessagingScreen.kt
│   │   ├── NodeDashboardScreen.kt
│   │   └── NodeDetailScreen.kt
│   ├── service/
│   │   ├── CarNotificationManager.kt
│   │   ├── CarReplyReceiver.kt
│   │   ├── CarStateCoordinator.kt
│   │   ├── ConversationShortcutManager.kt
│   │   ├── MeshtasticCarAppService.kt
│   │   └── MeshtasticCarSession.kt
│   └── util/
│       ├── CarScreenDataBuilder.kt
│       ├── CrashlyticsCarTagger.kt
│       ├── FuzzyNodeNameResolver.kt
│       ├── MessageFilter.kt
│       ├── NodeSubtitleFormatter.kt
│       └── PersonIconFactory.kt
├── src/test/kotlin/.../util/
│   ├── CarScreenDataBuilderTest.kt (533 lines)
│   ├── FuzzyNodeNameResolverTest.kt
│   └── MessageFilterTest.kt
└── src/main/res/
    ├── drawable/ (icons)
    ├── values/strings.xml
    └── xml/automotive_app_desc.xml

@github-actions github-actions Bot added enhancement New feature or request repo Repository maintenance labels May 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

✅ Docs staleness check passed

This PR includes updates to docs/en/ alongside the source changes. Thank you!

jamesarich and others added 26 commits May 28, 2026 11:10
Complete SDD specification for Android Auto / AAOS integration:

- spec.md: 7 user stories, 22 FRs, 11 NFRs, 10 success criteria
- plan.md: Implementation plan with research decisions, data model, contracts
- tasks.md: 40 dependency-ordered tasks across 10 phases
- research.md: 10 technical decisions with alternatives considered
- contracts/: Service and manifest declaration contracts
- checklists/: 65-item implementation checklist
- quickstart.md: Developer setup and DHU testing guide

Key decisions:
- CAL 1.9.0-alpha01 with all 7 new components (alpha risk accepted)
- MESSAGING + POI categories (no NAVIGATION)
- PlaceListMapTemplate for node positions (6-item cap)
- CAL built-in voice input (AppFunctions handles system AI separately)
- Shared BLE connection (Application-scoped via Koin)
- Crashlytics car_session tagging for observability
- Google flavor only distribution
- No parked-mode differentiation (per official docs)
- Cross-platform parity audit vs Meshtastic-Apple CarPlay

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Map strategy (User Story 5, FR-009, T029) deferred to allow proper
evaluation of:
- NAVIGATION category (full MapWithContentTemplate, live tracking)
  vs POI category (PlaceListMapTemplate, 6-item cap, simpler)
- Google Maps SDK for AAOS availability timeline
- Play Store review implications of NAVIGATION declaration
- Whether convoy use case justifies nav app exclusivity

Remaining 39 tasks (Phases 1-6, 8-10) proceed without map dependency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clean up references to MapScreen, PlaceListMapTemplate, POI category,
and related models across all spec artifacts following the map strategy
deferral decision. All map-related items are now properly marked as
DEFERRED or N/A.

Affected artifacts:
- contracts/car-app-service.md: screen hierarchy, tabs, template section
- contracts/manifest-declarations.md: POI category removed
- plan.md: file tree cleaned
- spec.md: component table, assumptions, clarification Q2
- data-model.md: MapUiState/NodePlace/LatLngWrapper commented out
- research.md: R6 marked UNDER REVIEW with options table
- tasks.md: T005, T014, delivery strategy
- checklists/car-integration.md: CHK020/030/038/043 marked N/A

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CrashlyticsCarTagger: sets car_session custom key for crash reports
- TemplateBuilders: helper extensions for CAL template construction
- CarUiModels: presentation state models for car UI screens
- HomeScreen: TabTemplate root screen with Messages/Nodes tabs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add messaging screens, utilities, and notification support:
- MessagingScreen: conversation list with debounced invalidation
- ConversationScreen: message view with voice reply/read-aloud actions
- FuzzyNodeNameResolver: LCS-based voice name matching
- MessageFilter: emoji-only/admin filtering + 237-byte outgoing limit
- BatchMessageLoader: session-start unread message batching
- CarNotificationManager: MessagingStyle notifications with reply/mark-read

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pha01

Complete implementation of the Android Auto / AAOS car module:

Phase 1 - Setup:
- Version catalog entries (car-app 1.9.0-alpha01)
- Module build.gradle.kts with android-library, flavors, koin
- AndroidManifest with MESSAGING category, minCarApiLevel 8
- AAOS automotive_app_desc.xml
- Car-specific string resources
- ProGuard keep rules

Phase 2 - Foundation:
- MeshtasticCarAppService (CarAppService entry point)
- MeshtasticCarSession (session lifecycle, Crashlytics tagging)
- FeatureCarModule (Koin DI with ComponentScan)
- HomeScreen (TabTemplate: Messages + Nodes)
- CrashlyticsCarTagger, TemplateBuilders helpers
- CarUiModels (presentation state models)

Phase 3 - Messaging (MVP):
- MessagingScreen (300ms debounced invalidation, max 10 conversations)
- ConversationScreen (voice reply, read-aloud, max 5 messages)
- FuzzyNodeNameResolver (LCS-based voice name matching)
- MessageFilter (emoji/admin exclusion, 237-byte limit)
- BatchMessageLoader (50 unread on session start)
- CarNotificationManager (MessagingStyle + reply/mark-read)

Phase 4 - Emergency:
- EmergencyHandler (flow collection, alert state, audio tone)
- EmergencySpotlightBuilder (alert rows for messaging screen)
- EmergencySessionWiring (lifecycle attach/detach)

Phase 5 - Nodes:
- NodeDashboardScreen (sorted list, signal/battery, topology header)
- NodeDetailScreen (PaneTemplate with Message action)

Phase 6 - Channels:
- ChannelChipBuilder (ActionStrip with unread badges)

Phase 8 - Status Panel:
- MeshStatusPanel (connection, node count, last msg time)
- MeshStatusSessionWiring (Flow-based lifecycle)

Phase 9 - Voice:
- CarTtsEngine (TTS read-aloud)
- VoiceDmCoordinator (fuzzy resolve + voice DM flow)

Phase 10 - Polish:
- OnboardingScreen (no channels configured state)
- DisconnectedScreen (BLE disconnect graceful degradation)
- ProGuard consumer rules

Verified: spotlessApply ✓ detekt ✓ compileGoogleDebugKotlin ✓ assembleGoogleDebug ✓

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add CarStateCoordinator bridging NodeRepository, PacketRepository,
  ServiceRepository, RadioConfigRepository, QuickChatActionRepository
  into car-optimized StateFlows
- Wire coordinator into MeshtasticCarSession lifecycle (start/destroy)
- Update HomeScreen to render real messaging and node lists from state
- Add core:database dependency for QuickChatAction entity access
- Fix FlavorModule ktfmt/ktlint conflict with @file:Suppress

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HomeScreen now observes CarStateCoordinator flows and calls
invalidate() when messaging or node state updates, ensuring
the template refreshes with live data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
17 tests covering message filtering (text type, blank, emoji-only,
byte length validation) and fuzzy node name resolution (exact match,
case-insensitive, partial/typo matching, edge cases).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…connected state

- Wire real message loading via PacketRepository with send support
- Add TTS read-aloud with message caching (limit 3 messages)
- Add onboarding screen when no channels configured
- Add disconnected state handling with reconnection notice
- Wire EmergencyHandler with placeholder flow for future detection
- Suppress TooManyFunctions for coordinator (legitimate orchestrator)
- Remove unused messagesCache from HomeScreen (moved to coordinator)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…safety

Critical fixes:
- EmergencyHandler: recreate scope on startCollecting() (singleton was permanently dead after first session)
- MeshtasticCarAppService: gate ALLOW_ALL_HOSTS_VALIDATOR behind BuildConfig.DEBUG, add hosts_allowlist.xml for production

High fixes:
- MeshtasticCarSession: wire destroy() via DefaultLifecycleObserver on ON_DESTROY
- MeshStatusPanel: remove unused CoroutineScope (no coroutines launched)
- CarStateCoordinator: use MutableStateFlow for channel index, ConcurrentHashMap for messagesCache
- CarTtsEngine: wire shutdown() call into coordinator destroy()

Medium fixes:
- NodeDetailScreen: replace all hardcoded strings with R.string resources
- DisconnectedScreen: replace hardcoded strings with resources
- Add comprehensive string resources for time formatting and status labels

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l identity

- Add car-specific drawables: ic_car_meshtastic (green-tinted logo),
  ic_car_message, ic_car_nodes, ic_car_person, ic_car_warning
- Add tab icons (messages/nodes) for visual recognition in TabTemplate
- Add row images for conversation items (person) and node items (mesh)
- Add icon to disconnected template (warning) and onboarding (logo)
- Replace generic system notification icon with Meshtastic icon
- Localize all remaining hardcoded strings (signal quality, offline status)
- Remove redundant node sort in NodeDashboardScreen (already sorted by coordinator)
- Use car_nodes_online string resource for header title

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace ConnectionStatus enum with core:model/ConnectionState directly
- Fix signal quality thresholds: use core's LoRa-correct SNR (-7/-15)
  and RSSI (-115/-126) instead of wrong 10/5/0 thresholds
- Replace manual formatLastHeard() with DateFormatter.formatRelativeTime()
  from core:common (already a dependency)
- Replace MeshStatusPanel time formatting with DateFormatter
- Remove unused time format string resources (car_time_just_now, etc.)
- Handle DeviceSleep as disconnected state in car UI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename SignalQuality enum to match core: POOR→BAD, UNKNOWN→NONE
- Add last heard time to node list subtitles (was only in detail view)
- Add message timestamps in conversation view
- Align string resources with core terminology (bad/none vs poor/unknown)
- Use DateFormatter.formatRelativeTime() consistently across all screens

Ensures car experience uses consistent terminology and information
density with the main Meshtastic app.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use CarColor.GREEN tint on node icons for online status (visual diff)
- Use Row.IMAGE_TYPE_ICON explicitly per official showcase sample
- Use ConstraintManager for dynamic list limits (host-aware)
- Online nodes get green-tinted mesh icon, offline get default white

Patterns sourced from android/car-samples showcase app.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fake NodeChip in car UI using SpannableString with node-unique colors
(same RGB-from-nodeNum algorithm as main app's NodeChip composable).

Node titles now render as '[JA] Long Name' where [JA] is colored with
the node's unique color. Signal quality text is also color-coded:
green for Excellent/Good, yellow for Fair, red for Bad.

This matches the main app's visual identity where each node has a
recognizable colored chip derived from its node number.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Better placement: use CarIcon.setTint() with the node's unique color
instead of cluttering the title with spannable text chips. Each node
row now has its own uniquely-colored mesh icon — the Car App Library
equivalent of the main app's NodeChip composable.

- Extract nodeColorsFromNum() to core:model as a shared utility
- Node.colors now delegates to nodeColorsFromNum(num)
- Remove chipColor/chipTextColor from NodeUi (derivable from nodeNum)
- Keep colored signal text in subtitle via ForegroundCarColorSpan

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Security: Replace implicit broadcast intents with explicit CarReplyReceiver
  targeting to prevent interception/spoofing of reply PendingIntents
- Security: Add warning log for debug-only ALLOW_ALL_HOSTS_VALIDATOR
- D1 (HIGH): Wire EmergencySpotlightBuilder into MessagingScreen with
  SectionedItemList for active emergency alerts at top of message list
- C1 (MEDIUM): Wire ChannelChipBuilder into MessagingScreen as ActionStrip
  for channel switching when multiple channels available
- E2 (LOW): Add <uses name="notification" /> to automotive_app_desc.xml
  for notification-based messaging compliance
- Register CarReplyReceiver in AndroidManifest (android:exported=false)
- Add car_emergency_alerts string resource

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- T041: Add CarToast feedback for voice/quick reply sent and reconnection
- T043: Add Alert API for emergency notifications via EmergencyHandler
- T044: Add LongMessageTemplate when messages exceed list limit
- T045: Add CarText.addVariant() responsive text for node subtitles
- T046: Add ParkedOnlyOnClickListener for send actions in ConversationScreen
- T042: Add refresh() to CarStateCoordinator; ActionStrip refresh button on
  NodeDashboardScreen (OnContentRefreshListener unavailable on ListTemplate)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix coroutine leak in CarStateCoordinator.refresh() by tracking Jobs
- Add CoroutineExceptionHandler to CarStateCoordinator and EmergencyHandler
- Fix unreachable LongMessageTemplate (MAX_MESSAGES=20, MAX_LIST_MESSAGES=5)
- Fix premature toast on voice reply and empty string quick reply
- Add MessageFilter.validateOutgoing() call in sendMessage()
- Add logging to EmergencyHandler catch block and CarTtsEngine
- Delete dead code: DisconnectedScreen, OnboardingScreen, TemplateBuilders,
  VoiceDmCoordinator, BatchMessageLoader, EmergencySessionWiring
- Inline EmergencySessionWiring into MeshtasticCarSession
- Extract shared NodeSubtitleFormatter from HomeScreen/NodeDashboardScreen
- Track meshNameJob in MeshStatusSessionWiring to avoid leak
- Redact message content from CarReplyReceiver log

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All checklist items validated against spec, plan, and implementation:
- 61 items checked off with clarification notes
- 4 items marked N/A (map features deferred, distribution out of scope)
- Zero items remain unresolved

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Migrate to SendMessageUseCase (replaces raw commandSender.sendData())
- Migrate Messages tab to ConversationItem API (official required pattern)
- Add ConversationCallback for native reply + mark-as-read
- Add Status tab with device metrics (battery, utilization, uptime, TX/RX)
- Add ConversationShortcutManager + PersonIconFactory for notification linking
- Add CarScreenDataBuilder with pure testable functions (533-line test suite)
- Fix notification actions (SEMANTIC_ACTION_REPLY/MARK_AS_READ, setShowsUserInterface)
- Wire CarReplyReceiver to actually send messages and clear unreads
- Lower minCarApiLevel from 8 to 7 with graceful API 8 fallbacks
- Add android:permission on CarAppService for security
- Remove dead code: ConversationScreen, CarTtsEngine, messagesCache

Files added: ConversationShortcutManager, PersonIconFactory, CarScreenDataBuilder,
  ic_car_status.xml, CarScreenDataBuilderTest (533 lines)
Files removed: ConversationScreen.kt, CarTtsEngine.kt

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delete 6 dead files: MessagingScreen, ChannelChipBuilder, EmergencySpotlightBuilder, NodeDashboardScreen, MeshStatusPanel, MeshStatusSessionWiring

- Fix CarReplyReceiver: use goAsync() to prevent scope leak

- Skip conversations with empty messages in ConversationItem builder

- Remove no-op Message Node button from NodeDetailScreen

- Remove unused constants and dead resolveNode function

- Clean up 14 unused string resources

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ode button

- ConversationShortcutManager now observes PacketRepository.getContacts()
  to publish shortcuts for actual DM conversations (not favorites)
- Re-add Message Node button on NodeDetailScreen, properly wired:
  navigates back to Messages tab and ensures the DM conversation exists
- Add userId field to NodeUi for contactKey construction
- Channels are still published as shortcuts alongside DMs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jamesarich and others added 2 commits May 28, 2026 11:11
- Remove QuickChatActionRepository dependency and unused quickChatActions
  StateFlow from CarStateCoordinator (never consumed by UI)
- Extract hardcoded "Tap to send a message" to car_new_conversation
  string resource for i18n compliance
- Remove empty companion object

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich force-pushed the feature/20260521-153452-car-app-library-integration branch from 97ff557 to c6a26fa Compare May 28, 2026 16:16
@jamesarich jamesarich added this to the 2.8.0 milestone May 30, 2026
jamesarich added a commit that referenced this pull request May 30, 2026
Squash merge of PR #5633 into release/2.8.0.

Full Android Car App Library integration — delivering a distraction-optimized,
safety-first mesh radio interface for Android Auto and AAOS. Includes native
ConversationItem messaging, node list with signal metrics, map screen with
mesh node markers, and settings access.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich
Copy link
Copy Markdown
Collaborator Author

🔗 Release 2.8.0 Integration Report

Branch: release/2.8.0 | Status: ✅ Demo verified on emulator

Integration Changes Required

  1. HomeScreen.kt — After AIDL removal (refactor: Remove AIDL API and modernize service architecture #5586), ServiceRepository.connectionState changed from a direct property to a StateFlow. Updated Car App HomeScreen to collect from the flow instead of reading a snapshot value.

  2. ConversationShortcutManager.ktPacketRepository API changed: getContactList() return type evolved from List<Contact> to Flow<List<Contact>> (now reactive). Updated to .first() for one-shot usage in car shortcuts.

  3. CarScreenDataBuilder.ktNodeEntity field access changed after discovery PR (feat(discovery): mesh network discovery #5275) added new fields. Builder needed adjustment for the expanded node model.

  4. FlavorModule.kt — Car module DI registration merged alongside App Functions module registration in the shared Google flavor module.

Merge Guidance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request repo Repository maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant