Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f5ccea9
feat(specs): Car App Library 1.9.0-alpha01 integration specification
jamesarich May 21, 2026
ed3942f
feat(specs): defer map implementation pending NAVIGATION vs POI decision
jamesarich May 21, 2026
b932a22
fix(specs): remove 23 stale map/POI references after deferral
jamesarich May 21, 2026
a4360dd
feat(car): add Phase 2 utilities, models, and HomeScreen
jamesarich May 21, 2026
0a8156f
feat(car): implement Phase 3 Messaging MVP (T016-T021)
jamesarich May 21, 2026
9c3c918
feat(car): implement feature/car module with Car App Library 1.9.0-al…
jamesarich May 21, 2026
b880fb5
feat(car): wire CarStateCoordinator to session and HomeScreen
jamesarich May 21, 2026
fe02f18
feat(car): add lifecycle-aware screen invalidation on state changes
jamesarich May 21, 2026
dc11e3b
test(car): add unit tests for MessageFilter and FuzzyNodeNameResolver
jamesarich May 21, 2026
d6bed2b
feat(car): complete data wiring — conversations, TTS, onboarding, dis…
jamesarich May 21, 2026
e5a3747
docs(car): mark all implementation tasks complete
jamesarich May 21, 2026
865a398
fix(car): address code review findings — lifecycle, security, thread …
jamesarich May 22, 2026
fa758f7
style(car): add Meshtastic branding — icons, localized strings, visua…
jamesarich May 22, 2026
cd54ba2
refactor(car): consolidate shared utilities — eliminate duplicated logic
jamesarich May 22, 2026
b33908a
style(car): align visual patterns with main app design system
jamesarich May 22, 2026
e6022d5
style(car): apply official Car App Library sample patterns
jamesarich May 22, 2026
23fc67e
feat(car): add colored node chips via ForegroundCarColorSpan
jamesarich May 22, 2026
514c930
refactor(car): move node color to icon tint, extract nodeColorsFromNum
jamesarich May 22, 2026
7e7db4d
fix(car): address security and spec verification findings
jamesarich May 23, 2026
b996a0c
feat(car): implement Phase 11 advanced CAL APIs
jamesarich May 23, 2026
2205a89
docs: mark Phase 11 tasks complete
jamesarich May 23, 2026
969715d
fix(car): address review issues in feature/car module
jamesarich May 23, 2026
c35c23d
docs: resolve car integration spec quality checklist (65/65 items)
jamesarich May 23, 2026
1c17b8d
feat(car): implement PR #5162 recommendations + official docs alignment
jamesarich May 28, 2026
87ae58a
refactor(car): remove dead code and fix scope leak in CarReplyReceiver
jamesarich May 28, 2026
203e472
feat(car): show DM conversations instead of favorites, wire Message N…
jamesarich May 28, 2026
225b44c
chore(car): remove unused quickChatActions, fix hardcoded string
jamesarich May 28, 2026
c6a26fa
fix(car): resolve detekt failure in node colors
jamesarich May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions .agent_memory/session_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,45 @@
# Do NOT edit or remove previous entries — stale state claims cause agent confusion.
# Format: ## YYYY-MM-DD — <summary>

## 2026-05-28 — Stabilized DatabaseManager withDb retry host test
- Hardened `DatabaseManagerWithDbRetryTest` to remove CI race conditions by running the manager on a `StandardTestDispatcher(testScheduler)` instead of real `Dispatchers.IO`.
- Added a `withTimeout(10_000)` guard around the test body to fail fast on coordination stalls instead of hanging/flapping.
- Kept the deterministic retry trigger (`error("Connection pool is closed")`) and retained assertions that first attempt uses old DB and retry uses current DB.
- Made teardown resilient with `if (::manager.isInitialized) manager.close()` so setup/early failures do not cascade into teardown crashes.
- Verified with `:core:database:jvmTest --tests "org.meshtastic.core.database.DatabaseManagerWithDbRetryTest*"` and repeated it 5 consecutive runs without failures; `:core:database:detekt` also passed.
## 2026-05-28 — Added comprehensive CarScreenDataBuilder unit coverage
- Created `feature/car/src/test/kotlin/org/meshtastic/feature/car/util/CarScreenDataBuilderTest.kt` with 533 lines covering signal quality thresholds/boundaries, node UI mapping, node and conversation sorting, local stats fallbacks, uptime formatting, recent message limiting, contact key generation, and constants.
- Restored the `MessageSnapshot` data class in `CarStateCoordinator.kt` and re-added `recentMessages()` plus `MAX_CONVERSATION_MESSAGES` in `CarScreenDataBuilder.kt` so the current source matched the requested pure-helper API surface for testing.
- Verified with `./gradlew :feature:car:spotlessCheck :feature:car:detekt :feature:car:testFdroidDebugUnitTest --quiet` and the requested quiet test command (`./gradlew :feature:car:testFdroidDebugUnitTest --quiet 2>&1 | tail -20`), both successful.

