Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
c018ca6
feat(auto): add Android Auto communications app with notification and…
Copilot Apr 17, 2026
e2700b2
fix(auto): clean up imports and simplify CarAppService
Copilot Apr 17, 2026
6d7ddeb
fix(auto): address code review — filterIndexed, remove deprecated ove…
Copilot Apr 17, 2026
41b99fd
fix(auto): fix ConnectionState data object usage and add list item li…
Copilot Apr 17, 2026
0df6d70
refactor(auto): extract Android Auto into feature:auto module
jamesarich Apr 17, 2026
86bb958
fix(auto): extract shortcut builders to fix LongMethod + catch specif…
jamesarich Apr 17, 2026
36f770f
fix(auto): preserve raw channel index for shortcut/unread contactKey
jamesarich Apr 17, 2026
0777291
fix(auto): project messaging notifications to Android Auto
jamesarich Apr 17, 2026
d17e715
fix(auto): clear unread count after inline reply
jamesarich Apr 17, 2026
72e27e3
chore(auto): log ReplyReceiver entry and completion
jamesarich Apr 17, 2026
c1073f3
fix(auto): don't re-post conversation notif on outgoing messages
jamesarich Apr 17, 2026
b5a631e
fix(auto): only include unread messages in conversation notif
jamesarich Apr 17, 2026
fb606db
fix(auto): refresh group summary when a conversation is cancelled
jamesarich Apr 17, 2026
9c75f5a
fix(auto): always cancel group summary when dismissing a conversation
jamesarich Apr 17, 2026
6d70d15
refactor(notifications): share markConversationRead helper across rec…
jamesarich Apr 17, 2026
eb3a27a
feat(auto): append outgoing reply to MessagingStyle for brief confirm…
jamesarich Apr 17, 2026
dac4880
feat(auto): replace ListTemplate with TabTemplate for iOS CarPlay parity
Copilot Apr 17, 2026
1d258da
test(notifications): add unit tests for reply/markAsRead/reaction rec…
jamesarich Apr 17, 2026
6af9cbf
Merge branch 'main' into copilot/add-messaging-feature-android-auto
garthvh Apr 17, 2026
38b7444
fix(auto): align TabTemplate with required Car API level 6 and tintab…
jamesarich Apr 17, 2026
01b1759
feat(auto): spec-compliance — minCarApiLevel=1, runtime API fallback,…
Copilot Apr 17, 2026
7c15c7b
feat(auto): unified Messages tab — channels + DMs, mirroring Contacts…
Copilot Apr 17, 2026
849aca7
plan: align Auto node/message row UI with phone NodeItem and ContactItem
Copilot Apr 17, 2026
9f0ead2
feat(auto): align Auto node/message row UI with phone NodeItem and Co…
Copilot Apr 17, 2026
2e74af7
feat(auto): polish - extract CarScreenDataBuilder, add unit tests, fi…
Copilot Apr 17, 2026
b828a12
style(auto): expand TabTemplate tab builder chains for readability
Copilot Apr 17, 2026
305a487
feat(mqtt): migrate to MQTTastic-Client-KMP (#5165)
jamesarich Apr 17, 2026
7f1ea28
refactor: use injected ioDispatcher and ApplicationCoroutineScope (#5…
jamesarich Apr 17, 2026
5eba7e4
fix: redact MeshLog proto secrets and centralize Compose keep-rules (…
jamesarich Apr 17, 2026
2a6e27d
fix(ui): stable LazyColumn keys, semantic roles, and content descript…
jamesarich Apr 17, 2026
1cd05d5
test: migrate MigrationTest to runTest and add missing repository fak…
jamesarich Apr 17, 2026
9c8085b
refactor: consolidate metric formatting through MetricFormatter (#5169)
jamesarich Apr 17, 2026
6ab3b96
chore(r8): remove redundant keep rules covered by consumer rules (#5172)
jamesarich Apr 17, 2026
d69b102
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 17, 2026
b49e8b2
fix(ui): finish accessibility roles and action labels for clickable s…
jamesarich Apr 17, 2026
d70c3b6
chore(strings): remove 4 unused string resources (#5173)
jamesarich Apr 17, 2026
67e300d
fix(auto): apply Android Auto best-practices audit fixes
Copilot Apr 17, 2026
cb5f11f
fix(auto): address branch review — cleanup, ProGuard, dedupe, API trim
Copilot Apr 17, 2026
8455198
diag(r8): disable minify for release builds (animation-freeze diagnos…
jamesarich Apr 17, 2026
7207ab3
Revert "diag(r8): disable minify for release builds (animation-freeze…
jamesarich Apr 17, 2026
9b0e1cc
fix(deps): pin androidx-compose runtime-tracing/ui-test to CMP versio…
jamesarich Apr 17, 2026
5c87002
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 18, 2026
84e70d0
feat(mqtt): adopt mqttastic-client-kmp 0.2.0 — disconnect reasons + T…
jamesarich Apr 18, 2026
b290db7
chore(deps): split androidx-compose version ref from CMP (#5183)
jamesarich Apr 18, 2026
7a21d9c
chore(deps): update compose-multiplatform to v1.11.0-rc01 (#5184)
renovate[bot] Apr 18, 2026
cc6114b
fix(widget): drive updates via debounced state observer (#5185)
jamesarich Apr 18, 2026
bb31d0a
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 18, 2026
730f340
chore(deps): update fastlane to v2.233.0 (#5190)
renovate[bot] Apr 19, 2026
7c674cf
chore(deps): update vico to v3.2.0-next.1 (#5191)
renovate[bot] Apr 19, 2026
7b797df
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 19, 2026
b94a31d
chore(deps): update plugin com.gradle.develocity to v4.4.1 (#5194)
renovate[bot] Apr 20, 2026
266d617
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 20, 2026
40adf86
Fix node-details remove action to preserve confirmation flow (#5192)
Copilot Apr 20, 2026
6596091
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 20, 2026
14e8ab4
fix(transport): improve BLE / TCP / USB reconnect and handshake resil…
jamesarich Apr 20, 2026
a1bdd09
fix(fdroid): prevent NotImplementedError crash on firmware release fe…
jamesarich Apr 21, 2026
2aee554
chore(deps): update org.jetbrains.androidx.navigation3:navigation3-ui…
renovate[bot] Apr 21, 2026
cafd076
fix(compass): stop coarse network fixes from clobbering GPS fixes (#5…
jamesarich Apr 21, 2026
a505fa5
chore(deps): update com.android.tools:common to v32.2.0 (#5202)
renovate[bot] Apr 21, 2026
d1875f2
chore(deps): update agp to v9.2.0 (#5201)
renovate[bot] Apr 21, 2026
8df6efb
fix(canned-messages): enable multiline text editing for long message …
jamesarich Apr 21, 2026
6c63af9
fix(settings): restore Import/Export button functionality in #4913 (#…
jamesarich Apr 21, 2026
a5a71d0
chore(deps): update core/proto/src/main/proto digest to d004f50 (#5205)
renovate[bot] Apr 21, 2026
652c898
feat(firmware): nRF52 BLE Legacy DFU support (#5209)
jamesarich Apr 22, 2026
338fde2
chore(deps): update core/proto/src/main/proto digest to 97ea65a (#5212)
renovate[bot] Apr 22, 2026
8dceeaf
chore(deps): update ktor to v3.4.3 (#5214)
renovate[bot] Apr 22, 2026
f48cc36
chore(deps): update koin.plugin to v1.0.0-rc2 (#5213)
renovate[bot] Apr 22, 2026
69ce7e6
feat(service): send polite ToRadio(disconnect=true) before transport …
jamesarich Apr 22, 2026
ae610bb
refactor: eliminate Accompanist permissions library (#5211)
jamesarich Apr 22, 2026
3f7cbae
fix: MQTT proxy connection and probe test failures (#5215)
jamesarich Apr 22, 2026
15dce97
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 22, 2026
24f19db
feat(node): smoother remote-admin UX with per-node session tracking (…
jamesarich Apr 22, 2026
37b805a
feat(connections): unified device list, ACCESS_LOCAL_NETWORK, transpo…
jamesarich Apr 22, 2026
46b228d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 22, 2026
35e9ea6
fix(ble): ensure GATT cleanup runs under NonCancellable on cancellati…
jdogg172 Apr 22, 2026
939132f
fix(ble): cleanup races discovered while reviewing #5207 (#5221)
jamesarich Apr 22, 2026
3a4cae5
fix(ble): unblock reconnect + kable audit (logging, priority, backoff…
jamesarich Apr 22, 2026
3814f74
chore(deps): update devtools.ksp to v2.3.7 (#5223)
renovate[bot] Apr 22, 2026
e500c9d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 23, 2026
c2b4826
feat: Enhance mPWRD-os WiFi provisioning success state and UI compone…
jamesarich Apr 23, 2026
85d923d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 23, 2026
98e9b59
fix(ui): make footer buttons expand downwards (#5226)
zt64 Apr 23, 2026
094c3fb
chore(deps): update kotlin to v2.3.21 (#5228)
renovate[bot] Apr 23, 2026
f091b6b
feat(messaging): add entry points for filter settings (#5229)
jamesarich Apr 23, 2026
0ab6285
fix(desktop): unbreak release builds (CMP beta03 + pwsh -P quoting) (…
jamesarich Apr 23, 2026
32b2319
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 23, 2026
2ba5b45
fix(desktop): suppress Vico ColorScale ProGuard warnings (#5232)
jamesarich Apr 23, 2026
0e5d1da
fix(desktop): unbreak Windows launch + Pi-installable arm64 .deb (#5233)
jamesarich Apr 23, 2026
29706df
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 23, 2026
fe7ef17
fix(desktop): unbreak release crash via correct ProGuard rules (#5236)
jamesarich Apr 23, 2026
087309e
chore(deps): update dd.sdk.android to v3.9.1 (#5237)
renovate[bot] Apr 23, 2026
a244db3
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 23, 2026
1901185
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (…
jamesarich Apr 24, 2026
8aef06b
Merge branch 'main' into copilot/add-messaging-feature-android-auto
garthvh Apr 24, 2026
afd56c8
Merge branch 'main' into copilot/add-messaging-feature-android-auto
garthvh Apr 26, 2026
483ab96
feat(auto): add local stats to Status tab and unit tests
Copilot Apr 26, 2026
970957b
fix(auto): ConversationItem API, manifest guards, and detekt compliance
jamesarich Apr 28, 2026
033985c
style: spotlessApply PersonIconFactory KDoc wrapping
jamesarich Apr 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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ dependencies {
implementation(projects.feature.settings)
implementation(projects.feature.firmware)
implementation(projects.feature.wifiProvision)
implementation(projects.feature.auto)
implementation(projects.feature.widget)

implementation(libs.jetbrains.compose.material3.adaptive)
Expand Down
8 changes: 8 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
# for auditing. Inspect this file after a release build to see what libraries inject.
-printconfiguration build/outputs/mapping/r8-merged-config.txt

# ---- Android Auto / Car App Library -----------------------------------------

# MeshtasticCarAppService and MeshtasticCarSession are instantiated by class name
# by the Android Auto host. Keep both classes (and their no-arg constructors) so
# release builds aren't broken by R8 tree-shaking.
-keep class org.meshtastic.feature.auto.MeshtasticCarAppService { <init>(); }
-keep class org.meshtastic.feature.auto.MeshtasticCarSession { <init>(); }

# ---- Networking (transitive references from Ktor on Android) ----------------

-dontwarn org.conscrypt.**
Expand Down
4 changes: 4 additions & 0 deletions app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {

override fun cancelMessageNotification(contactKey: String) {}

override suspend fun markConversationRead(contactKey: String) {}

override suspend fun appendOutgoingMessage(contactKey: String, text: String) {}

override fun cancelLowBatteryNotification(node: Node) {}

override fun clearClientNotification(notification: ClientNotification) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ class MeshDataHandlerImpl(
read = fromLocal || isFiltered,
filtered = isFiltered,
)
if (!isFiltered) {
if (!isFiltered && !fromLocal) {
handlePacketNotification(dataPacket, contactKey, updateNotification)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ interface MeshServiceNotifications {

fun cancelMessageNotification(contactKey: String)

/**
* Marks the conversation for [contactKey] as read: clears its unread count in the packet repository and cancels the
* posted message notification (and the group summary). Intended for use by notification action receivers (reply,
* mark-as-read, reaction) to keep behavior consistent.
*/
suspend fun markConversationRead(contactKey: String)

/**
* Appends an outgoing [text] message attributed to the local user to the currently posted conversation notification
* for [contactKey]. Used so that assistants such as Android Auto can briefly observe the reply in the
* MessagingStyle history before the notification is cancelled. No-op when there is nothing to update.
*/
suspend fun appendOutgoingMessage(contactKey: String, text: String)

fun cancelLowBatteryNotification(node: Node)

fun clearClientNotification(notification: ClientNotification)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.service

import android.content.Context
import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.MeshServiceNotifications
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class MarkAsReadReceiverTest {

private lateinit var context: Context
private lateinit var notifications: RecordingNotifications

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
notifications = RecordingNotifications(mutableListOf())
val dispatcher = UnconfinedTestDispatcher()
startKoin {
modules(
module {
single<MeshServiceNotifications> { notifications }
single { CoroutineDispatchers(io = dispatcher, main = dispatcher, default = dispatcher) }
},
)
}
}

@After
fun tearDown() {
stopKoin()
}

@Test
fun `markAsRead action invokes markConversationRead`() {
val contactKey = "0!deadbeef"
val intent =
Intent(context, MarkAsReadReceiver::class.java).apply {
action = MarkAsReadReceiver.MARK_AS_READ_ACTION
putExtra(MarkAsReadReceiver.CONTACT_KEY, contactKey)
}

MarkAsReadReceiver().onReceive(context, intent)

assertEquals(listOf(contactKey), notifications.markReadCalls)
}

@Test
fun `missing contactKey does not invoke markConversationRead`() {
val intent =
Intent(context, MarkAsReadReceiver::class.java).apply { action = MarkAsReadReceiver.MARK_AS_READ_ACTION }

MarkAsReadReceiver().onReceive(context, intent)

assertEquals(emptyList(), notifications.markReadCalls)
}

@Test
fun `wrong action is ignored`() {
val intent =
Intent(context, MarkAsReadReceiver::class.java).apply {
action = "some.other.ACTION"
putExtra(MarkAsReadReceiver.CONTACT_KEY, "0!abcd")
}

MarkAsReadReceiver().onReceive(context, intent)

assertEquals(emptyList(), notifications.markReadCalls)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class MeshServiceNotificationsImplTest {
context = context,
packetRepository = lazy<PacketRepository> { error("Not used in this test") },
nodeRepository = lazy<NodeRepository> { error("Not used in this test") },
shortcutManager = lazy<ConversationShortcutManager> { error("Not used in this test") },
)

notifications.initChannels()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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.service

import android.content.Context
import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import dev.mokkery.MockMode
import dev.mokkery.answering.calls
import dev.mokkery.answering.returns
import dev.mokkery.everySuspend
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify.VerifyMode
import dev.mokkery.verifySuspend
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.ServiceRepository
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class ReactionReceiverTest {

private lateinit var context: Context
private lateinit var notifications: RecordingNotifications
private lateinit var serviceRepository: ServiceRepository

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
notifications = RecordingNotifications(mutableListOf())
serviceRepository = mock(MockMode.autofill)
val dispatcher = UnconfinedTestDispatcher()
startKoin {
modules(
module {
single<ServiceRepository> { serviceRepository }
single<MeshServiceNotifications> { notifications }
single { CoroutineDispatchers(io = dispatcher, main = dispatcher, default = dispatcher) }
},
)
}
}

@After
fun tearDown() {
stopKoin()
}

@Test
fun `reaction dispatches ServiceAction and marks conversation read`() {
val contactKey = "0!cafebabe"
val emoji = "👍"
val replyId = 42
everySuspend { serviceRepository.onServiceAction(any()) } returns Unit

val intent =
Intent(context, ReactionReceiver::class.java).apply {
action = ReactionReceiver.REACT_ACTION
putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey)
putExtra(ReactionReceiver.EXTRA_EMOJI, emoji)
putExtra(ReactionReceiver.EXTRA_REPLY_ID, replyId)
}

ReactionReceiver().onReceive(context, intent)

verifySuspend(VerifyMode.exactly(1)) {
serviceRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey))
}
assertEquals(listOf(contactKey), notifications.markReadCalls)
}

@Test
fun `reaction does not markRead when ServiceAction dispatch throws`() {
val contactKey = "0!feedface"
val throwingRepo = mock<ServiceRepository>(MockMode.autofill)
everySuspend { throwingRepo.onServiceAction(any()) } calls { throw IllegalStateException("boom") }
stopKoin()
val dispatcher = UnconfinedTestDispatcher()
startKoin {
modules(
module {
single<ServiceRepository> { throwingRepo }
single<MeshServiceNotifications> { notifications }
single { CoroutineDispatchers(io = dispatcher, main = dispatcher, default = dispatcher) }
},
)
}

val intent =
Intent(context, ReactionReceiver::class.java).apply {
action = ReactionReceiver.REACT_ACTION
putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey)
putExtra(ReactionReceiver.EXTRA_REACTION, "🎉")
putExtra(ReactionReceiver.EXTRA_PACKET_ID, 7)
}

ReactionReceiver().onReceive(context, intent)

assertEquals(emptyList(), notifications.markReadCalls)
}

@Test
fun `reaction without contactKey is dropped`() {
val intent =
Intent(context, ReactionReceiver::class.java).apply {
action = ReactionReceiver.REACT_ACTION
putExtra(ReactionReceiver.EXTRA_EMOJI, "👍")
}

ReactionReceiver().onReceive(context, intent)

assertEquals(emptyList(), notifications.markReadCalls)
}

@Test
fun `wrong action is ignored`() {
val intent =
Intent(context, ReactionReceiver::class.java).apply {
action = "other.ACTION"
putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, "0!abcd")
putExtra(ReactionReceiver.EXTRA_EMOJI, "👍")
}

ReactionReceiver().onReceive(context, intent)

assertEquals(emptyList(), notifications.markReadCalls)
}
}
Loading