feat(specs): Car App Library 1.9.0-alpha01 integration specification

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>
This commit is contained in:
James Rich
2026-05-21 16:54:57 -05:00
parent 97ce3cd27f
commit 8f910d692f
12 changed files with 1918 additions and 2 deletions

View File

@@ -54,6 +54,7 @@ KMP modules have different task names than pure-Android modules. Using the wrong
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
shell commands, and other important information, read the current plan at
specs/20260521-153452-car-app-library-integration/plan.md
<!-- SPECKIT END -->

View File

@@ -1 +1,3 @@
{"feature_directory":"specs/20260520-153412-nav-tab-labels"}
{
"feature_directory": "specs/20260521-153452-car-app-library-integration"
}

View File

@@ -0,0 +1,107 @@
# Car App Library Integration Checklist: Car App Library Integration
**Purpose**: Validate requirements quality, completeness, and clarity for the Car App Library 1.9.0-alpha01 integration — covering automotive safety, component usage, connectivity, distribution, and testability
**Created**: 2026-05-21
**Feature**: [spec.md](../spec.md)
## Requirement Completeness
- [ ] CHK001 — Are CarAppService lifecycle requirements specified (onCreateSession, onDestroy, multi-session behavior)? [Completeness, Gap]
- [ ] CHK002 — Are requirements defined for all 7 new 1.9.0-alpha01 components (Spotlight, Condensed Items, Chips, Minimized Control Panel, Banners, Section Headers, Expanded Headers)? [Completeness, Spec §FR-002FR-013]
- [ ] CHK003 — Are Screen navigation graph requirements documented (which screens link to which, back-stack behavior)? [Completeness, Gap]
- [ ] CHK004 — Are ConversationItem requirements specified (message grouping, read/unread state, sender avatar rendering)? [Completeness, Spec §FR-002]
- [ ] CHK005 — Are TTS readback requirements defined with language, speed, and fallback behavior? [Completeness, Spec §US-7]
- [ ] CHK006 — Are quick-reply template storage and configuration requirements specified? [Completeness, Spec §FR-004]
- [ ] CHK007 — Are Koin module registration requirements documented for the `feature/car` DI graph? [Completeness, Gap]
- [ ] CHK008 — Are requirements defined for the google-flavor-only build gate (how other flavors exclude the car module)? [Completeness, Gap]
- [ ] CHK009 — Are requirements specified for CarAppService `onNewIntent` handling and deep-link entry points? [Completeness, Gap]
- [ ] CHK010 — Are node detail view content requirements exhaustively enumerated (last heard, distance, hardware model, firmware version, hops)? [Completeness, Spec §FR-012]
## Requirement Clarity
- [ ] CHK011 — Is "within 3 seconds" latency (FR-002/NFR-002) measured from radio receipt, BLE delivery, or repository emission? [Clarity, Spec §NFR-002]
- [ ] CHK012 — Is "high-priority banner" (FR-005) defined with specific CAL Banner priority level and duration? [Clarity, Spec §FR-005]
- [ ] CHK013 — Is "signal quality indicator" quantified — specific icon set, numeric dBm ranges, or named levels (excellent/good/fair/poor)? [Clarity, Spec §FR-007]
- [ ] CHK014 — Is "< 10% battery drain" measured under defined conditions (screen brightness, BLE activity, message frequency)? [Clarity, Spec §NFR-003]
- [ ] CHK015 — Is "visually distinguished" for offline nodes defined with specific styling (opacity, icon, sort order)? [Clarity, Spec §US-3 Scenario 3]
- [ ] CHK016 — Is "distinct color treatment" for emergency banners specified with concrete color values or semantic tokens? [Clarity, Spec §US-2 Scenario 1]
- [ ] CHK017 — Is "6+ nodes visible simultaneously without scrolling" dependent on a specific screen density or display size? [Clarity, Spec §SC-003]
- [ ] CHK018 — Is "configurable template responses" clear on who configures them, where they're stored, and defaults? [Clarity, Spec §FR-004]
- [ ] CHK019 — Is Car API Level 8 minimum clearly justified — which specific 1.9.0 APIs require it? [Clarity, Spec §NFR-004]
## Requirement Consistency
- [ ] CHK020 — Do FR-009 (PlaceListMapTemplate under POI) and Non-Goals (NAVIGATION deferred to v2) consistently align with map update latency requirement SC-009 (< 5s)? [Consistency, Spec §FR-009, §SC-009]
- [ ] CHK021 — Are voice input requirements consistent between US-1 (reply), US-7 (in-context), and FR-003 (primary method)? [Consistency]
- [ ] CHK022 — Does "no parked-mode differentiation" (Clarifications) conflict with any acceptance scenario implying driving-only behavior? [Consistency, Spec §Edge Cases]
- [ ] CHK023 — Are emergency handling requirements consistent between FR-005 (banner), FR-006 (spotlight), and US-2 (all scenarios)? [Consistency]
- [ ] CHK024 — Is the shared BLE connection assumption consistent with CarAppService lifecycle — what happens when phone app is force-stopped? [Consistency, Spec §Assumptions]
- [ ] CHK025 — Are "within 1 second" requirements (FR-005 emergency, SC-006 channel switch) measured consistently with NFR-002's 3-second messaging latency? [Consistency]
## Acceptance Criteria Quality
- [ ] CHK026 — Is SC-008 ("95% voice replies succeed") measurable without defining what "success" means (sent vs. accurately transcribed vs. delivered)? [Measurability, Spec §SC-008]
- [ ] CHK027 — Is SC-010 ("zero crashes in 2-hour session") sufficient as a release gate — what about ANRs, OOM, or session drops? [Measurability, Spec §SC-010]
- [ ] CHK028 — Is SC-007 ("passes Android Auto App Quality review") measurable before actual store submission? [Measurability, Spec §SC-007]
- [ ] CHK029 — Is SC-001 ("15 seconds total interaction time") measured from notification appearance or screen wake? [Measurability, Spec §SC-001]
- [ ] CHK030 — Are acceptance scenarios for US-5 (map) testable on DHU given DHU's limited map rendering capabilities? [Measurability, Spec §US-5]
## Scenario Coverage
- [ ] CHK031 — Are requirements defined for initial onboarding flow when no radio is paired? [Coverage, Gap]
- [ ] CHK032 — Are requirements specified for behavior when Android Auto host disconnects mid-session (cable pull, Bluetooth drop)? [Coverage, Gap]
- [ ] CHK033 — Are requirements defined for multi-device scenario (phone switches between two radios)? [Coverage, Gap]
- [ ] CHK034 — Are requirements specified for app behavior during phone call interruption on the head unit? [Coverage, Gap]
- [ ] CHK035 — Are requirements defined for Screen refresh/invalidation cadence (how often templates re-render)? [Coverage, Gap]
- [ ] CHK036 — Are data freshness requirements defined for cached messages shown during disconnection? [Coverage, Spec §FR-015]
- [ ] CHK037 — Are requirements specified for ConversationItem threading — flat list or grouped by conversation? [Coverage, Spec §FR-002]
## Edge Case Coverage
- [ ] CHK038 — Is behavior defined when PlaceListMapTemplate's item limit is reached (max 6 items per CAL docs)? [Edge Case, Spec §FR-009]
- [ ] CHK039 — Is behavior defined when a channel has zero messages (empty state for messaging screen per channel)? [Edge Case, Gap]
- [ ] CHK040 — Are requirements defined for handling very long node names that exceed Condensed Item text bounds? [Edge Case, Spec §FR-007]
- [ ] CHK041 — Is behavior defined when voice recognition returns empty/null result or times out? [Edge Case, Spec §FR-003]
- [ ] CHK042 — Is behavior defined for rapid consecutive emergency alerts from multiple nodes? [Edge Case, Spec §Edge Cases]
- [ ] CHK043 — Are requirements defined for handling GPS-less nodes on the map screen (nodes without position data)? [Edge Case, Spec §FR-009]
- [ ] CHK044 — Is behavior defined when the message being composed via voice exceeds mesh packet size limit (228 bytes)? [Edge Case, Gap]
- [ ] CHK045 — Is behavior defined when Minimized Control Panel data sources become stale (BLE connected but no mesh traffic)? [Edge Case, Spec §FR-010]
## Non-Functional Requirements
- [ ] CHK046 — Are memory usage requirements specified for the car module (AAOS devices may have constrained RAM)? [NFR, Gap]
- [ ] CHK047 — Are cold-start performance requirements defined for CarAppService (time from launch to first screen rendered)? [NFR, Gap]
- [ ] CHK048 — Are requirements specified for Crashlytics `car_session` key format, lifecycle (set/clear), and what constitutes a "session"? [NFR, Spec §NFR-009]
- [ ] CHK049 — Are ProGuard/R8 keep rules requirements documented for the car module (CAL uses reflection for template inflation)? [NFR, Gap]
- [ ] CHK050 — Are requirements defined for handling Android Auto's 10-second ANR threshold on the main thread? [NFR, Gap]
- [ ] CHK051 — Is backward-compatibility behavior specified for hosts below Car API Level 8 (graceful absence vs. crash vs. fallback)? [NFR, Spec §NFR-004, §Assumptions]
- [ ] CHK052 — Are requirements specified for process priority / foreground service behavior to keep BLE alive when phone app is backgrounded? [NFR, Gap]
## Dependencies & Assumptions
- [ ] CHK053 — Is the alpha stability risk (1.9.0-alpha01) quantified with a fallback plan if APIs change before stable release? [Assumption, Spec §Assumptions]
- [ ] CHK054 — Is the assumption "users configure channels on phone first" validated — what if a user only has AAOS with no phone? [Assumption, Spec §Assumptions]
- [ ] CHK055 — Are Play Store review requirements for MESSAGING category documented (conversation API compliance, notification delegation)? [Dependency, Gap]
- [ ] CHK056 — Is the relationship with AppFunctions feature clearly bounded — are there shared components or only independent parallel features? [Dependency, Spec §Clarifications]
- [ ] CHK057 — Are DHU and Automotive Emulator API 35-ext15 testing environment requirements documented as a verification prerequisite? [Dependency, Gap]
- [ ] CHK058 — Is the Koin Application-scoped BleConnectionManager's threading model documented (which dispatcher, coroutine scope)? [Assumption, Spec §Architecture]
## Distribution & Build Integration
- [ ] CHK059 — Are Play Store listing requirements specified for the car app (screenshots, description, category metadata)? [Completeness, Gap]
- [ ] CHK060 — Are internal/closed testing track progression criteria defined (when to promote from internal → closed → open → production)? [Completeness, Gap]
- [ ] CHK061 — Is the manifest merger strategy documented for adding `<meta-data>` and `<service>` entries only in the google flavor? [Completeness, Gap]
- [ ] CHK062 — Are automotive-specific permission requirements documented (e.g., `androidx.car.app.ACCESS_SURFACE`)? [Completeness, Gap]
## Cross-Artifact Consistency
- [ ] CHK063 — Do architecture component names in spec match planned module/package structure in plan.md? [Consistency]
- [ ] CHK064 — Are all 7 user stories reflected as distinct implementation tasks in tasks.md? [Consistency]
- [ ] CHK065 — Do NFR metrics (latency, battery, build time) have corresponding verification methods defined? [Traceability]
## Notes
- Check items off as they are resolved (requirement clarified, gap filled, or explicitly marked N/A)
- Items marked [Gap] indicate missing requirements that should be added to spec.md
- Items marked [Assumption] should be validated or converted to explicit requirements
- 80%+ items include traceability references to spec sections or gap markers

View File

@@ -0,0 +1,36 @@
# Specification Quality Checklist: Car App Library Integration
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-05-21
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
- Architecture section references module paths and component names for planning context — these describe *what* exists, not *how* to implement.
- Alpha library risk explicitly acknowledged in Assumptions section per user directive.