## 2026-05-28 — Lowered car min API to 7 and removed dead conversation code
- Changed `feature/car` manifest `androidx.car.app.minCarApiLevel` metadata from 8 to 7.
- Guarded `HomeScreen.showEmergencyAlert()` behind `carContext.carAppApiLevel >= 8` and logged unsupported API 7 hosts with Kermit.
- Removed unused `ConversationScreen`, `CarTtsEngine`, message snapshot/cache/read-aloud plumbing, and now-unused car reply/read-aloud strings.
- Simplified `CarStateCoordinator` and `CarScreenDataBuilder` to match the inline `ConversationItem` flow.
- Verified with `./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:compileFdroidDebugKotlin --quiet 2>&1 | tail -30`.

## 2026-05-28 — Migrated car home messages tab to ConversationItem
- Reworked `feature/car` `HomeScreen` messaging tab to build CAL `ConversationItem` entries instead of browsable `Row`s, including `Person`/`CarMessage` helpers and native reply/mark-read callbacks.
- Removed `HomeScreen` conversation navigation so the car host owns messaging affordances; `ConversationScreen` remains on disk for later cleanup phases.
- Added `CarStateCoordinator.markAsRead()` using `packetRepository.clearUnreadCount(...)` with Kermit error logging via `runCatching`.
- Verified with `./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:compileFdroidDebugKotlin` and the requested quiet compile command (`:feature:car:compileFdroidDebugKotlin --quiet 2>&1 | tail -20`), both successful.

## 2026-05-28 — Implemented car conversation shortcuts and avatars
- Added `feature/car/.../util/PersonIconFactory.kt` to render circular initial avatars using node-derived foreground/background colors for `Person` and shortcut icons.
- Added `feature/car/.../service/ConversationShortcutManager.kt` to publish long-lived dynamic conversation shortcuts for favorite nodes and active channels, plus on-demand shortcut creation for notifications.
- Wired `MeshtasticCarSession` to start/stop shortcut observation on a dedicated session coroutine scope.
- Updated `CarNotificationManager` to ensure conversation shortcuts exist before posting and to attach both `shortcutId` and `LocusIdCompat` to messaging notifications.
- Verified green with `./gradlew :feature:car:spotlessCheck :feature:car:detekt --quiet` and `./gradlew :feature:car:compileFdroidDebugKotlin --quiet 2>&1 | tail -20` after workspace bootstrap.

## 2026-05-28 — Implemented car local stats tab and extracted screen data builder
- Added `CarLocalStats` to `feature/car` UI models and exposed `localStatsState` from `CarStateCoordinator`.
- Wired a new HomeScreen `Status` tab with battery, channel utilization, air utilization, node counts, uptime, and packet TX/RX rows.
- Created `feature/car/.../util/CarScreenDataBuilder.kt` to centralize pure UI-model mapping helpers for nodes, conversations, local stats, uptime formatting, contact key building, and recent message selection.
- Added the new `ic_car_status.xml` drawable plus status strings in `feature/car/src/main/res/values/strings.xml`.
- Cleaned up `CarReplyReceiver` detekt violations that blocked module validation.
- Ran `python3 scripts/sort-strings.py` and verified green with `./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:compileFdroidDebugKotlin :feature:car:testFdroidDebugUnitTest`.

