Skip to content

Commit 0951720

Browse files
jamesarichCopilotclaude
authored
feat(car): Android Car App Library integration (#5633)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 781dcfa commit 0951720

51 files changed

Lines changed: 4878 additions & 18 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agent_memory/session_context.md

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,45 @@
1212
- Fix (NodeClusterMarkers.kt ONLY): icons baked in-scope via `rememberComposeBitmapDescriptor(node){ PulsingNodeChip }` into a snapshot stateMap; custom `private class NodeClusterRenderer : DefaultClusterRenderer` assigns them in onBeforeClusterItemRendered/onClusterItemUpdated (bg thread, READ-only — never composes, so the crash class is gone). Native info windows (super sets title/snippet) + onClusterItemInfoWindowClick→navigateToNodeDetails; precision circles drawn from the renderer's own `unclusteredItems` MutableState (clusterItemDecoration can't fire — `ClusterRendererItemState` is lib-internal). Strictly better than the elegant-euler Canvas branch — keeps the REAL Compose chip.
1313
- `compileGoogleDebugKotlin` + `spotlessCheck` + `detekt` PASS. NOT committed, NOT device-verified. Next: device-test (clusters show chips + info-window popups + no FATAL), then commit/push.
1414

15-
## 2026-05-28 — Stabilized DatabaseManager withDb retry host test
16-
- Hardened `DatabaseManagerWithDbRetryTest` to remove CI race conditions by running the manager on a `StandardTestDispatcher(testScheduler)` instead of real `Dispatchers.IO`.
17-
- Added a `withTimeout(10_000)` guard around the test body to fail fast on coordination stalls instead of hanging/flapping.
18-
- Kept the deterministic retry trigger (`error("Connection pool is closed")`) and retained assertions that first attempt uses old DB and retry uses current DB.
19-
- Made teardown resilient with `if (::manager.isInitialized) manager.close()` so setup/early failures do not cascade into teardown crashes.
20-
- Verified with `:core:database:jvmTest --tests "org.meshtastic.core.database.DatabaseManagerWithDbRetryTest*"` and repeated it 5 consecutive runs without failures; `:core:database:detekt` also passed.
15+
## 2026-05-28 — Added comprehensive CarScreenDataBuilder unit coverage
16+
- 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.
17+
- 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.
18+
- 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.
19+
20+
## 2026-05-28 — Lowered car min API to 7 and removed dead conversation code
21+
- Changed `feature/car` manifest `androidx.car.app.minCarApiLevel` metadata from 8 to 7.
22+
- Guarded `HomeScreen.showEmergencyAlert()` behind `carContext.carAppApiLevel >= 8` and logged unsupported API 7 hosts with Kermit.
23+
- Removed unused `ConversationScreen`, `CarTtsEngine`, message snapshot/cache/read-aloud plumbing, and now-unused car reply/read-aloud strings.
24+
- Simplified `CarStateCoordinator` and `CarScreenDataBuilder` to match the inline `ConversationItem` flow.
25+
- Verified with `./gradlew :feature:car:spotlessApply :feature:car:spotlessCheck :feature:car:detekt :feature:car:compileFdroidDebugKotlin --quiet 2>&1 | tail -30`.
26+
27+
## 2026-05-28 — Migrated car home messages tab to ConversationItem
28+
- 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.
29+
- Removed `HomeScreen` conversation navigation so the car host owns messaging affordances; `ConversationScreen` remains on disk for later cleanup phases.
30+
- Added `CarStateCoordinator.markAsRead()` using `packetRepository.clearUnreadCount(...)` with Kermit error logging via `runCatching`.
31+
- 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.
32+
33+
## 2026-05-28 — Implemented car conversation shortcuts and avatars
34+
- Added `feature/car/.../util/PersonIconFactory.kt` to render circular initial avatars using node-derived foreground/background colors for `Person` and shortcut icons.
35+
- 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.
36+
- Wired `MeshtasticCarSession` to start/stop shortcut observation on a dedicated session coroutine scope.
37+
- Updated `CarNotificationManager` to ensure conversation shortcuts exist before posting and to attach both `shortcutId` and `LocusIdCompat` to messaging notifications.
38+
- Verified green with `./gradlew :feature:car:spotlessCheck :feature:car:detekt --quiet` and `./gradlew :feature:car:compileFdroidDebugKotlin --quiet 2>&1 | tail -20` after workspace bootstrap.
39+
40+
## 2026-05-28 — Implemented car local stats tab and extracted screen data builder
41+
- Added `CarLocalStats` to `feature/car` UI models and exposed `localStatsState` from `CarStateCoordinator`.
42+
- Wired a new HomeScreen `Status` tab with battery, channel utilization, air utilization, node counts, uptime, and packet TX/RX rows.
43+
- 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.
44+
- Added the new `ic_car_status.xml` drawable plus status strings in `feature/car/src/main/res/values/strings.xml`.
45+
- Cleaned up `CarReplyReceiver` detekt violations that blocked module validation.
46+
- 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`.
47+
48+
## 2026-05-28 — Implemented car module Phase 1 messaging wiring fixes
49+
- 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.
50+
- Updated `CarNotificationManager` reply and mark-read notification actions with semantic action metadata and `setShowsUserInterface(false)` for automotive-friendly inline handling.
51+
- Reworked `CarReplyReceiver` into a `KoinComponent` that injects `SendMessageUseCase` and `PacketRepository`, then sends replies / clears unread counts asynchronously with Kermit error logging.
52+
- Added `android:permission="androidx.car.app.CarAppService"` to the `MeshtasticCarAppService` manifest declaration.
53+
- Verified with `./gradlew :feature:car:compileFdroidDebugKotlin --quiet` after required workspace bootstrap.
2154

2255
## 2026-05-21 — Upgraded Chirpy to a fully-personalized Live Diagnostic Node & Mesh Assistant
2356
- Integrated `NodeRepository` into `GeminiNanoDocAssistant.kt` and the Google AI Koin dependency injection module (`GoogleAiModule.kt`).

.github/copilot-instructions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ These are specific to the Copilot CLI environment and are not covered in AGENTS.
4141

4242
<!-- SPECKIT START -->
4343
For additional context about technologies to be used, project structure,
44-
shell commands, and other important information, read the current plan
44+
shell commands, and other important information, read the current plan at
45+
specs/20260521-153452-car-app-library-integration/plan.md
4546
<!-- SPECKIT END -->

.specify/feature.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
{"feature_directory":"specs/20260520-153412-nav-tab-labels"}
1+
{
2+
"feature_directory": "specs/20260521-153452-car-app-library-integration"
3+
}

androidApp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ dependencies {
267267
debugImplementation(libs.androidx.compose.ui.test.manifest)
268268
debugImplementation(libs.androidx.glance.preview)
269269

270+
googleImplementation(projects.feature.car)
270271
googleImplementation(libs.location.services)
271272
googleImplementation(libs.play.services.maps)
272273
googleImplementation(libs.maps.compose)

androidApp/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
* You should have received a copy of the GNU General Public License
1515
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
*/
17+
@file:Suppress("ktlint:standard:max-line-length")
18+
1719
package org.meshtastic.app.di
1820

1921
import org.koin.core.annotation.Module
2022
import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
23+
import org.meshtastic.feature.car.di.FeatureCarModule
2124

2225
@Module(
2326
includes =
24-
[GoogleNetworkModule::class, GoogleMapsKoinModule::class, GoogleAiModule::class, AppFunctionsModule::class],
27+
[
28+
GoogleNetworkModule::class,
29+
GoogleMapsKoinModule::class,
30+
GoogleAiModule::class,
31+
AppFunctionsModule::class,
32+
FeatureCarModule::class,
33+
],
2534
)
2635
class FlavorModule

core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,7 @@ data class Node(
7272
get() = lastHeard > onlineTimeThreshold()
7373

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

8577
val isUnknownUser
8678
get() = user.hw_model == HardwareModel.UNSET
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package org.meshtastic.core.model
18+
19+
private const val RED_WEIGHT = 0.299
20+
private const val GREEN_WEIGHT = 0.587
21+
private const val BLUE_WEIGHT = 0.114
22+
private const val BRIGHTNESS_THRESHOLD = 0.5
23+
private const val MAX_CHANNEL = 255
24+
private const val RED_MASK = 0xFF0000
25+
private const val GREEN_MASK = 0x00FF00
26+
private const val BLUE_MASK = 0x0000FF
27+
private const val ALPHA_MASK = 0xFF
28+
private const val RED_SHIFT = 16
29+
private const val GREEN_SHIFT = 8
30+
private const val ALPHA_SHIFT = 24
31+
private const val BLACK = 0xFF000000.toInt()
32+
private const val WHITE = 0xFFFFFFFF.toInt()
33+
34+
/** Derives a unique color pair from a node number. Returns (foreground, background) as @ColorInt. */
35+
fun nodeColorsFromNum(nodeNum: Int): Pair<Int, Int> {
36+
val r = (nodeNum and RED_MASK) shr RED_SHIFT
37+
val g = (nodeNum and GREEN_MASK) shr GREEN_SHIFT
38+
val b = nodeNum and BLUE_MASK
39+
val brightness = ((r * RED_WEIGHT) + (g * GREEN_WEIGHT) + (b * BLUE_WEIGHT)) / MAX_CHANNEL
40+
val foreground = if (brightness > BRIGHTNESS_THRESHOLD) BLACK else WHITE
41+
val background = (ALPHA_MASK shl ALPHA_SHIFT) or (r shl RED_SHIFT) or (g shl GREEN_SHIFT) or b
42+
return foreground to background
43+
}

feature/car/build.gradle.kts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
plugins {
19+
alias(libs.plugins.meshtastic.android.library)
20+
alias(libs.plugins.meshtastic.android.library.flavors)
21+
id("meshtastic.koin")
22+
}
23+
24+
android {
25+
namespace = "org.meshtastic.feature.car"
26+
27+
buildFeatures { buildConfig = true }
28+
29+
defaultConfig {
30+
minSdk = 23
31+
consumerProguardFiles("proguard-rules.pro")
32+
}
33+
}
34+
35+
dependencies {
36+
implementation(projects.core.common)
37+
implementation(projects.core.data)
38+
implementation(projects.core.database)
39+
implementation(projects.core.domain)
40+
implementation(projects.core.model)
41+
implementation(projects.core.repository)
42+
43+
implementation(libs.androidx.car.app)
44+
implementation(libs.androidx.car.app.projected)
45+
46+
implementation(libs.koin.android)
47+
implementation(libs.koin.annotations)
48+
49+
implementation(platform(libs.firebase.bom))
50+
implementation(libs.firebase.crashlytics)
51+
implementation(libs.kermit)
52+
53+
testImplementation(libs.androidx.car.app.testing)
54+
testImplementation(libs.koin.test)
55+
testImplementation(kotlin("test-junit"))
56+
testRuntimeOnly(libs.junit.vintage.engine)
57+
}

feature/car/proguard-rules.pro

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Car App Library ProGuard/R8 rules
2+
3+
# CarAppService must not be obfuscated (resolved by android:exported="true" in manifest,
4+
# but keep rule ensures R8 doesn't remove it during aggressive shrinking)
5+
-keep class org.meshtastic.feature.car.service.MeshtasticCarAppService { *; }
6+
7+
# Keep Koin-annotated classes for runtime DI resolution
8+
-keep @org.koin.core.annotation.Single class * { *; }
9+
-keep @org.koin.core.annotation.Factory class * { *; }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<application>
5+
<service
6+
android:name="org.meshtastic.feature.car.service.MeshtasticCarAppService"
7+
android:exported="true"
8+
android:permission="androidx.car.app.CarAppService">
9+
<intent-filter>
10+
<action android:name="androidx.car.app.CarAppService" />
11+
<category android:name="androidx.car.app.category.MESSAGING" />
12+
</intent-filter>
13+
</service>
14+
15+
<receiver
16+
android:name="org.meshtastic.feature.car.service.CarReplyReceiver"
17+
android:exported="false" />
18+
19+
<meta-data
20+
android:name="androidx.car.app.minCarApiLevel"
21+
android:value="7" />
22+
</application>
23+
</manifest>

0 commit comments

Comments
 (0)