View File

@@ -0,0 +1,230 @@
# Car App Service Contract
**Feature**: Car App Library Integration
**Date**: 2026-05-21
## Service Declaration
The `MeshtasticCarAppService` is the entry point for Android Auto and AAOS hosts.
### AndroidManifest.xml Contract
```xml
<service
android:name="org.meshtastic.feature.car.service.MeshtasticCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.MESSAGING" />
<category android:name="androidx.car.app.category.POI" />
</intent-filter>
</service>
```
### Categories
| Category | Purpose | Justification |
|----------|---------|---------------|
| `MESSAGING` | Primary — enables ConversationItem, voice reply | Core use case: read/reply to mesh messages |
| `POI` | Secondary — enables PlaceListMapTemplate | Node map with static pins (not navigation) |
### Car API Level
```xml
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="8" />
```
Car API Level 8 is required for:
- Spotlight Sections
- Condensed Items
- Minimized Control Panel
- Banners
- Chips
- Section Headers
- Expanded Header Layout
Hosts below API Level 8 will not display the app (graceful absence).
## Session Contract
### MeshtasticCarSession
```kotlin
class MeshtasticCarSession(private val sessionInfo: SessionInfo) : Session() {
override fun onCreateScreen(intent: Intent): Screen
// Returns: HomeScreen (tab-based root)
// Side effects:
// - Sets Crashlytics "car_session" custom key
// - Starts collecting emergency message flow
// - Registers MeshStatusPanel
override fun onNewIntent(intent: Intent)
// Handles deep links (e.g., open specific conversation from notification)
override fun onCarConfigurationChanged(newConfiguration: Configuration)
// Handles theme/density changes (dark mode, etc.)
}
```
### Screen Stack Contract
```
HomeScreen (root, never popped)
├── MessagingScreen (tab 1)
│ └── ConversationScreen (push on conversation tap)
├── NodeDashboardScreen (tab 2)
│ └── NodeDetailScreen (push on node tap)
└── MapScreen (tab 3)
└── NodeDetailScreen (push on map item tap)
```
Maximum screen depth: 3 (compliant with CAL template depth limits).
## Template Contracts
### HomeScreen → TabTemplate (proposed, falls back to ListTemplate if tabs unavailable)
```
TabTemplate {
tabs: [
Tab("Messages", messagingIcon),
Tab("Nodes", nodeIcon),
Tab("Map", mapIcon),
]
headerAction: Action.APP_ICON
}
```
### MessagingScreen → ListTemplate with Chips + Spotlight Section
```
ListTemplate {
header: Header {
title: "Messages"
chipActions: [ChannelChip(name, unreadBadge) for each channel]
}
spotlightSection: SpotlightSection { // Only if activeEmergencies.isNotEmpty()
items: [emergencyConversationItems...]
}
sections: [
SectionHeader("Channel: {name}"),
ConversationItem(name, lastMessage, time, unread) for each conversation
]
}
```
### ConversationScreen → MessageTemplate / ListTemplate
```
MessageTemplate {
// For the selected conversation
messages: [MessageItem(text, sender, time) ...]
actions: [
Action("Reply", voiceIcon) → triggers CAL voice input
Action("Quick Reply", listIcon) → shows quick-reply list
Action("Read Aloud", speakerIcon) → triggers TTS
]
}
```
### NodeDashboardScreen → ListTemplate with Expanded Header + Condensed Items
```
ListTemplate {
header: ExpandedHeader {
title: "Mesh Network"
subtitle: "{onlineNodes}/{totalNodes} nodes online"
image: meshTopologyIcon
}
items: [
CondensedItem(
title: node.longName,
subtitle: "Signal: {quality} • Battery: {percent}%",
image: signalIcon(quality),
onClickListener: → push NodeDetailScreen
) for each node, sorted online-first
]
}
```
### NodeDetailScreen → PaneTemplate
```
PaneTemplate {
title: node.longName
pane: Pane {
rows: [
Row("Last Heard", formatTimeAgo(node.lastHeard)),
Row("Distance", formatDistance(distanceMeters)),
Row("Hardware", node.hwModel.name),
Row("Battery", "${node.batteryPercent}%"),
Row("Signal", formatSnr(node.snr)),
]
actions: [
Action("Message", messageIcon) → push ConversationScreen for DM
]
}
}
```
### MapScreen → PlaceListMapTemplate
```
PlaceListMapTemplate {
title: "Node Map"
itemList: ItemList {
items: [
Row(
title: node.name,
text: "Updated {timeAgo} • {distanceFormatted}",
metadata: Place(LatLng(lat, lng)),
onClickListener: → push NodeDetailScreen
) for each node with position
]
}
anchor: LatLng(ownLat, ownLng) // if own position available
isCurrentLocationEnabled: true
}
```
### MeshStatusPanel → Minimized Control Panel
```
// Attached to Session, visible across all screens
MinimizedControlPanel {
icon: connectionStatusIcon
title: "{onlineNodeCount} nodes online"
subtitle: "Last msg: {timeAgo}"
onClickListener: → expand to full detail panel
}
```
### Emergency Banner
```
// Triggered by EmergencyHandler when emergency packet received
AppManager.showAlert(
Alert {
title: "⚠️ EMERGENCY"
subtitle: "{senderName}: {messagePreview}"
icon: emergencyIcon
actions: [Action("View", → push emergency detail)]
duration: Alert.DURATION_LONG
}
)
```
## Error Contracts
| Condition | Behavior |
|-----------|----------|
| BLE disconnected | Banner shown; screens degrade to cached data (read-only) |
| No channels configured | Show onboarding PaneTemplate directing to phone app |
| No nodes in range | Empty state in NodeDashboard: "No nodes heard" |
| No positions available | MapScreen shows empty map with "No positions reported" |
| Template item limit exceeded | Paginate with "Load more" action row |
| Voice input fails | Fall back to quick-reply template list |
| Session crash | Crashlytics captures with `car_session` tag; session restarts cleanly |

View File

@@ -0,0 +1,133 @@
# Manifest Declarations Contract
**Feature**: Car App Library Integration
**Date**: 2026-05-21
## feature/car/src/main/AndroidManifest.xml
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Car App Library service declaration -->
<application>
<service
android:name="org.meshtastic.feature.car.service.MeshtasticCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.MESSAGING" />
<category android:name="androidx.car.app.category.POI" />
</intent-filter>
</service>
<!-- Minimum Car API Level for 1.9.0-alpha01 components -->
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="8" />
</application>
</manifest>
```
## AAOS Support: automotive_app_desc.xml
Located at `feature/car/src/main/res/xml/automotive_app_desc.xml`:
```xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="template" />
</automotiveApp>
```
## androidApp Manifest Additions (google flavor only)
In `androidApp/src/google/AndroidManifest.xml` (or merged automatically via manifest merger):
```xml
<!-- No additional declarations needed — the feature/car manifest merges automatically
when the module is included as a dependency in the google flavor -->
```
## Gradle Dependency Declaration
In `androidApp/build.gradle.kts`:
```kotlin
dependencies {
// Car module (google flavor only - CAL requires Play Services)
"googleImplementation"(projects.feature.car)
}
```
In `settings.gradle.kts` (new include):
```kotlin
include(":feature:car")
```
## Version Catalog Additions (gradle/libs.versions.toml)
```toml
[versions]
car-app = "1.9.0-alpha01"
[libraries]
androidx-car-app = { module = "androidx.car.app:app", version.ref = "car-app" }
androidx-car-app-projected = { module = "androidx.car.app:app-projected", version.ref = "car-app" }
androidx-car-app-automotive = { module = "androidx.car.app:app-automotive", version.ref = "car-app" }
androidx-car-app-testing = { module = "androidx.car.app:app-testing", version.ref = "car-app" }
```
## feature/car/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.flavors)
alias(libs.plugins.meshtastic.koin)
}
android {
namespace = "org.meshtastic.feature.car"
defaultConfig {
minSdk = 23 // Android Auto projection minimum
}
}
dependencies {
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.repository)
implementation(projects.core.ble)
implementation(libs.androidx.car.app)
implementation(libs.androidx.car.app.projected)
implementation(libs.koin.android)
implementation(libs.koin.annotations)
implementation(libs.firebase.crashlytics)
testImplementation(libs.androidx.car.app.testing)
testImplementation(libs.koin.test)
testImplementation(kotlin("test"))
}
```
## Permissions
No additional permissions required. The car module:
- Does NOT request `BLUETOOTH` permissions (handled by `core/ble` at the app level)
- Does NOT request location permissions (handled by existing app permissions)
- Does NOT request microphone permissions (CAL voice input is delegated to the system)
## ProGuard / R8 Rules
```proguard
# Car App Library service must not be obfuscated (resolved by exported service)
-keep class org.meshtastic.feature.car.service.MeshtasticCarAppService { *; }
```

View File