## 2026-05-28 — Implemented car module Phase 1 messaging wiring fixes
- Replaced `CommandSender` usage in `feature/car` `CarStateCoordinator` with injected `SendMessageUseCase`, keeping the public `sendMessage()` API synchronous for UI callbacks while launching the use case on the coordinator scope after message-length validation.
- Updated `CarNotificationManager` reply and mark-read notification actions with semantic action metadata and `setShowsUserInterface(false)` for automotive-friendly inline handling.
- Reworked `CarReplyReceiver` into a `KoinComponent` that injects `SendMessageUseCase` and `PacketRepository`, then sends replies / clears unread counts asynchronously with Kermit error logging.
- Added `android:permission="androidx.car.app.CarAppService"` to the `MeshtasticCarAppService` manifest declaration.
- Verified with `./gradlew :feature:car:compileFdroidDebugKotlin --quiet` after required workspace bootstrap.

## 2026-05-21 — Upgraded Chirpy to a fully-personalized Live Diagnostic Node & Mesh Assistant
- Integrated `NodeRepository` into `GeminiNanoDocAssistant.kt` and the Google AI Koin dependency injection module (`GoogleAiModule.kt`).
Expand Down
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Any session that passes ~10-12 turns without compaction should be compacted. Con

<!-- 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 -->

4 changes: 3 additions & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{"feature_directory":"specs/20260520-153412-nav-tab-labels"}
{
"feature_directory": "specs/20260521-153452-car-app-library-integration"
}
1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.glance.preview)

googleImplementation(projects.feature.car)
googleImplementation(libs.location.services)
googleImplementation(libs.play.services.maps)
googleImplementation(libs.maps.compose)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("ktlint:standard:max-line-length")

package org.meshtastic.app.di

import org.koin.core.annotation.Module
import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
import org.meshtastic.feature.car.di.FeatureCarModule

@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class, GoogleAiModule::class])
@Module(
includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class, GoogleAiModule::class, FeatureCarModule::class],
)
class FlavorModule
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,7 @@ data class Node(
get() = lastHeard > onlineTimeThreshold()

val colors: Pair<Int, Int>
get() { // returns foreground and background @ColorInt for each 'num'
val r = (num and 0xFF0000) shr 16
val g = (num and 0x00FF00) shr 8
val b = num and 0x0000FF
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
val foreground = if (brightness > 0.5) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
val background = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
return foreground to background
}
get() = nodeColorsFromNum(num)

val isUnknownUser
get() = user.hw_model == HardwareModel.UNSET
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model

private const val RED_WEIGHT = 0.299
private const val GREEN_WEIGHT = 0.587
private const val BLUE_WEIGHT = 0.114
private const val BRIGHTNESS_THRESHOLD = 0.5
private const val MAX_CHANNEL = 255
private const val RED_MASK = 0xFF0000
private const val GREEN_MASK = 0x00FF00
private const val BLUE_MASK = 0x0000FF
private const val ALPHA_MASK = 0xFF
private const val RED_SHIFT = 16
private const val GREEN_SHIFT = 8
private const val ALPHA_SHIFT = 24
private const val BLACK = 0xFF000000.toInt()
private const val WHITE = 0xFFFFFFFF.toInt()

/** Derives a unique color pair from a node number. Returns (foreground, background) as @ColorInt. */
fun nodeColorsFromNum(nodeNum: Int): Pair<Int, Int> {
val r = (nodeNum and RED_MASK) shr RED_SHIFT
val g = (nodeNum and GREEN_MASK) shr GREEN_SHIFT
val b = nodeNum and BLUE_MASK
val brightness = ((r * RED_WEIGHT) + (g * GREEN_WEIGHT) + (b * BLUE_WEIGHT)) / MAX_CHANNEL
val foreground = if (brightness > BRIGHTNESS_THRESHOLD) BLACK else WHITE
val background = (ALPHA_MASK shl ALPHA_SHIFT) or (r shl RED_SHIFT) or (g shl GREEN_SHIFT) or b
return foreground to background
}
57 changes: 57 additions & 0 deletions feature/car/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.flavors)
id("meshtastic.koin")
}

