mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-24 23:01:22 -04:00
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:
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -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 -->
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
{"feature_directory":"specs/20260520-153412-nav-tab-labels"}
|
||||
{
|
||||
"feature_directory": "specs/20260521-153452-car-app-library-integration"
|
||||
}
|
||||
|
||||
@@ -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-002–FR-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
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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 { *; }
|
||||
```
|
||||
226
specs/20260521-153452-car-app-library-integration/data-model.md
Normal file
226
specs/20260521-153452-car-app-library-integration/data-model.md
Normal 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 |
|
||||
134
specs/20260521-153452-car-app-library-integration/plan.md
Normal file
134
specs/20260521-153452-car-app-library-integration/plan.md
Normal 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 |
|
||||
150
specs/20260521-153452-car-app-library-integration/quickstart.md
Normal file
150
specs/20260521-153452-car-app-library-integration/quickstart.md
Normal 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.
|
||||
164
specs/20260521-153452-car-app-library-integration/research.md
Normal file
164
specs/20260521-153452-car-app-library-integration/research.md
Normal 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
|
||||
460
specs/20260521-153452-car-app-library-integration/spec.md
Normal file
460
specs/20260521-153452-car-app-library-integration/spec.md
Normal 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 **5–10 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)
|
||||
273
specs/20260521-153452-car-app-library-integration/tasks.md
Normal file
273
specs/20260521-153452-car-app-library-integration/tasks.md
Normal 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 (T001–T007)
|
||||
2. Complete Phase 2: Foundational (T008–T015)
|
||||
3. Complete Phase 3: User Story 1 — Messaging (T016–T021)
|
||||
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
|
||||
Reference in New Issue
Block a user