@@ -0,0 +1,226 @@
# Data Model: Car App Library Integration
**Feature**: Car App Library Integration
**Date**: 2026-05-21
## Overview
The car module introduces **no new persistent entities**. All data is consumed from existing `core/` repositories. This document defines the **presentation state models** and **UI state containers** used within the car module to bridge repository data to CAL templates.
## Existing Entities (consumed, not modified)
### Node (core/model)
| Field | Type | Car Usage |
|-------|------|-----------|
| `num` | `Int` | Unique identifier, key for node DB |
| `user.id` | `String` | User ID (e.g., "!1234abcd") |
| `user.longName` | `String` | Display name in Condensed Items |
| `user.shortName` | `String` | Abbreviated name for compact views |
| `user.hwModel` | `HardwareModel` | Shown in node detail |
| `position.latitude` | `Double` | Map pin latitude |
| `position.longitude` | `Double` | Map pin longitude |
| `position.time` | `Int` | Last position update epoch |
| `lastHeard` | `Int` | Last communication epoch |
| `snr` | `Float` | Signal-to-noise ratio display |
| `deviceMetrics.batteryLevel` | `Int?` | Battery indicator |
| `isFavorite` | `Boolean` | Priority in node list |
### DataPacket (core/model)
| Field | Type | Car Usage |
|-------|------|-----------|
| `from` | `String` | Sender identifier |
| `to` | `String` | Destination identifier |
| `channel` | `Int` | Channel index for grouping |
| `bytes` | `ByteArray?` | Message content |
| `dataType` | `Int` | Message type classification |
| `time` | `Long` | Timestamp for display |
| `id` | `Int` | Unique packet ID |
| `status` | `MessageStatus` | Delivery status indicator |
### QuickChatAction (core/database)
| Field | Type | Car Usage |
|-------|------|-----------|
| `uuid` | `Long` | Unique ID |
| `name` | `String` | Display label for quick-reply button |
| `message` | `String` | Text to send when tapped |
| `mode` | `Int` | Instant vs append mode |
| `position` | `Int` | Sort order |
### MyNodeInfo (core/model)
| Field | Type | Car Usage |
|-------|------|-----------|
| `myNodeNum` | `Int` | Our node number |
| `firmwareVersion` | `String?` | Display in expanded status panel |
| `model` | `String?` | Hardware model display |
## Presentation State Models (new, car module only)
### CarSessionState
Top-level state for a car session lifecycle.
```kotlin
data class CarSessionState(
val connectionStatus: ConnectionStatus,
val onlineNodeCount: Int,
val lastMessageTime: Long?, // epoch millis, null if no messages
val activeEmergencies: List<EmergencyAlert>,
val meshName: String?,
)
enum class ConnectionStatus {
CONNECTED,
CONNECTING,
DISCONNECTED,
}
```
**Source**: Derived from `BleConnectionState`, `NodeRepository.onlineNodeCount`, `PacketRepository`
### MessagingUiState
State for the messaging screen template builder.
```kotlin
data class MessagingUiState(
val channels: List<ChannelUi>,
val selectedChannelIndex: Int,
val conversations: List<ConversationUi>,
val emergencySpotlight: List<EmergencyAlert>?,
)
data class ChannelUi(
val index: Int,
val name: String,
val unreadCount: Int,
)
data class ConversationUi(
val contactKey: String,
val displayName: String,
val lastMessage: String,
val lastMessageTime: Long,
val unreadCount: Int,
val isEmergency: Boolean,
)
```
**Source**: `PacketRepository.getContacts()`, `PacketRepository.getUnreadCountFlow()`, channel config from radio
### NodeDashboardUiState
State for the node dashboard condensed items grid.
```kotlin
data class NodeDashboardUiState(
val nodes: List<NodeUi>,
val topologyHeader: TopologyHeader,
)
data class NodeUi(
val nodeNum: Int,
val longName: String,
val shortName: String,
val signalQuality: SignalQuality,
val batteryPercent: Int?,
val isOnline: Boolean,
val lastHeard: Long,
val hasPosition: Boolean,
)
enum class SignalQuality { EXCELLENT, GOOD, FAIR, POOR, UNKNOWN }
data class TopologyHeader(
val totalNodes: Int,
val onlineNodes: Int,
val meshName: String?,
)
```
**Source**: `NodeRepository.nodeDBbyNum`, `NodeRepository.onlineNodeCount`
### MapUiState
State for the PlaceListMapTemplate.
```kotlin
data class MapUiState(
val places: List<NodePlace>,
val ownPosition: LatLngWrapper?,
)
data class NodePlace(
val nodeNum: Int,
val name: String,
val latitude: Double,
val longitude: Double,
val lastUpdateTime: Long,
val distanceMeters: Float?, // from own position, null if own position unknown
)
data class LatLngWrapper(
val latitude: Double,
val longitude: Double,
)
```
**Source**: `NodeRepository.nodeDBbyNum` filtered to nodes with valid positions
### EmergencyAlert
Model for emergency messages requiring banner treatment.
```kotlin
data class EmergencyAlert(
val packetId: Int,
val senderName: String,
val senderNodeNum: Int,
val message: String,
val timestamp: Long,
val latitude: Double?,
val longitude: Double?,
val acknowledged: Boolean,
)
```
**Source**: `PacketRepository` flow filtered by emergency message type/priority
## State Transitions
### Car Session Lifecycle
```
[App Not Visible] → onCreateScreen() → [Active Session]
↓ ↓
↓ Screens pushed/popped via ScreenManager
↓ ↓
[App Not Visible] ← onDestroy() ← [Active Session]
```
### Connection Status
```
DISCONNECTED → (BLE scan + connect) → CONNECTING → (handshake complete) → CONNECTED
↑ |
└──────────────────── (link lost / timeout) ──────────────────────────────┘
```
### Emergency Alert Flow
```
[Message received] → (priority == EMERGENCY?) → YES → Add to activeEmergencies
→ Show Banner
→ Play notification sound
→ NO → Normal message flow
```
## Validation Rules
| Rule | Enforcement |
|------|-------------|
| Node name display ≤ 30 chars | Truncated by CAL host automatically |
| Message content ≤ 300 chars in list | Truncate with "…"; full on tap/TTS |
| Channel name ≤ 12 chars for Chip | Truncated with "…" |
| Max 6 conversations visible | CAL template item limit; paginate |
| Map pins require valid lat/lng | Filter nodes without position |
| Emergency banner requires non-empty message | Skip silent emergency packets |

View File

@@ -0,0 +1,134 @@
# Implementation Plan: Car App Library Integration
**Branch**: `feature/20260521-153452-car-app-library-integration` | **Date**: 2026-05-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `specs/20260521-153452-car-app-library-integration/spec.md`
## Summary
Integrate Android Car App Library 1.9.0-alpha01 into Meshtastic-Android as a new `feature/car` module, delivering a complete automotive mesh radio interface with 7 screens (messaging, node dashboard, channel management, emergency alerts, map, quick actions, mesh status panel). The module is Android-only, reuses all existing `core/` business logic via Koin DI, and leverages CAL's template-based rendering (no Compose). Voice reply uses CAL's built-in ConversationItem voice input; system-level "Hey Google" commands are handled separately by the AppFunctions feature.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21, Car API Level 8+
**Primary Dependencies**: `androidx.car.app:app:1.9.0-alpha01`, `androidx.car.app:app-projected:1.9.0-alpha01`, `androidx.car.app:app-automotive:1.9.0-alpha01`, Koin 4.2.1 (Koin Annotations + K2 Plugin), Firebase Crashlytics (BOM 34.13.0)
**Storage**: Room KMP (existing), DataStore KMP (existing) — no new storage
**Testing**: `./gradlew :feature:car:testGoogleDebugUnitTest` (Android-only module), `androidx.car.app:app-testing:1.9.0-alpha01` for host simulation, Robolectric for unit tests
**Target Platform**: Android Auto (projection, API 23+) and AAOS (embedded), Car API Level 8 minimum
**Project Type**: Mobile app — new Android-only feature module within KMP project
**Performance Goals**: Message display latency ≤ 3s, emergency banner ≤ 1s, channel switch ≤ 1s, map pin update ≤ 5s
**Constraints**: ≤ 2 taps for all primary actions, < 10% battery overhead, zero crashes/ANRs in 2-hour sessions, `google` flavor only
**Scale/Scope**: 7 car screens, ~15-20 new source files, 1 new Gradle module, 0 changes to existing modules' APIs
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- **I. Kotlin Multiplatform Core**: ✅ PASS — No `commonMain` changes. All new code resides in `feature/car/src/main/` (Android-only module). Business logic is consumed from existing `core/repository`, `core/data`, `core/domain`, `core/ble` KMP modules via their public interfaces. No new business logic is introduced in the car module — it is purely a presentation layer adapting existing repositories to CAL templates.
- **II. Zero Lint Tolerance**: ✅ PASS — Will run:
- `./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck`
- `./gradlew :feature:car:detekt`
- Module is Android-only so uses standard detekt tasks (not KMP variants)
- **III. Compose Multiplatform UI**: ✅ N/A — Car App Library uses its own template-based rendering system, not Compose. No `@Composable` functions are introduced. `MeshtasticNavDisplay` and `NavigationBackHandler` do not apply to CAL's `ScreenManager` navigation. No floats displayed (all text pre-formatted by existing `MetricFormatter`/`NumberFormatter` in core modules).
- **IV. Privacy First**: ✅ PASS — No new data collection or network calls. Reuses existing repositories with their privacy controls. Location data on map uses existing user-opt-in position sharing. No PII/keys in logs. Crashlytics tagging uses session ID only (no PII). `core/proto` submodule not modified.
- **V. Design Standards Compliance**: ✅ N/A (justified) — CAL apps use automotive-specific template design language enforced by the Android Auto host, not the Meshtastic Client Design Standards which target phone/desktop Compose UI. The host enforces readability (font sizes, item limits, distraction guidelines). Cross-Platform Spec field is N/A because CAL is Android-only with no cross-platform equivalent. Emergency alert visual treatment follows NHTSA Phase 2 automotive HMI guidelines via CAL Banner APIs.
- **VI. Verify Before Push**: ✅ Commands recorded:
```bash
# Local verification
./gradlew spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:testGoogleDebugUnitTest
# Post-push CI check
gh pr checks <PR> || gh run list --branch feature/20260521-153452-car-app-library-integration --limit 5
```
## Project Structure
### Documentation (this feature)
```text
specs/20260521-153452-car-app-library-integration/
├── plan.md # This file
├── research.md # Phase 0: CAL API research, architecture decisions
├── data-model.md # Phase 1: Entities and state models
├── quickstart.md # Phase 1: Developer onboarding guide
├── contracts/ # Phase 1: CAL service contracts and manifest declarations
│ ├── car-app-service.md
│ └── manifest-declarations.md
└── tasks.md # Phase 2 output (/speckit.tasks command)
```
### Source Code (repository root)
```text
feature/car/
├── build.gradle.kts # Android-only library, google flavor only
├── src/
│ ├── main/
│ │ ├── AndroidManifest.xml # CarAppService declaration, categories
│ │ ├── kotlin/org/meshtastic/feature/car/
│ │ │ ├── di/
│ │ │ │ └── FeatureCarModule.kt # Koin module for car DI
│ │ │ ├── service/
│ │ │ │ ├── MeshtasticCarAppService.kt # CarAppService entry point
│ │ │ │ └── MeshtasticCarSession.kt # Session lifecycle, screen manager
│ │ │ ├── screens/
│ │ │ │ ├── HomeScreen.kt # Tab-based entry (messaging, nodes, map)
│ │ │ │ ├── MessagingScreen.kt # ConversationItem list, channel chips
│ │ │ │ ├── ConversationScreen.kt # Single conversation with voice reply
│ │ │ │ ├── NodeDashboardScreen.kt # Condensed Items node grid
│ │ │ │ ├── NodeDetailScreen.kt # Expanded node info
│ │ │ │ ├── MapScreen.kt # PlaceListMapTemplate
│ │ │ │ └── ChannelManagementScreen.kt # Channel selection/switching
│ │ │ ├── alerts/
│ │ │ │ └── EmergencyHandler.kt # Banner management for emergencies
│ │ │ ├── panels/
│ │ │ │ └── MeshStatusPanel.kt # Minimized Control Panel
│ │ │ └── util/
│ │ │ ├── CrashlyticsCarTagger.kt # car_session key tagging
│ │ │ └── TemplateBuilders.kt # Helper extensions for CAL templates
│ │ └── res/
│ │ ├── values/
│ │ │ └── strings.xml # Car-specific strings
│ │ └── xml/
│ │ └── automotive_app_desc.xml # AAOS app description
│ └── test/
│ └── kotlin/org/meshtastic/feature/car/
│ ├── service/
│ │ └── MeshtasticCarSessionTest.kt
│ ├── screens/
│ │ ├── MessagingScreenTest.kt
│ │ ├── NodeDashboardScreenTest.kt
│ │ └── MapScreenTest.kt
│ └── alerts/
│ └── EmergencyHandlerTest.kt
# Existing modules (consumed, NOT modified):
core/repository/ # PacketRepository, NodeRepository, QuickChatActionRepository, SendMessageUseCase
core/data/ # NodeRepositoryImpl, PacketRepositoryImpl
core/ble/ # BleConnection (Application-scoped singleton)
core/model/ # Node, DataPacket, MyNodeInfo, etc.
core/domain/ # Use cases (SendMessageUseCase, etc.)
```
**Structure Decision**: New `feature/car` module as an Android-only library (not KMP). Follows existing feature module pattern but uses `AndroidLibraryFlavorsConventionPlugin` instead of KMP plugin since CAL has no multiplatform support. Only the `google` flavor includes this module (mirrors Maps/Crashlytics flavor split).
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| III. Compose Multiplatform UI — N/A | CAL uses proprietary template system, not Compose | Cannot render Compose inside automotive templates; CAL enforces distraction-safe UI via templates exclusively |
| V. Design Standards — N/A | Automotive design is governed by NHTSA + host-enforced constraints | Meshtastic Design Standards target phone/desktop Compose; applying them to CAL templates would conflict with automotive safety requirements |
| Android-only module in KMP project | CAL SDK is Android-exclusive | No KMP equivalent exists; all business logic remains in `commonMain` — only the thin presentation adapter is platform-specific |

View File