android {
namespace = "org.meshtastic.feature.car"

buildFeatures { buildConfig = true }

defaultConfig {
minSdk = 23
consumerProguardFiles("proguard-rules.pro")
}
}

dependencies {
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.repository)

implementation(libs.androidx.car.app)
implementation(libs.androidx.car.app.projected)

implementation(libs.koin.android)
implementation(libs.koin.annotations)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
implementation(libs.kermit)

testImplementation(libs.androidx.car.app.testing)
testImplementation(libs.koin.test)
testImplementation(kotlin("test-junit"))
testRuntimeOnly(libs.junit.vintage.engine)
}
9 changes: 9 additions & 0 deletions feature/car/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Car App Library ProGuard/R8 rules

# CarAppService must not be obfuscated (resolved by android:exported="true" in manifest,
# but keep rule ensures R8 doesn't remove it during aggressive shrinking)
-keep class org.meshtastic.feature.car.service.MeshtasticCarAppService { *; }

# Keep Koin-annotated classes for runtime DI resolution
-keep @org.koin.core.annotation.Single class * { *; }
-keep @org.koin.core.annotation.Factory class * { *; }
23 changes: 23 additions & 0 deletions feature/car/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<service
android:name="org.meshtastic.feature.car.service.MeshtasticCarAppService"
android:exported="true"
android:permission="androidx.car.app.CarAppService">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.MESSAGING" />
</intent-filter>
</service>

<receiver
android:name="org.meshtastic.feature.car.service.CarReplyReceiver"
android:exported="false" />

<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="7" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.car.alerts

import android.media.AudioManager
import android.media.ToneGenerator
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.feature.car.model.EmergencyAlert

/**
* Manages emergency alert state for the car display. Observes incoming packets for emergency-priority messages,
* maintains active alert list, and triggers audio notifications.
*/
@Single
class EmergencyHandler {

private var scope: CoroutineScope? = null
private val _activeAlerts = MutableStateFlow<List<EmergencyAlert>>(emptyList())
val activeAlerts: StateFlow<List<EmergencyAlert>> = _activeAlerts.asStateFlow()

private val _latestAlert = MutableStateFlow<EmergencyAlert?>(null)
val latestAlert: StateFlow<EmergencyAlert?> = _latestAlert.asStateFlow()

private var toneGenerator: ToneGenerator? = null

private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Logger.e(tag = "EmergencyHandler", throwable = throwable) { "Emergency flow collection failed" }
}

fun startCollecting(emergencyFlow: Flow<EmergencyAlert>) {
scope?.cancel()
scope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + exceptionHandler).also { newScope ->
newScope.launch {
emergencyFlow.collect { alert ->
addAlert(alert)
_latestAlert.value = alert
playEmergencyTone()
}
}
}
}

fun stopCollecting() {
scope?.cancel()
scope = null
toneGenerator?.release()
toneGenerator = null
}

fun dismissAlert(nodeNum: Int) {
_activeAlerts.value =
_activeAlerts.value.map { alert -> if (alert.nodeNum == nodeNum) alert.copy(isActive = false) else alert }
}

fun clearAll() {
_activeAlerts.value = emptyList()
}

private fun addAlert(alert: EmergencyAlert) {
val current = _activeAlerts.value.toMutableList()
// Replace existing alert from same node, or add new
val existingIndex = current.indexOfFirst { it.nodeNum == alert.nodeNum }
if (existingIndex >= 0) {
current[existingIndex] = alert
} else {
current.add(0, alert) // newest first
}
_activeAlerts.value = current
}

@Suppress("TooGenericExceptionCaught") // ToneGenerator may throw various runtime exceptions
private fun playEmergencyTone() {
try {
if (toneGenerator == null) {
toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, TONE_VOLUME)
}
toneGenerator?.startTone(ToneGenerator.TONE_PROP_BEEP, TONE_DURATION_MS)
} catch (e: RuntimeException) {
Logger.w(tag = "EmergencyHandler", throwable = e) { "Emergency tone playback failed" }
}
}

companion object {
private const val TONE_VOLUME = 80
private const val TONE_DURATION_MS = 1000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.car.di

import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module

@Module
@ComponentScan("org.meshtastic.feature.car")
class FeatureCarModule
Loading