@@ -0,0 +1,150 @@
# Quickstart: Car App Library Integration
**Feature**: Car App Library Integration
**Date**: 2026-05-21
## Prerequisites
- Android Studio Ladybug or newer (for CAL preview tools)
- JDK 21 (`JAVA_HOME` set)
- `ANDROID_HOME` set with API 35+ SDK installed
- Proto submodule initialized: `git submodule update --init`
- `local.properties` configured: `cp secrets.defaults.properties local.properties`
- Android Auto Desktop Head Unit (DHU) installed via SDK Manager → SDK Tools → Android Auto Desktop Head Unit
## Setup
### 1. Sync and Build
```bash
# Full sync (includes new :feature:car module)
./gradlew sync
# Build google flavor (required — car module is google-only)
./gradlew assembleGoogleDebug
```
### 2. Install DHU for Testing
The Desktop Head Unit simulates Android Auto on your development machine.
```bash
# Install via SDK Manager (or command line)
sdkmanager "extras;google;auto"
# Start DHU (after connecting a device/emulator with the app installed)
$ANDROID_HOME/extras/google/auto/desktop-head-unit
```
### 3. Run on Android Auto (Projection Mode)
1. Install the google debug build on a physical device: `./gradlew installGoogleDebug`
2. Enable Developer Mode in Android Auto settings on the phone
3. Start the DHU: `desktop-head-unit`
4. The Meshtastic car app appears in the DHU's app launcher under "Messaging" category
### 4. Run on AAOS Emulator
```bash
# Create AAOS emulator (API 33+ automotive system image)
avdmanager create avd -n "AAOS_Test" -k "system-images;android-33;google_apis_playstore;x86_64" --device "automotive_1024p_landscape"
# Start emulator
emulator -avd AAOS_Test
# Install
./gradlew installGoogleDebug
```
## Development Workflow
### Module Location
All car-specific code lives in `feature/car/`:
```
feature/car/src/main/kotlin/org/meshtastic/feature/car/
├── di/ → Koin DI module
├── service/ → CarAppService + Session
├── screens/ → CAL Screen implementations
├── alerts/ → Emergency banner handler
├── panels/ → Minimized Control Panel
└── util/ → Helpers (Crashlytics tagger, template builders)
```
### Key Development Patterns
**Screen implementation**:
```kotlin
class MessagingScreen(carContext: CarContext) : Screen(carContext) {
// Inject repositories via Koin
private val packetRepository: PacketRepository by inject()
override fun onGetTemplate(): Template {
// Build template from current state
// Call invalidate() when data changes to trigger re-render
}
}
```
**Data observation** (CAL doesn't use Compose — use coroutine collection):
```kotlin
// In Screen's lifecycle, collect flows and call invalidate()
lifecycleScope.launch {
repository.getContacts().collect { contacts ->
cachedContacts = contacts
invalidate() // Triggers onGetTemplate() re-call
}
}
```
**Template refresh**: CAL screens are invalidated manually — no reactive binding. Call `invalidate()` whenever backing data changes.
### Testing
```bash
# Unit tests (uses androidx.car.app:app-testing)
./gradlew :feature:car:testGoogleDebugUnitTest
# Lint + formatting
./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck :feature:car:detekt
```
**Test approach**: Use `SessionController` and `TestCarContext` from `app-testing` artifact to simulate host interactions without a real car/DHU.
```kotlin
@Test
fun `messaging screen shows conversations`() {
val controller = SessionController(
MeshtasticCarSession(testSessionInfo),
TestCarContext(ApplicationProvider.getApplicationContext())
)
// Push screen, assert template content
}
```
### Debugging
- **CAL Logcat filter**: `tag:CarApp OR tag:CarService`
- **Template errors**: CAL validates templates at runtime — check logcat for `TemplateValidationException`
- **Screen stack**: Use `ScreenManager.getTop()` to inspect current screen
- **Crashlytics**: Filter by `car_session` custom key in Firebase Console
## Common Tasks
| Task | Command / Action |
|------|------------------|
| Add a new screen | Create `Screen` subclass in `screens/`, register in navigation |
| Add a CAL dependency | Update `gradle/libs.versions.toml` + `feature/car/build.gradle.kts` |
| Test with DHU | `desktop-head-unit` after installing google debug build |
| Check template compliance | Run app on DHU; host validates template constraints |
| Filter car crashes | Firebase Console → Crashlytics → Filter: `car_session` is not empty |
| Full verification | `./gradlew spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:testGoogleDebugUnitTest` |
## Architecture Notes
- **No Compose**: CAL uses its own template-based rendering. Don't mix Compose APIs.
- **No `commonMain`**: This is an Android-only module. All code in `src/main/kotlin/`.
- **Shared BLE**: Don't create new BLE connections. Inject existing `BleConnection` singleton.
- **Koin DI**: All core repositories are already in the graph. Just `inject()` them.
- **Flavor**: Only `google` flavor includes this module. Never reference it from `fdroid` code.

View File

@@ -0,0 +1,164 @@
# Research: Car App Library Integration
**Feature**: Car App Library Integration
**Date**: 2026-05-21
## R1: Car App Library 1.9.0-alpha01 New Components
**Decision**: Use all 7 new CAL 1.9.0-alpha01 components as specified
**Rationale**: The alpha release provides modern automotive UI components that directly map to Meshtastic use cases. The user explicitly accepted alpha risk.
**Components and their application**:
| CAL Component | Meshtastic Screen | Purpose |
|---------------|-------------------|---------|
| Spotlight Section | Messaging (emergency) | Emergency messages pinned at top of message list |
| Condensed Items | Node Dashboard | Dense node list showing 6+ nodes without scroll |
| Chips | Messaging (channels) | Channel switching with unread badges |
| Minimized Control Panel | All screens (persistent) | Mesh status: radio connection, node count, last message time |
| Banners | Emergency alerts | Full-screen overlay for emergency broadcasts |
| Section Headers | Messaging | Group messages by channel within conversation list |
| Expanded Header Layout | Node Dashboard | Mesh topology summary at top of node grid |
**Alternatives considered**:
- Wait for stable 1.9.0 release → Rejected: Timeline unknown; alpha APIs are functionally complete
- Use legacy ListTemplate/MessageTemplate → Rejected: Misses density benefits (Condensed Items) and visual hierarchy (Spotlight/Headers)
**API Level requirement**: Car API Level 8 (maps to `minCarApiLevel 8` in manifest). Older hosts gracefully hide the app.
## R2: Module Architecture — Android-Only vs KMP
**Decision**: Create `feature/car` as an Android-only library module (not KMP)
**Rationale**: CAL SDK is exclusively Android. Creating a KMP module with only `androidMain` source sets would add unnecessary complexity (empty `commonMain`, unused KMP plugin overhead). The project already has Android-only modules (`core/api`, `core/barcode`, `androidApp`) as precedent.
**Build plugin**: `AndroidLibraryFlavorsConventionPlugin` (not `KmpLibraryConventionPlugin`) — ensures proper flavor-aware configuration consistent with existing Android-only modules.
**Alternatives considered**:
- KMP module with `androidMain` only → Rejected: No cross-platform value; KMP plugin adds 2-3s build overhead with zero benefit
- Inline within `androidApp` module → Rejected: Violates separation of concerns; feature modules should be independent
## R3: BLE Connection Sharing Strategy
**Decision**: Shared Application-scoped `BleConnection` singleton via Koin, no new connection management
**Rationale**: The existing `BleConnection` in `core/ble` is already scoped to the Application lifecycle via Koin's singleton scope. When Android Auto starts the `CarAppService`, it runs in the same process as the phone app (projection mode) — the Koin graph is shared naturally. The `CarAppService` keeps the process alive via the Android Auto host binding, ensuring the BLE connection persists.
**Key implementation detail**: `KableBleConnection` is instantiated by `KableBleConnectionFactory` and held as a Koin singleton. The car module simply injects the same instance — no reconnection logic needed.
**AAOS (embedded) consideration**: On AAOS, the app runs as a standalone process. The same Koin graph initializes in `Application.onCreate()`. BLE connection management is identical because it's Application-scoped regardless of entry point.
**Alternatives considered**:
- Dedicated car BLE connection → Rejected: Would conflict with phone app's connection; BLE to Meshtastic radio is single-link
- Service binding to phone app → Rejected: Unnecessary IPC; same process in projection mode; AAOS doesn't have the phone app
## R4: Crashlytics car_session Tagging
**Decision**: Tag all Crashlytics events with `car_session` custom key during car session lifecycle
**Rationale**: Enables filtering car-specific crashes/ANRs in Firebase console without new infrastructure. The `MeshtasticCarSession` sets the key on `onCreateScreen()` and clears on `onDestroy()`.
**Implementation**:
```kotlin
// In MeshtasticCarSession.onCreateScreen():
FirebaseCrashlytics.getInstance().setCustomKey("car_session", sessionInfo.sessionId.toString())
// In MeshtasticCarSession lifecycle end:
FirebaseCrashlytics.getInstance().setCustomKey("car_session", "")
```
**Alternatives considered**:
- Separate Crashlytics instance → Not possible; Firebase is process-wide singleton
- DataDog APM → Rejected: Project uses Crashlytics; DataDog not in dependency graph
## R5: Messaging via ConversationItem + Voice Reply
**Decision**: Use `ConversationItem` API with CAL's built-in voice input for reply
**Rationale**: CAL's `ConversationItem` is purpose-built for messaging apps on Android Auto. It handles:
- Message display with sender avatar, name, timestamp
- Unread indicators
- Voice reply flow (tap → record → send) with no custom speech recognition needed
- Quick-reply suggestions
The existing `SendMessageUseCase` in `core/repository` accepts `(text, contactKey, replyId)` — the car module calls this directly after voice transcription completes.
**Data flow**: `ConversationItem.onReply { text -> sendMessageUseCase(text, contactKey) }`
**Alternatives considered**:
- Custom speech recognition → Rejected: CAL handles this automatically; would duplicate system capabilities
- Google Assistant App Actions → Rejected: Separate concern handled by AppFunctions feature
## R6: PlaceListMapTemplate for Node Map (POI Category)
**Decision**: Use `PlaceListMapTemplate` under POI category for static node position display
**Rationale**: POI category avoids NAVIGATION category requirements (turn-by-turn guidance, active routing), which would trigger additional Play Store review burden and potential conflicts with navigation apps. `PlaceListMapTemplate` renders a map with place items (pins) + a scrollable list — perfect for showing node positions.
**Implementation approach**:
- Each node with known GPS position becomes a `Place` item with `LatLng`
- List items show node name + distance + last update time
- Map auto-zooms to fit all visible pins
- Tap a list item → NodeDetailScreen with message option
- Refresh interval: 5 seconds (matches NFR map update latency requirement)
**Limitation**: No live tracking line or animated position updates (NAVIGATION category feature, deferred to v2)
**Alternatives considered**:
- MapWithContentTemplate + NAVIGATION category → Rejected by spec decision; deferred to v2
- No map at all → Rejected: Location awareness is core Meshtastic differentiator
## R7: Koin DI Integration for Car Module
**Decision**: New `FeatureCarModule` using Koin Annotations, registered in app's module graph
**Rationale**: Consistent with project's DI pattern. All feature modules declare a Koin module that is included by the `androidApp` module graph. The car module's DI graph is simple — it only needs to declare car-specific Screen factories and the EmergencyHandler; all business logic comes from existing core modules.
**Registration**: `androidApp/src/googleMain/` includes `FeatureCarModule` in the Koin application configuration (google flavor only).
**Key bindings**:
- `MeshtasticCarSession` → factory (new per session)
- `EmergencyHandler` → singleton (one per process)
- `CrashlyticsCarTagger` → singleton
- All repositories, use cases → inherited from existing core modules (already in graph)
## R8: AppFunctions Interop — Shared Interface Reuse
**Decision**: Reuse `FuzzyNameResolver` pattern from AppFunctions for node name matching in voice replies
**Rationale**: When a driver sends a direct message via voice, they may say a node name imprecisely. The AppFunctions feature (in-flight) implements fuzzy node name resolution. While the `AiFunctionProvider` interface is not yet merged, the car module can implement the same fuzzy matching logic directly using `NodeRepository.nodeDBbyNum` and Levenshtein distance or substring matching.
**Implementation**: Standalone `FuzzyNodeNameResolver` utility class in `feature/car/util/` that queries `NodeRepository` and performs case-insensitive substring + edit-distance matching. If/when AppFunctions lands and exposes a shared resolver in `core/data/commonMain`, the car module can delegate to it.
**Alternatives considered**:
- Wait for AppFunctions to land first → Rejected: Unclear timeline; car module should not block on it
- Exact match only → Rejected: Poor voice UX ("node exclamation one two three four" vs "James")
## R9: Emergency Alert Banner Strategy
**Decision**: Observe emergency messages via `PacketRepository` Flow, trigger CAL Banner API
**Rationale**: Emergency messages are already classified in the packet data layer (message type/priority). The `EmergencyHandler` subscribes to the message flow, filters for emergency-priority packets, and immediately invokes `CarToast` + `AppManager.showAlert()` to display a Banner. The Banner overlays any active screen within CAL's rendering pipeline.
**Audio**: Use `NotificationManager` to play a notification sound on the car's notification audio channel (`AudioAttributes.USAGE_NOTIFICATION`), not media channel (per NFR-008).
**Alternatives considered**:
- Poll for emergencies on timer → Rejected: Violates 1-second latency requirement
- Use Android notifications only → Rejected: Would not overlay within CAL UI; needs in-app Banner
## R10: Build Configuration — Google Flavor Only
**Decision**: `feature/car` module included only in the `google` product flavor
**Rationale**: CAL apps require Google Play Services for Android Auto projection. The F-Droid flavor explicitly excludes Google dependencies. The module is conditionally included via flavor-based dependency in `androidApp/build.gradle.kts`:
```kotlin
"googleImplementation"(projects.feature.car)
```
This mirrors existing patterns like Firebase/Maps dependencies being google-flavor-only.
**Alternatives considered**:
- Include in all flavors → Rejected: CAL requires Google Play Services; F-Droid builds would fail
- Separate app module for car → Rejected: Adds unnecessary complexity; flavor separation is simpler

View File

@@ -0,0 +1,460 @@
# Feature Specification: Car App Library Integration
**Feature Branch**: `feature/20260521-153452-car-app-library-integration`
**Created**: 2026-05-21
**Status**: Draft
**Input**: Integrate Android Car App Library 1.9.0-alpha01 as a fully-featured, first-class car app
**Cross-Platform Spec**: N/A — platform-specific only (Android Auto / AAOS exclusive; CAL has no cross-platform equivalent)
## Summary
Integrate the Android Car App Library 1.9.0-alpha01 into Meshtastic-Android to deliver a fully-featured, first-class automotive experience for Android Auto and Android Automotive OS. The integration creates a distraction-optimized, safety-first mesh radio interface for vehicles — enabling drivers to monitor mesh network status, read and reply to messages via voice, view node locations on maps, and receive emergency alerts with immediate prominence. A new `feature/car` module houses the Android-only CAL layer while reusing all shared business logic from existing core and feature modules.
## Clarifications
### Session 2026-05-21
- Q: How should voice commands be implemented — CAL built-in voice input, full Assistant App Actions, or both? → A: CAL built-in voice input only (tap reply → dictate → send). System-level "Hey Google" commands are handled separately by the AppFunctions feature (`specs/20260521-091500-app-functions/`), which exposes `sendMessage`, `getMeshStatus`, `listNodes`, `getRecentMessages`, and `getNodePosition` to Android system AI (Gemini) automatically — including on car displays.
- Q: Should the app declare NAVIGATION category for MapWithContentTemplate, or use PlaceListMapTemplate under POI? → A: Stay with POI category, use PlaceListMapTemplate (static pin list, refreshable). Avoids nav app conflicts and Play Store review burden. Live position tracking under NAVIGATION category deferred to v2.
- Q: Should the CarAppService maintain an independent BLE connection or share the phone app's existing connection? → A: Shared connection — single Application-scoped BleConnectionManager instance via Koin. CarAppService keeps the process alive via Android Auto host; BLE connection persists at the Service/Application level, not Activity level.
- Q: What observability approach should the car module use? → A: Reuse existing Crashlytics with `car_session` custom key tagging for car-specific filtering. No new observability infrastructure; tag existing analytics paths.
- Q: Should the car app unlock additional features when the vehicle is parked? → A: No parked-mode differentiation. Templated messaging apps provide a uniform experience regardless of driving state. Voice reply is built into ConversationItem. The Android Auto host enforces its own driving restrictions; the app just provides templates.
## Goals
1. **Complete automotive mesh experience** — Deliver all seven core screens (messaging, node dashboard, channel management, emergency alerts, map, quick actions, mesh status panel) as a single release
2. **Safety-first interaction model** — Every interaction completes in ≤ 2 taps or via voice, meeting automotive distraction guidelines
3. **Leverage 1.9.0-alpha01 components** — Showcase Spotlight Sections, Condensed Items, Chips, Minimized Control Panel, Banners, Section Headers, and Expanded Headers for a modern car UI
4. **Zero disruption to existing app** — The new `feature/car` module integrates via dependency injection without modifying existing module APIs or behavior
5. **Voice-first messaging** — Message composition defaults to voice input, with quick-reply templates as fallback for hands-free operation
## Non-Goals
- Firmware updates via the car interface (too complex and risky while driving)
- Full settings UI in-car (a minimal parked-only subset may be considered in future)
- Desktop or iOS car support (this is Android Auto / AAOS specific)
- Video playback or media/audio streaming features
- Compose UI interop (CAL uses its own template-based rendering system)
- Google Assistant App Actions / voice command routing (handled by separate AppFunctions feature)
- NAVIGATION category declaration / live map tracking (deferred to v2; v1 uses POI with PlaceListMapTemplate)
- Phone app UI changes (car UI is additive only)
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Read and Reply to Mesh Messages While Driving (Priority: P1)
A driver receives mesh messages from their group while on the road. They glance at the head unit to see new messages and use voice to compose a reply, keeping hands on the wheel and eyes on the road.
**Why this priority**: Messaging is the primary use case for Meshtastic. Enabling safe in-car messaging addresses the #1 reason users would want car integration.
**Independent Test**: Can be fully tested by sending a message from a second Meshtastic device, verifying it appears on the car display, and dictating a voice reply that arrives on the sender's device.
**Acceptance Scenarios**:
1. **Given** the car app is connected to a Meshtastic radio and a new message arrives, **When** the driver views the messaging screen, **Then** the new message appears within 3 seconds with sender name, timestamp, and message content visible at a glance
2. **Given** the driver is viewing a conversation, **When** they tap the reply action, **Then** the system presents voice input as the default composition method
3. **Given** the driver has initiated voice reply, **When** they speak their message and confirm, **Then** the message is sent to the correct channel/DM within 2 seconds
4. **Given** the driver prefers not to use voice, **When** they select quick-reply, **Then** a list of configurable template responses (e.g., "On my way", "Copy that", "10 minutes out") is presented for one-tap selection
5. **Given** the mesh radio is disconnected, **When** the driver opens messaging, **Then** a banner clearly indicates offline status and cached messages remain visible as read-only
---
### User Story 2 - Emergency Alert Reception (Priority: P1)
A driver receives an emergency alert broadcast from a mesh node (SOS, hazard warning, etc.). The alert demands immediate attention with distinct visual and audio treatment, regardless of which screen is currently active.
**Why this priority**: Emergency alerts are life-safety critical. Failure to surface them prominently could have real-world safety consequences.
**Independent Test**: Can be tested by triggering an emergency broadcast from a test device and verifying the car app interrupts current activity with a banner alert.
**Acceptance Scenarios**:
1. **Given** any screen is active, **When** an emergency message is received, **Then** a high-priority banner appears immediately (within 1 second) with emergency iconography and distinct color treatment
2. **Given** an emergency banner is displayed, **When** the driver taps it, **Then** full emergency details are shown including sender identity, location (if available), and timestamp
3. **Given** an emergency alert has been received, **When** the driver navigates to the messaging screen, **Then** the emergency message appears in a Spotlight Section at the top, visually distinguished from normal messages
4. **Given** emergency audio alerts are enabled, **When** an emergency message arrives, **Then** an audible notification tone plays through the car's audio system
---
### User Story 3 - Monitor Node Network Status (Priority: P2)
A driver glances at the head unit to check how many mesh nodes are in range, their signal strength, and battery levels — useful for caravan/convoy scenarios or checking if they're still in range of base camp.
**Why this priority**: Node awareness is the second-most-common Meshtastic use case and provides critical situational awareness for mobile users.
**Independent Test**: Can be tested by having 3+ nodes in range and verifying the dashboard displays each with correct signal/battery metrics.
**Acceptance Scenarios**:
1. **Given** the car app is connected with multiple nodes in range, **When** the driver opens the node dashboard, **Then** all known nodes are displayed as Condensed Items showing node name, signal quality indicator, and battery level
2. **Given** 6+ nodes are in range, **When** viewing the dashboard, **Then** at least 6 nodes are visible simultaneously without scrolling (leveraging Condensed Items)
3. **Given** a node goes offline, **When** the dashboard refreshes, **Then** the offline node is visually distinguished (dimmed or marked) and sorted to the bottom
4. **Given** the node list is displayed, **When** the driver taps a node, **Then** a detail view shows last heard time, distance (if location known), hardware model, and direct message option
---
### User Story 4 - Switch Between Channels (Priority: P2)
A driver participating in multiple mesh channels (e.g., "Convoy", "Emergency", "General") quickly switches between them to view messages from different groups.
**Why this priority**: Channel management is essential for users in organized groups and must be achievable without complex navigation.
**Independent Test**: Can be tested by configuring 3+ channels and verifying single-tap channel switching via chips.
**Acceptance Scenarios**:
1. **Given** the device has multiple channels configured, **When** the messaging screen loads, **Then** channel chips are displayed at the top allowing single-tap switching
2. **Given** channel chips are visible, **When** the driver taps a different channel chip, **Then** the message list updates to show that channel's messages within 1 second
3. **Given** a channel has unread messages, **When** viewing the chip bar, **Then** that channel's chip displays an unread indicator (badge or visual emphasis)
---
### User Story 5 - View Node Locations on Map (Priority: P2)
A driver in a convoy scenario views the locations of all mesh nodes on a map to understand relative positions and navigate toward or away from group members.
**Why this priority**: Location awareness is a core differentiator of Meshtastic and maps are natural for automotive interfaces.
**Independent Test**: Can be tested by having 2+ nodes reporting GPS positions and verifying pins appear at correct locations on the car map.
**Acceptance Scenarios**:
1. **Given** nodes are reporting GPS positions, **When** the driver opens the map screen, **Then** node locations appear as labeled items in a PlaceListMapTemplate with pins on the map
2. **Given** the map is displayed with node pins, **When** the driver taps a node item in the list, **Then** a detail panel shows node name, distance, last update time, and option to send a direct message
3. **Given** the driver's own position is available, **When** viewing the map, **Then** their position is shown distinctly from other nodes
4. **Given** a node's position updates, **When** the map is visible, **Then** the pin moves to the new position within 5 seconds
---
### User Story 6 - Persistent Mesh Status at a Glance (Priority: P3)
While using any car app feature, the driver can glance at a persistent mini-panel showing mesh connectivity health — how many nodes are online, time since last message, and connection status to the radio.
**Why this priority**: Persistent status awareness reduces the need to navigate between screens, minimizing distraction.
**Independent Test**: Can be tested by verifying the minimized control panel remains visible across all screens and updates in real-time.
**Acceptance Scenarios**:
1. **Given** the car app is active on any screen, **When** the driver glances at the minimized control panel, **Then** they see: radio connection status, node count online, and time since last received message
2. **Given** the radio disconnects, **When** the status panel updates, **Then** it clearly indicates "Disconnected" with warning iconography
3. **Given** the minimized panel is visible, **When** the driver taps it, **Then** it expands to show additional detail (mesh name, own node battery, firmware version)
---
### User Story 7 - In-Context Voice Input for Actions (Priority: P3)
A driver uses CAL's built-in voice input to compose messages and perform actions without typing — tapping reply then dictating, or using TTS readback of messages. System-level voice commands ("Hey Google, send Meshtastic message to John") are handled separately by the AppFunctions feature and work automatically on car displays without car module code.
**Why this priority**: Voice is the safest interaction modality while driving and rounds out the hands-free experience.
**Independent Test**: Can be tested by tapping the reply action, dictating a message via CAL voice input, and verifying delivery. System-level "Hey Google" commands are tested via the AppFunctions spec.
**Acceptance Scenarios**:
1. **Given** the car app is on a conversation screen, **When** the driver taps the reply action and speaks a message, **Then** voice composition targets that node/channel using CAL's built-in voice input API
2. **Given** a message is displayed, **When** the driver taps a "read aloud" action, **Then** the message is read via TTS including sender name and content
3. **Given** the driver initiates a direct message from the node dashboard, **When** they tap a node and select "message", **Then** voice input is presented as the default composition method with `FuzzyNameResolver` used for node name matching
---
### Edge Cases
- What happens when the Bluetooth connection to the Meshtastic radio drops mid-conversation? → Banner notification + graceful degradation to cached data, auto-reconnect in background
- What happens when the message list exceeds CAL template item limits? → Cap at 10 conversations with 5 messages each per Android Auto best practices; most recent first
- How does the system handle very long messages that exceed car display constraints? → Truncation with "..." and full message available on tap or read-aloud
- What happens when outgoing messages exceed 237 bytes (Meshtastic protocol limit)? → Reject with user feedback ("Message too long"); do not attempt to send
- What happens when the car's system restricts interaction (e.g., car moving at speed)? → No parked-mode differentiation; the templated messaging UI is uniform regardless of driving state. Voice reply is built into ConversationItem automatically. The Android Auto host enforces its own driving restrictions — the app provides templates only.
- What happens when multiple emergency alerts arrive simultaneously? → Stack as multiple banners; Spotlight Section shows all active emergencies chronologically
- How does the app handle no configured channels? → Show onboarding prompt directing user to configure channels on their phone first
- What happens with emoji-only or admin messages? → Filtered from car display entirely (not shown in conversation list or read aloud)
- What happens on initial session connect with existing unread messages? → Batch-load up to 50 unread messages across conversations; also post MessagingStyle notifications for read-back support
- How are favorites vs recent contacts distinguished? → Favorites (node.favorite == true) grouped at top of DM list with Section Header; remaining contacts sorted by last-heard, capped at 24
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST register as a Car App Service discoverable by Android Auto and AAOS hosts
- **FR-002**: System MUST display incoming mesh messages in a scrollable list grouped by channel using Section Headers
- **FR-003**: System MUST support voice-based message composition as the primary reply method
- **FR-004**: System MUST provide quick-reply templates selectable with a single tap
- **FR-005**: System MUST display emergency messages as high-priority Banners that overlay any active screen within 1 second of receipt
- **FR-006**: System MUST present emergency messages in a Spotlight Section when viewing the messaging screen
- **FR-007**: System MUST display all known mesh nodes as Condensed Items showing name, signal quality, and battery level
- **FR-008**: System MUST support channel switching via Chips displayed at the top of the messaging screen
- **FR-009**: System MUST render node positions on a map using PlaceListMapTemplate under the POI category (static pin list, refreshable; NAVIGATION category with MapWithContentTemplate deferred to v2)
- **FR-010**: System MUST maintain a persistent Minimized Control Panel showing radio status, online node count, and last message time
- **FR-011**: System MUST display a Banner when the Bluetooth connection to the radio is lost
- **FR-012**: System MUST support expanding node details on tap (last heard, distance, hardware model)
- **FR-013**: System MUST use Expanded Header Layout for the node dashboard showing mesh topology summary
- **FR-014**: System MUST declare MESSAGING as the primary category and POI as secondary
- **FR-015**: System MUST gracefully degrade to cached/read-only data when the mesh radio is disconnected
- **FR-016**: System MUST support unread message indicators on channel Chips
- **FR-017**: System MUST filter emoji-only and admin messages from the car display (only text messages shown)
- **FR-018**: System MUST reject outgoing messages exceeding 237 bytes (Meshtastic packet limit) with user-visible feedback
- **FR-019**: System MUST display at most 10 conversations and at most 5 messages per ConversationItem, per Android Auto best practices
- **FR-020**: System MUST group direct message contacts into "Favorites" (nodes marked favorite) and "Recent" sections using Section Headers
- **FR-021**: System MUST load up to 50 unread messages across conversations on session start, most recent first
- **FR-022**: System MUST also implement notification-based messaging (NotificationCompat.MessagingStyle with reply and mark-as-read Actions) as required by templated messaging apps
### Non-Functional Requirements
- **NFR-001**: All interactive elements MUST be reachable within 2 taps from any screen
- **NFR-002**: New message display latency MUST be ≤ 3 seconds from radio receipt to screen render
- **NFR-003**: Car app battery overhead MUST be < 10% additional drain compared to the phone app running alone
- **NFR-004**: Car App minimum API level MUST be Car API Level 8 (required for 1.9.0 components)
- **NFR-005**: The car module MUST NOT introduce dependencies that affect the phone app's build time by more than 5%
- **NFR-006**: All text elements MUST meet automotive readability guidelines (minimum font sizes per OEM requirements)
- **NFR-007**: The app MUST support both Android Auto (projection) and AAOS (embedded) deployment modes
- **NFR-008**: Emergency alert audio MUST play through the car's notification channel, not media channel
- **NFR-009**: Car module MUST tag all Crashlytics events with a `car_session` custom key (value: session ID) to enable car-specific crash/ANR filtering and diagnosis
- **NFR-010**: Screen invalidation MUST be debounced (≥300ms) and MUST NOT recreate Screen objects; use `invalidate()` to trigger `onGetTemplate()` re-evaluation, matching CarPlay's proven refresh pattern
- **NFR-011**: Template data refresh latency MUST be ≤500ms from invalidation trigger to rendered update
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| MeshtasticCarAppService | `feature/car/service/` | CAL Session host, entry point for Android Auto/AAOS |
| MessagingScreen | `feature/car/screens/` | Message list with channel chips, voice reply, quick-reply |
| NodeDashboardScreen | `feature/car/screens/` | Condensed Items grid of all mesh nodes |
| MapScreen | `feature/car/screens/` | PlaceListMapTemplate showing node positions as place items |
| EmergencyHandler | `feature/car/alerts/` | Banner management for emergency messages |
| MeshStatusPanel | `feature/car/panels/` | Minimized Control Panel with mesh health |
| CarMessageRepository | `core/data/` | Existing message repository (reused) |
| CarNodeRepository | `core/data/` | Existing node repository (reused) |
| ChannelManager | `core/domain/` | Existing channel logic (reused) |
| BleConnectionManager | `core/ble/` | Existing BLE connection (reused; Application-scoped singleton shared with phone app — CarAppService keeps process alive via host) |
### Component Interaction
```
┌─────────────────────────────────────────────────┐
│ Android Auto / AAOS Host │
└────────────────────┬────────────────────────────┘
│ CAL Session
┌────────────────────▼────────────────────────────┐
│ MeshtasticCarAppService │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
│ │Messaging │ │ Nodes │ │ Map Screen ││
│ │ Screen │ │Dashboard │ │ ││
│ └────┬─────┘ └────┬─────┘ └───────┬──────────┘│
│ │ │ │ │
│ ┌────▼─────────────▼───────────────▼──────────┐│
│ │ MeshStatusPanel (persistent) ││
│ └─────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────┐│
│ │ EmergencyHandler (banners) ││
│ └─────────────────────────────────────────────┘│
└────────────────────┬────────────────────────────┘
│ Koin DI
┌────────────────────▼────────────────────────────┐
│ Shared Business Logic (core/) │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ ┌───────┐ │
│ │Messages │ │ Nodes │ │Channels│ │ BLE │ │
│ │ Repo │ │ Repo │ │Manager │ │Connect│ │
│ └─────────┘ └─────────┘ └────────┘ └───────┘ │
└─────────────────────────────────────────────────┘
```
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | No changes | All shared business logic already exists in core modules |
| `androidMain` | New `feature/car` module | CAL is Android-only; entire car UI layer is platform-specific |
## Design Standards Compliance
- [ ] New screens reviewed against automotive HMI distraction guidelines (NHTSA Phase 2)
- [ ] CAL template system used exclusively (no custom rendering that bypasses automotive safety checks)
- [ ] Accessibility: Voice readback of all visual information, high-contrast automotive color schemes
- [ ] Typography: Uses CAL's built-in automotive-safe text sizing (enforced by host)
- [ ] Emergency alerts use distinct visual language (color, iconography) distinguishable from informational banners
## Privacy Assessment
- [ ] No PII, location data, or cryptographic keys logged or exposed beyond what existing modules already handle
- [ ] Car app reuses existing data layer — no new network calls or data collection
- [ ] Node location data displayed on map uses existing privacy controls (user opt-in for position sharing)
- [ ] No data sent to third-party automotive services
- [ ] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can read a new message and send a voice reply in under 15 seconds total interaction time
- **SC-002**: Emergency alerts are visible to the driver within 1 second of receipt by the radio
- **SC-003**: Node dashboard displays 6+ nodes simultaneously without scrolling (Condensed Items density)
- **SC-004**: All primary actions (read message, reply, check nodes, view map) reachable within 2 taps from home
- **SC-005**: Car app adds < 10% battery drain overhead compared to phone-only operation over a 1-hour driving session
- **SC-006**: Channel switching completes (chip tap to new message list rendered) within 1 second
- **SC-007**: App passes Android Auto App Quality review criteria for the MESSAGING category
- **SC-008**: 95% of voice-initiated replies complete successfully without fallback to touch input
- **SC-009**: Map displays node positions with < 5-second update latency when positions change
- **SC-010**: Zero crashes or ANRs attributed to the car module during a 2-hour continuous driving session
## Assumptions
- Car App Library 1.9.0-alpha01 APIs are sufficiently stable for production use (alpha risk accepted per user directive)
- The existing `core/data` repositories provide all necessary data access; no new data sources required
- Meshtastic radio remains paired and connected via BLE during driving (standard operating mode)
- BLE connection is Application-scoped (not Activity-scoped); CarAppService keeps the host process alive so the connection naturally persists regardless of phone app Activity state
- Users have already configured channels and node settings via the phone app before driving
- Android Auto host enforces its own distraction-optimization rules (template item limits, interaction restrictions); the app respects these constraints
- The `google` build flavor is the distribution target; F-Droid/GitHub flavors do not include car support
- Quick-reply templates are configurable via the phone app's settings; the car app consumes them read-only
- Voice input quality depends on the car's microphone hardware; the app delegates to Android's speech recognition system
- MapWithContentTemplate availability depends on NAVIGATION category declaration (deferred to v2); v1 uses PlaceListMapTemplate under POI which is widely supported
- Minimum Car API Level 8 is required; older Android Auto hosts will not show the app (graceful absence, not crash)
- Koin dependency injection is used consistently with Koin Annotations for the new module
- TTS (text-to-speech) for reading messages aloud uses Android's built-in TTS engine
## External References & Research
### Official Documentation
| Resource | URL | Relevance |
|----------|-----|-----------|
| Car App Library Release Notes | https://developer.android.com/jetpack/androidx/releases/car-app | 1.8.0-beta01 & 1.9.0-alpha01 component APIs |
| Building Car Apps (Training) | https://developer.android.com/training/cars/apps | CarAppService setup, templates, lifecycle |
| Templated Messaging Guide | https://developer.android.com/training/cars/communication/templated-messaging | ConversationItem, voice reply, notification integration |
| Notification-based Messaging | https://developer.android.com/training/cars/messaging | MessagingStyle, reply/mark-as-read Actions |
| Android Auto Add Support | https://developer.android.com/training/cars/apps/auto | Manifest, automotive_app_desc.xml, projection |
| Component Design Guidance | https://developer.android.com/design/ui/cars/guides/components/overview | Automotive HMI patterns |
| Car App Quality Guidelines | https://developer.android.com/docs/quality-guidelines/car-app-quality | Review criteria for MESSAGING category |
| Testing with DHU | https://developer.android.com/training/cars/testing | Desktop Head Unit setup and usage |
### Google I/O 2026 Announcements
| Resource | URL | Key Takeaways |
|----------|-----|---------------|
| Android for Cars: Unifying Platforms | https://android-developers.googleblog.com/2026/05/android-for-cars-unifying-platforms-premium-experiences.html | CAL 1.8.0 media templates, CAL 1.9.0 components, Material 3 Expressive, video support |
### Key API Patterns from Official Docs
#### Templated Messaging (from official guidance)
- **ConversationItem** auto-provides voice reply + mark-as-read actions
- Max **510 conversations**, each with ≤ **5 messages**
- Refresh cadence: ≤ **500ms** per invalidation
- Must also implement **notification-based messaging** (MessagingStyle) as fallback
- Distribution: Currently **internal + closed testing** tracks only (production opening later)
#### Manifest Requirements
```xml
<!-- automotive_app_desc.xml for templated messaging -->
<automotiveApp>
<uses name="notification" />
<uses name="template" />
</automotiveApp>
<!-- CarAppService intent filter -->
<service android:name=".MeshtasticCarAppService" android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.MESSAGING" />
</intent-filter>
</service>
<!-- Minimum Car API Level -->
<meta-data android:name="androidx.car.app.minCarApiLevel" android:value="8" />
```
#### ConversationItem Pattern (from official sample)
```kotlin
ConversationItem.Builder()
.setConversationCallback(callback)
.setId(conversation.id)
.setTitle(conversation.title)
.setIcon(conversation.icon)
.setMessages(carMessages)
.setSelf(selfPerson)
.setGroupConversation(conversation.isGroup)
.build()
```
### Related In-Flight Features
| Feature | Branch | Spec | Relationship |
|---------|--------|------|-------------|
| App Functions | `jamesarich/crispy-barnacle` | `specs/20260521-091500-app-functions/` | Provides "Hey Google" system AI integration for sendMessage, getMeshStatus, listNodes, getRecentMessages, getNodePosition — complementary to CAL voice input |
#### Shared Infrastructure from AppFunctions
- **`AiFunctionProvider`** interface in `core/data/commonMain` — platform-agnostic contract for AI-driven operations
- **`FuzzyNameResolver`** in `core/data/commonMain` — LCS-based node/channel name matching (50% threshold)
- **`RateLimiter`** in `core/data/commonMain` — sliding window rate limiter (5 calls/60s) for mesh airtime protection
- **Architecture pattern:** Thin Android wrappers (`androidApp/src/google/`) calling shared business logic
#### Integration Points
- Car module reuses `FuzzyNameResolver` for voice reply targeting (e.g., "reply to James" → resolve to node)
- `RateLimiter` can protect car-originated sends from exceeding mesh airtime
- AppFunctions "Hey Google" commands work on car displays automatically (system-level, no car module code needed)
- Both features share: `NodeRepository`, `CommandSender`, `RadioConfigRepository`, `PacketRepository`
### CAL 1.9.0-alpha01 Component Reference
| Component | API Class | Min Car API | Use in Meshtastic |
|-----------|-----------|-------------|-------------------|
| Spotlight Section | `SpotlightSection.Builder()` | 8 | Emergency messages pinned at top |
| Condensed Items | `CondensedItem.Builder()` | 8 | Dense node list (6+ visible) |
| Chips | `Chip.Builder()` | 8 | Channel switching + unread badges |
| Minimized Control Panel | `SectionedItemTemplate` | 8 | Persistent mesh status strip |
| Banners | `Banner.Builder()` | 8 | Emergency overlay + disconnection alerts |
| Section Headers | `SectionHeader.Builder()` | 8 | Message grouping by channel |
| Expanded Header Layout | `Header.Builder()` | 8 | Mesh topology summary (node dashboard) |
### Distribution Constraints (as of May 2026)
- **Templated messaging apps:** Internal + closed testing tracks only on Play Store
- **Production track:** Not yet open for templated messaging category
- **AAOS:** Separate distribution channel (OEM app stores or Play for Automotive)
- **F-Droid:** Excluded (CAL requires Google Play Services)
- **Timeline:** Production track expected to open "later" per Google (no firm date)
### Cross-Platform Parity: Meshtastic-Apple CarPlay
**Source:** `Meshtastic-Apple/Meshtastic/CarPlay/` (main branch, May 21, 2026)
**Apple CarPlay features (shipped):**
- Two-tab UI: Channels + Direct Messages (with Favorites/Recent sections)
- SiriKit voice compose/read-back via `INSendMessageIntent`
- Unread badges per channel and per DM
- "Not Connected" graceful degradation
- Live Activity (Dynamic Island) with node telemetry stats
- Batch donation of 50 unread messages on session start
- 300ms debounced refresh (updateSections, not rebuild)
- Message search via `INSearchForMessagesIntent`
- Message filtering: no emoji-only, no admin messages
- 200-byte message limit enforcement
**Parity decisions incorporated into this spec:**
- FR-017: Message filtering (emoji/admin exclusion) — matches Apple
- FR-018: Message size limit enforcement — matches Apple (237 bytes for Meshtastic)
- FR-019: Conversation caps (10 convos, 5 msgs each) — per Android guidance
- FR-020: Favorites section grouping — matches Apple's Favorites/Recent pattern
- FR-021: Session start unread batch load — matches Apple's 50-message donation
- FR-022: Notification-based messaging fallback — required per Android templated messaging docs
- NFR-010: Refresh debouncing (≥300ms) — matches Apple's proven 300ms debounce
- NFR-011: Refresh latency (≤500ms) — matches Apple's observed performance
**Android-exclusive features (exceeding Apple):**
- Node dashboard with Condensed Items (Apple has no node visibility)
- Emergency Banner overlays with audio alerts (Apple shows emergencies as regular messages)
- Map integration via PlaceListMapTemplate (Apple has no map)
- Channel Chips for instant switching (Apple requires tab navigation)
- Quick-reply templates (Apple only offers Siri voice)
- Visual hierarchy via Spotlight/Section Headers/Expanded Headers
- Persistent Minimized Control Panel (Apple uses separate Live Activity)
**Deferred to v2 (Apple has, we don't yet):**
- Message search (SearchTemplate or via AppFunctions)
- Live Activity equivalent (Android ongoing notification with mesh telemetry)

View File

@@ -0,0 +1,273 @@
# Tasks: Car App Library Integration
**Input**: Design documents from `/specs/20260521-153452-car-app-library-integration/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
**Tests**: Not explicitly requested in spec. Test tasks omitted per template rules.
**Verification**: Constitution-required validation (spotlessCheck, detekt, compile/test) included in final phase.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Setup (Project Initialization)
**Purpose**: Create the `feature/car` module structure, Gradle configuration, and version catalog entries
- [ ] T001 Add Car App Library version catalog entries in gradle/libs.versions.toml (car-app version, 4 library entries)
- [ ] T002 Add `include(":feature:car")` to settings.gradle.kts
- [ ] T003 Create module build file at feature/car/build.gradle.kts with android-library, flavors, koin plugins, and all dependencies per contracts/manifest-declarations.md
- [ ] T004 [P] Add `"googleImplementation"(projects.feature.car)` dependency in androidApp/build.gradle.kts
- [ ] T005 [P] Create AndroidManifest.xml at feature/car/src/main/AndroidManifest.xml with CarAppService, MESSAGING+POI categories, and minCarApiLevel 8 meta-data
- [ ] T006 [P] Create AAOS descriptor at feature/car/src/main/res/xml/automotive_app_desc.xml
- [ ] T007 [P] Create car-specific strings file at feature/car/src/main/res/values/strings.xml with initial string resources
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that ALL user stories depend on — service entry point, session lifecycle, DI, utilities
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
- [ ] T008 Create Koin DI module at feature/car/src/main/kotlin/org/meshtastic/feature/car/di/FeatureCarModule.kt declaring MeshtasticCarSession (factory), EmergencyHandler (singleton), CrashlyticsCarTagger (singleton)
- [ ] T009 Register FeatureCarModule in androidApp google flavor Koin configuration (androidApp/src/google/ Koin app module graph)
- [ ] T010 [P] Create CrashlyticsCarTagger utility at feature/car/src/main/kotlin/org/meshtastic/feature/car/util/CrashlyticsCarTagger.kt implementing car_session custom key set/clear
- [ ] T011 [P] Create TemplateBuilders helper extensions at feature/car/src/main/kotlin/org/meshtastic/feature/car/util/TemplateBuilders.kt with reusable CAL template construction helpers
- [ ] T012 Create MeshtasticCarAppService at feature/car/src/main/kotlin/org/meshtastic/feature/car/service/MeshtasticCarAppService.kt extending CarAppService, creating sessions via Koin
- [ ] T013 Create MeshtasticCarSession at feature/car/src/main/kotlin/org/meshtastic/feature/car/service/MeshtasticCarSession.kt with onCreateScreen (returns HomeScreen), onNewIntent, onCarConfigurationChanged, Crashlytics tagging, 300ms invalidation debouncing
- [ ] T014 Create presentation state models (CarSessionState, ConnectionStatus, MessagingUiState, ChannelUi, ConversationUi, NodeDashboardUiState, NodeUi, SignalQuality, TopologyHeader, MapUiState, NodePlace, LatLngWrapper, EmergencyAlert) at feature/car/src/main/kotlin/org/meshtastic/feature/car/model/CarUiModels.kt
- [ ] T015 Create HomeScreen (TabTemplate with Messages/Nodes/Map tabs) at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/HomeScreen.kt
**Checkpoint**: Foundation ready — CarAppService binds, session creates, HomeScreen renders tabs. User story implementation can now begin in parallel.
---
## Phase 3: User Story 1 — Read and Reply to Mesh Messages While Driving (Priority: P1) 🎯 MVP
**Goal**: Drivers can view incoming mesh messages grouped by channel and reply via voice or quick-reply templates
**Independent Test**: Send a message from a second Meshtastic device → appears on car display within 3s → dictate voice reply → arrives on sender's device
### Implementation for User Story 1
- [ ] T016 [P] [US1] Create MessagingScreen at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/MessagingScreen.kt with ListTemplate, channel Chips header, Section Headers grouping conversations, ConversationItem list (max 10), 300ms debounced invalidation, favorites/recent DM grouping
- [ ] T017 [P] [US1] Create ConversationScreen at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/ConversationScreen.kt with MessageTemplate showing messages (max 5 per conversation), voice reply action via CAL built-in ConversationItem voice input, quick-reply action list from QuickChatActionRepository, read-aloud TTS action
- [ ] T018 [US1] Reuse `FuzzyNameResolver` from `core/data/commonMain` (shared with AppFunctions feature) for voice-initiated DM node name matching — inject via Koin from existing `core/data` module. If AppFunctions branch not yet merged, temporarily duplicate LCS algorithm in feature/car/src/main/kotlin/org/meshtastic/feature/car/util/FuzzyNodeNameResolver.kt with TODO to consolidate post-merge
- [ ] T019 [US1] Implement message filtering logic in MessagingScreen — exclude emoji-only and admin messages from display (FR-017), enforce 237-byte outgoing limit with user feedback (FR-018)
- [ ] T020 [US1] Implement session-start batch loading of up to 50 unread messages in MeshtasticCarSession (FR-021) and post MessagingStyle notifications for read-back support
- [ ] T021 [US1] Implement notification-based messaging (NotificationCompat.MessagingStyle with reply and mark-as-read Actions) at feature/car/src/main/kotlin/org/meshtastic/feature/car/service/CarNotificationManager.kt (FR-022)
**Checkpoint**: Messaging fully functional — driver can see messages, switch channels, voice reply, use quick-reply templates, and receive MessagingStyle notifications
---
## Phase 4: User Story 2 — Emergency Alert Reception (Priority: P1)
**Goal**: Emergency broadcasts immediately surface as prominent banners with audio alerts regardless of active screen
**Independent Test**: Trigger emergency broadcast from test device → banner appears within 1s → audio alert plays → tap shows full details in Spotlight Section
### Implementation for User Story 2
- [ ] T022 [P] [US2] Create EmergencyHandler at feature/car/src/main/kotlin/org/meshtastic/feature/car/alerts/EmergencyHandler.kt observing PacketRepository flow for emergency-priority packets, triggering Banner via AppManager.showAlert(), managing active emergency list, stacking multiple banners chronologically
- [ ] T023 [US2] Implement emergency audio alert playback in EmergencyHandler using NotificationManager on USAGE_NOTIFICATION audio channel (NFR-008), not media channel
- [ ] T024 [US2] Integrate Spotlight Section in MessagingScreen for active emergencies — display EmergencyAlert items at top of messaging list when activeEmergencies is non-empty (FR-006). **Depends on T016 (MessagingScreen must exist first)**
- [ ] T025 [US2] Wire EmergencyHandler into MeshtasticCarSession lifecycle — start collecting on onCreateScreen, stop on session destroy
**Checkpoint**: Emergency alerts fully operational — banners overlay any screen within 1s, audio plays, Spotlight Section shows in messaging view
---
## Phase 5: User Story 3 — Monitor Node Network Status (Priority: P2)
**Goal**: Driver views all mesh nodes as a dense Condensed Items grid with signal/battery metrics and topology header
**Independent Test**: Have 3+ nodes in range → open node dashboard → all nodes displayed with correct signal/battery → tap node → detail view shows full info
### Implementation for User Story 3
- [ ] T026 [P] [US3] Create NodeDashboardScreen at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDashboardScreen.kt with ListTemplate, Expanded Header Layout (mesh topology summary: online/total), Condensed Items for each node (name, signal quality, battery), online-first sorting with offline dimmed at bottom
- [ ] T027 [P] [US3] Create NodeDetailScreen at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/NodeDetailScreen.kt with PaneTemplate showing last heard, distance, hardware model, battery, SNR, and "Message" action to push ConversationScreen for DM
**Checkpoint**: Node dashboard shows 6+ nodes without scrolling via Condensed Items, detail drill-down works, DM action connects to messaging
---
## Phase 6: User Story 4 — Switch Between Channels (Priority: P2)
**Goal**: Single-tap channel switching via Chips with unread badges at the top of the messaging screen
**Independent Test**: Configure 3+ channels → messaging screen shows channel chips → tap chip → message list updates within 1s → unread badge visible on channels with new messages
### Implementation for User Story 4
- [ ] T028 [US4] Implement channel Chip actions with unread badge indicators in MessagingScreen header — single-tap switches selectedChannelIndex, triggers message list re-filter within 1s (FR-008, FR-016)
**Checkpoint**: Channel chips render with unread counts, tapping switches view to that channel's conversations immediately
---
## Phase 7: User Story 5 — View Node Locations on Map (Priority: P2)
**Goal**: Nodes with GPS positions displayed as place items on a PlaceListMapTemplate with auto-zoom and detail drill-down
**Independent Test**: Have 2+ nodes reporting GPS → open map → pins at correct locations → tap list item → node detail with distance and DM option
### Implementation for User Story 5
- [ ] T029 [US5] Create MapScreen at feature/car/src/main/kotlin/org/meshtastic/feature/car/screens/MapScreen.kt with PlaceListMapTemplate under POI category, node Place items with LatLng from NodeRepository (filtered to valid positions), distance + last update in row text, own position as anchor, onClickListener pushing NodeDetailScreen, 5-second refresh interval. **Cap at 6 Place items per CAL PlaceListMapTemplate limit — prioritize by distance (nearest first), then recency**
**Checkpoint**: Map displays node pins, auto-zooms to fit, list items show distance, tap navigates to node detail
---
## Phase 8: User Story 6 — Persistent Mesh Status at a Glance (Priority: P3)
**Goal**: Minimized Control Panel visible across all screens showing radio status, node count, last message time
**Independent Test**: Navigate between all screens → mini-panel always visible → shows correct node count → disconnect radio → panel shows "Disconnected"
### Implementation for User Story 6
- [ ] T030 [US6] Create MeshStatusPanel at feature/car/src/main/kotlin/org/meshtastic/feature/car/panels/MeshStatusPanel.kt implementing Minimized Control Panel — connectionStatusIcon, "{N} nodes online" title, "Last msg: {timeAgo}" subtitle, onClickListener expanding to full detail (mesh name, own battery, firmware version)
- [ ] T031 [US6] Register MeshStatusPanel in MeshtasticCarSession lifecycle — attach to session on creation, observe BleConnectionState + NodeRepository for live updates, show "Disconnected" with warning icon on radio disconnect (FR-010, FR-011)
**Checkpoint**: Persistent mini-panel visible across all screens, updates in real-time, expands on tap
---
## Phase 9: User Story 7 — In-Context Voice Input for Actions (Priority: P3)
**Goal**: Voice reply is the default composition method, TTS reads messages aloud, FuzzyNodeNameResolver handles voice-initiated DMs
**Independent Test**: Tap reply → dictate → message sent → tap "read aloud" → TTS reads message with sender name
### Implementation for User Story 7
- [ ] T032 [US7] Implement TTS read-aloud action in ConversationScreen using Android built-in TTS engine — reads sender name + message content on tap of "Read Aloud" action
- [ ] T033 [US7] Wire FuzzyNodeNameResolver into node detail "Message" action flow — when initiating DM from NodeDashboard, voice input is default composition method with resolved node context
**Checkpoint**: Voice reply works end-to-end, TTS reads messages clearly, node-initiated DMs use voice by default
---
## Phase 10: Polish & Cross-Cutting Concerns
**Purpose**: Error handling, degraded states, compliance, and verification
- [ ] T034 [P] Implement BLE disconnection Banner + graceful degradation to cached read-only data across all screens (FR-011, FR-015)
- [ ] T035 [P] Implement empty/error states: no channels configured → onboarding PaneTemplate, no nodes → "No nodes heard", no positions → "No positions reported" (per error contracts)
- [ ] T036 [P] Add ProGuard/R8 keep rule for MeshtasticCarAppService in feature/car/proguard-rules.pro
- [ ] T037 [P] Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
- [ ] T038 [P] Review all screens against automotive HMI distraction guidelines — verify ≤ 2 taps for all primary actions (NFR-001)
- [ ] T039 Run constitution-required verification: `./gradlew spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:testGoogleDebugUnitTest`
- [ ] T040 Validate quickstart.md developer workflow documentation is accurate for the implemented module
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Setup)**: No dependencies — start immediately
- **Phase 2 (Foundational)**: Depends on Phase 1 — BLOCKS all user stories
- **Phase 3 (US1 - Messaging)**: Depends on Phase 2 — MVP target
- **Phase 4 (US2 - Emergency)**: Depends on Phase 2; integrates with MessagingScreen (Phase 3 T016)
- **Phase 5 (US3 - Nodes)**: Depends on Phase 2 — independent of messaging
- **Phase 6 (US4 - Channels)**: Depends on Phase 3 (modifies MessagingScreen)
- **Phase 7 (US5 - Map)**: Depends on Phase 5 (reuses NodeDetailScreen from T027)
- **Phase 8 (US6 - Status Panel)**: Depends on Phase 2 — independent
- **Phase 9 (US7 - Voice)**: Depends on Phase 3 (ConversationScreen T017, FuzzyNodeNameResolver T018)
- **Phase 10 (Polish)**: Depends on all user story phases
### User Story Dependencies
- **US1 (Messaging, P1)**: Can start after Phase 2 — no other story dependencies
- **US2 (Emergency, P1)**: Can start after Phase 2 — integrates with US1's MessagingScreen (T016) for Spotlight Section (T024)
- **US3 (Nodes, P2)**: Can start after Phase 2 — fully independent
- **US4 (Channels, P2)**: Depends on US1 (extends MessagingScreen)
- **US5 (Map, P2)**: Depends on US3 (reuses NodeDetailScreen)
- **US6 (Status Panel, P3)**: Can start after Phase 2 — fully independent
- **US7 (Voice, P3)**: Depends on US1 (extends ConversationScreen)
### Within Each User Story
- State models → Screen implementation → Integration logic
- Screens before cross-screen wiring
- Core implementation before refinement
### Parallel Opportunities
- **Phase 1**: T004, T005, T006, T007 can all run in parallel
- **Phase 2**: T010, T011 in parallel; T014 parallel with T010/T011
- **After Phase 2**: US1, US3, and US6 can start simultaneously (independent)
- **Within US1**: T016 and T017 in parallel (different files)
- **Within US2**: T022 independent of other stories
- **Within US3**: T026 and T027 in parallel (different files)
- **Phase 10**: T034, T035, T036, T037, T038 all in parallel
---
## Parallel Example: After Foundational Phase
```bash
# Three stories can start simultaneously:
# Developer A: US1 (Messaging)
Task: T016 "Create MessagingScreen"
Task: T017 "Create ConversationScreen"
# Developer B: US3 (Nodes)
Task: T026 "Create NodeDashboardScreen"
Task: T027 "Create NodeDetailScreen"
# Developer C: US6 (Status Panel)
Task: T030 "Create MeshStatusPanel"
Task: T031 "Register panel in session"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup (T001T007)
2. Complete Phase 2: Foundational (T008T015)
3. Complete Phase 3: User Story 1 — Messaging (T016T021)
4. **STOP and VALIDATE**: Test messaging end-to-end with DHU
5. Deploy to internal testing track if ready
### Incremental Delivery
1. Setup + Foundational → Module compiles and binds to Android Auto
2. Add US1 (Messaging) → Core value delivered (MVP!)
3. Add US2 (Emergency) → Safety-critical alerts operational
4. Add US3 + US5 (Nodes + Map) → Location awareness complete
5. Add US4 (Channels) → Multi-channel workflows enabled
6. Add US6 + US7 (Panel + Voice) → Polish and hands-free refinement
7. Each increment is independently testable with the Desktop Head Unit (DHU)
### Parallel Team Strategy
With multiple developers after Phase 2:
- Developer A: US1 (Messaging) → US4 (Channels) → US7 (Voice)
- Developer B: US3 (Nodes) → US5 (Map)
- Developer C: US2 (Emergency) + US6 (Status Panel)
---
## Notes
- All screens use `invalidate()` for refresh (never recreate Screen objects) per NFR-010
- 300ms debounce on all invalidation triggers per NFR-010
- CAL host enforces distraction guidelines — app provides templates only
- Existing `core/` modules consumed read-only via Koin DI — no API changes
- Google flavor only — F-Droid builds unaffected
- Car API Level 8 minimum — older hosts gracefully hide the app