Compare commits

..

440 commits

Author SHA1 Message Date
James Rich
f21d8af9ae
fix(transport): improve BLE / TCP / USB reconnect and handshake resilience (#5196)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 17:34:16 +00:00
James Rich
a90cb2d89e
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5195)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-20 17:32:58 +00:00
Copilot
7492a33cf8
Fix node-details remove action to preserve confirmation flow (#5192)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jamesarich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 15:59:20 +00:00
James Rich
2b47da3b61
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5193)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-20 07:40:08 -05:00
renovate[bot]
3322257cfd
chore(deps): update plugin com.gradle.develocity to v4.4.1 (#5194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:47:09 +00:00
James Rich
99e7407a90
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5189)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-19 20:07:52 +00:00
renovate[bot]
9dd57725f2
chore(deps): update vico to v3.2.0-next.1 (#5191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-19 12:31:11 -05:00
renovate[bot]
2c1984ace5
chore(deps): update fastlane to v2.233.0 (#5190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-19 16:30:34 +00:00
James Rich
94856d257f
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5186)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-18 12:09:22 +00:00
James Rich
84fe24467f
fix(widget): drive updates via debounced state observer (#5185)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-18 04:11:32 +00:00
renovate[bot]
68a414b75b
chore(deps): update compose-multiplatform to v1.11.0-rc01 (#5184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 22:00:34 -05:00
James Rich
4257e7b7e4
chore(deps): split androidx-compose version ref from CMP (#5183)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:41:36 -05:00
James Rich
14e86b90f1
feat(mqtt): adopt mqttastic-client-kmp 0.2.0 — disconnect reasons + Test Connection (#5181)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:33:55 -05:00
James Rich
ef0e159abb
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5177)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-17 21:20:58 -05:00
James Rich
61d7f6fef3
fix(deps): pin androidx-compose runtime-tracing/ui-test to CMP version (#5179)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:46:59 -05:00
James Rich
a273dc6623
Revert "diag(r8): disable minify for release builds (animation-freeze diagnostic)" (#5176)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:07:54 -05:00
James Rich
c866f60b59
diag(r8): disable minify for release builds (animation-freeze diagnostic) (#5174)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:36:33 +00:00
James Rich
10bc58d417
chore(strings): remove 4 unused string resources (#5173)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:36:32 +00:00
James Rich
dd74e501f3
fix(ui): finish accessibility roles and action labels for clickable surfaces (#5170)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:33:38 +00:00
James Rich
56cbc3670d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5163)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-17 17:13:27 +00:00
James Rich
15a7c19b74
chore(r8): remove redundant keep rules covered by consumer rules (#5172)
Co-authored-by: GitHub Copilot CLI <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:13:26 +00:00
James Rich
b979663e24
refactor: consolidate metric formatting through MetricFormatter (#5169)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:13:01 +00:00
James Rich
9f3fe865e3
test: migrate MigrationTest to runTest and add missing repository fakes (#5171)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:35:41 +00:00
James Rich
90f6e21a9c
fix(ui): stable LazyColumn keys, semantic roles, and content descriptions (#5168)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:24:18 +00:00
James Rich
cdeb1ac532
fix: redact MeshLog proto secrets and centralize Compose keep-rules (#5166)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:20:50 +00:00
James Rich
adfe3bfed1
refactor: use injected ioDispatcher and ApplicationCoroutineScope (#5167)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:18:45 +00:00
James Rich
a97f704300
feat(mqtt): migrate to MQTTastic-Client-KMP (#5165)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:19:08 +00:00
James Rich
df3b5365f9
fix(node): don't recreate Vico CartesianChartModelProducer on channel switch (#5160)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 02:40:17 +00:00
James Rich
a6a889430b
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5159)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-16 21:43:35 -05:00
renovate[bot]
65b885a073
chore(deps): update core/proto/src/main/proto digest to 4d5b500 (#5161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:41:36 -05:00
James Rich
17e69c6d4c
chore: review-cleanup fleet (audit + fix + hardening) (#5158) 2026-04-17 00:02:59 +00:00
James Rich
872c566ef1
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5157)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-16 20:25:12 +00:00
renovate[bot]
3a2f2fc56b
chore(deps): update kotlin to v2.3.21-rc2 (#5155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:33:25 +00:00
renovate[bot]
50896d455b
chore(deps): update dd.sdk.android to v3.9.0 (#5156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:33:13 +00:00
James Rich
a580cd0467
chore(analytics): disable Datadog Compose action tracking (#5153)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 23:09:21 +00:00
James Rich
8e5d99410c
refactor(di): adopt @KoinApplication with startKoin<T>() compiler plugin API (#5152)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 22:52:59 +00:00
renovate[bot]
0f900fe7d7
chore(deps): update core/proto/src/main/proto digest to c9067da (#5151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 13:12:53 -05:00
James Rich
9ac02cf851
fix(app): disable R8 optimization to fix Compose animation freeze (#5150)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 17:45:27 +00:00
James Rich
878905aea3
perf(messaging): batch node + reply lookups in message loading (#5149)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 15:48:26 +00:00
James Rich
dea364dd17
fix(app): add R8 keep rules for Compose animation/runtime/ui (#5146)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 14:30:33 +00:00
James Rich
c7d2a76851
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5145)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-15 07:48:12 -05:00
renovate[bot]
f72b91328d
chore(deps): update androidx.compose to v1.11.0-rc01 (#5144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 07:47:53 -05:00
James Rich
d0057752f6
fix(ci): remove Renovate groupings and decouple AndroidX Compose version ref (#5143)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 07:23:20 -05:00
James Rich
84621acb04
fix: align BLE connection handshake with firmware protocol expectations (#5141)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 11:55:15 +00:00
James Rich
96419f3251
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5140)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-15 06:07:21 -05:00
James Rich
60ff495037
chore(r8): clean up ProGuard rules and enable Compose Hot Reload (#5139)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 03:26:39 +00:00
James Rich
401f59489a
chore: remove deprecated mesh_service_example module (#5055) 2026-04-15 03:10:23 +00:00
James Rich
a2763bdfeb
fix(charts): apply Vico 3.1.0 best-practice audit fixes (#5138)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 02:20:33 +00:00
James Rich
72b981f73b
chore: KMP audit — commonize code, centralize utilities, eliminate dead abstractions (#5133)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 02:17:50 +00:00
James Rich
50ade01e55
docs(agents): add PR and commit hygiene guidance (#5137)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 20:49:34 -05:00
James Rich
79ed0a865a
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5128)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-15 01:14:38 +00:00
James Rich
bf0deef708
fix(icons): audit and correct icon migration regressions from #5030 #5040 #5056 (#5136)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 01:14:31 +00:00
James Rich
fa63a4ac50
feat: add high-contrast theme with accessible message bubbles (#5135)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 01:14:20 +00:00
James Rich
f48fc61729
feat(environment): add 1-Wire multi-thermometer (DS18B20) display support (#5130)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 00:03:24 +00:00
James Rich
099aea2d81
feat(desktop): add entitlements and wire MeshConnectionManager into orchestrator (#5127) 2026-04-14 15:16:10 +00:00
renovate[bot]
c6f58cc799
chore(deps): update core/proto/src/main/proto digest to 940ac38 (#5126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 09:48:25 -05:00
James Rich
27055290e2
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5125)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-14 12:37:12 +00:00
renovate[bot]
3aadd29e67
chore(deps): update core/proto/src/main/proto digest to a045501 (#5124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 07:17:49 -05:00
James Rich
9acdf5309f
refactor: modern APIs — Koin 4.2, CMP 1.11, Ktor resilience, Room @Upsert, injected dispatchers (#5119) 2026-04-14 11:41:01 +00:00
renovate[bot]
99378c9291
chore(deps): update core/proto/src/main/proto digest to 98e95ee (#5123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 06:50:59 -05:00
James Rich
3c7e1266f8
fix: truncate traceroute chart x-values to whole seconds to prevent Vico crash (#5122) 2026-04-14 11:01:03 +00:00
James Rich
743851b0b5
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5120)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-14 10:35:19 +00:00
James Rich
e46a8296cb
feat(core/ui): add safeLaunch, UiState, KMP permissions, and CMP lifecycle modernization (#5118) 2026-04-14 00:45:34 +00:00
James Rich
27367e9064
fix(build): pin Skiko version to align with Compose Multiplatform (#5117) 2026-04-13 23:32:00 +00:00
James Rich
28be6933c8
fix(proguard): disable shrinking for Compose animation classes (#5116) 2026-04-13 21:55:52 +00:00
James Rich
92166f0fa2
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5115)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-13 15:52:55 -05:00
renovate[bot]
8e7c4f54a3
chore(deps): update actions/upload-pages-artifact action to v5 (#5114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 15:24:43 -05:00
James Rich
938a951737
refactor: leverage CMP 1.11 + Lifecycle 2.11 — v2 test API, Json privacy, dropUnlessResumed nav guards (#5112) 2026-04-13 20:02:31 +00:00
James Rich
76386e419c
refactor: migrate remaining raw stateIn(WhileSubscribed) to stateInWhileSubscribed extension (#5113) 2026-04-13 20:02:06 +00:00
James Rich
b13f9bf989
fix(resources): add resourcePrefix to KMP + widget modules, rename prefixed resources (#5111) 2026-04-13 18:25:23 +00:00
James Rich
8a06157ff4
docs: remove agent cruft, condense and validate remaining docs (#5110) 2026-04-13 17:59:19 +00:00
renovate[bot]
75e2177da7
chore(deps): update com.android.tools:common to v32.1.1 (#5108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 17:16:04 +00:00
renovate[bot]
61f90352c4
chore(deps): update agp to v9.2.0-rc01 (#5107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 17:15:52 +00:00
James Rich
087fbbfb45
fix(build): overhaul R8 rules and DRY up build-logic conventions (#5109) 2026-04-13 17:11:42 +00:00
James Rich
048c74db13
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5105)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-13 12:37:53 +00:00
James Rich
39620d063b
fix(nav): restore broken traceroute map navigation (#5104) 2026-04-13 12:25:21 +00:00
James Rich
35bf1fded5
build: align Compose Multiplatform versions and exclude transitive BOMs (#5103) 2026-04-13 12:02:52 +00:00
James Rich
4dd591af25
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5101)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-13 11:04:58 +00:00
James Rich
a8cdec7f55
fix(ci): isolate JetBrains Compose Multiplatform in Renovate config (#5102) 2026-04-13 06:09:22 -05:00
renovate[bot]
1e29fec469
chore(deps): update androidx (general) to v1.11.0-rc01 (#5099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
2026-04-12 22:33:44 -05:00
James Rich
b0c603c7ed
fix(build): align AndroidX Compose versions with CMP and migrate to runComposeUiTest (#5096) 2026-04-13 02:49:11 +00:00
James Rich
e424d4d076
fix(build): add explicit compose-multiplatform-animation dependency (#5095) 2026-04-12 22:36:37 +00:00
James Rich
17d85c88c4
fix(release): publish GitHub release on promotion instead of staying draft (#5094) 2026-04-12 16:04:13 -05:00
James Rich
5c47256b3f
test(prefs): migrate DataStore tests from androidHostTest to commonTest (#5092) 2026-04-12 20:45:19 +00:00
James Rich
a11dee42a7
test: migrate Compose UI tests from androidTest to commonTest (#5091) 2026-04-12 20:20:00 +00:00
James Rich
4156acf297
ci: fix Gradle cache path validation warning for Robolectric jars (#5093) 2026-04-12 15:18:02 -05:00
James Rich
c059f19cc6
ci: reduce CI costs by ~54% — skip desktop builds in PR/main, reduce scheduled frequency (#5090) 2026-04-12 18:59:21 +00:00
James Rich
ade314d503
build: upgrade TARGET_SDK to 37 and update AGP to 9.2.0-alpha08 (#5089) 2026-04-12 17:42:58 +00:00
James Rich
bc44af1597
fix(connections): show device name during connecting state (#5085) 2026-04-12 17:29:25 +00:00
James Rich
eeed780e51
chore(ai): modernize and unify agent tooling and instructions (#5087) 2026-04-12 17:29:05 +00:00
James Rich
d03e61af6f
fix(build): remove Compose BOM to resolve compileSdk 37 conflict (#5088) 2026-04-12 17:05:52 +00:00
renovate[bot]
916eb51b94
chore(deps): update androidx.compose:compose-bom-alpha to v2026.04.00 (#5086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 10:01:40 -05:00
James Rich
7ca7179197
build: migrate Compose dependencies to Compose Multiplatform (#5084) 2026-04-12 14:45:11 +00:00
James Rich
9281324be3
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5082) 2026-04-12 06:44:03 -05:00
renovate[bot]
6da9f088a9
chore(deps): update softprops/action-gh-release action to v3 (#5081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 06:43:45 -05:00
James Rich
e85300531e
refactor(transport): complete transport architecture overhaul — extract callback, wire BleReconnectPolicy, fix safety issues (#5080) 2026-04-12 04:22:18 +00:00
James Rich
962c619c4c
chore(deps): bump Kotlin 2.3.21-RC, Koin plugin 1.0.0-RC1, drop datetime compat (#5079) 2026-04-12 02:09:23 +00:00
James Rich
19502cd1e0
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5078) 2026-04-12 01:48:42 +00:00
James Rich
9468bc6ebe
refactor(service): unify dual connectionState flows into single source of truth (#5077) 2026-04-12 00:50:52 +00:00
James Rich
5e44cbd3a9
fix(data): make MeshConnectionManagerImpl.onConnectionChanged atomic (#5076) 2026-04-12 00:49:09 +00:00
James Rich
62264b10c6
refactor(model): remove ConnectionState helper methods and fix updateStatusNotification return type (#5074) 2026-04-11 23:41:34 +00:00
James Rich
174315b21f
refactor(data): replace lateinit var scope + start() with constructor injection (#5075) 2026-04-11 23:39:29 +00:00
James Rich
172680fd46
fix(mqtt): replace yield() with proper connection readiness signal (#5073) 2026-04-11 23:38:33 +00:00
James Rich
a3c0a4832d
fix(transport): Kable BLE audit + thread-safety, MQTT, and logging fixes across transport layers (#5071) 2026-04-11 22:56:29 +00:00
James Rich
5f0e60eb21
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5067) 2026-04-11 22:07:04 +00:00
James Rich
40ea45a4fe
fix(settings): hide Status Message config until firmware v2.8.0 (#5070) 2026-04-11 22:06:53 +00:00
James Rich
1fe3f4423d
fix(ui): add missing @ParameterName annotations on actual rememberReadTextFromUri declarations (#5072) 2026-04-11 22:06:44 +00:00
James Rich
0441093ce8
refactor(node): move Position to last in telemetry list on node details (#5068) 2026-04-11 17:06:17 +00:00
James Rich
b3d0c97206
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5065) 2026-04-11 04:53:07 -05:00
James Rich
1f88a26d51
feat(desktop): align versioning with Android, build runnable distributions in CI (#5064) 2026-04-11 03:50:32 +00:00
James Rich
6b77658cb1
ci: remove mesh_service_example from CI checks and Codecov (#5066) 2026-04-11 03:48:09 +00:00
James Rich
3794c79dae
refactor: adopt M3 Expressive components from material3 1.11.0-alpha06 (#5063) 2026-04-11 02:10:03 +00:00
James Rich
a6423d0a0f
feat(metrics): redesign position log with SelectableMetricCard and add CSV export to all metrics screens (#5062) 2026-04-11 01:26:26 +00:00
James Rich
37e9e2c8f0
fix(charts): hoist rememberVicoZoomState above vararg layers to prevent ClassCastException (#5060) 2026-04-11 00:05:56 +00:00
James Rich
9c8532f80d
refactor: leverage new dependency features from recent updates (#5057) 2026-04-10 22:51:08 +00:00
James Rich
929e273978
fix(build): resolve all actionable compile-time warnings (#5058) 2026-04-10 22:42:30 +00:00
James Rich
3d139d32fd
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5059) 2026-04-10 22:15:59 +00:00
James Rich
ce32e640de
fix(icons): replace outline (FILL=0) pathData with filled (FILL=1) from upstream Material Symbols (#5056) 2026-04-10 22:15:43 +00:00
James Rich
77e30b60e1
chore(build): enable AboutLibraries offlineMode by default (#5054) 2026-04-10 21:34:38 +00:00
James Rich
520fa717a9
refactor(metrics/map): DRY up charts, decompose MapView monoliths, add test coverage (#5049) 2026-04-10 20:54:09 +00:00
James Rich
56332f4d77
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5053) 2026-04-10 20:41:55 +00:00
James Rich
02f6fd67b8
fix: clean up flaky, duplicated, and misplaced tests; remove redundant deps (#5048) 2026-04-10 19:46:45 +00:00
James Rich
e70dabe94d
test(navigation): add tests for NavigationConfig, DeepLinkRouter, and… (#5052) 2026-04-10 19:18:59 +00:00
James Rich
eec27cf6f7
chore(resources): remove 131 unused string keys (#5051) 2026-04-10 19:13:33 +00:00
renovate[bot]
7ef382cce7
chore(deps): update google maps compose to v8.3.0 (#5050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 18:22:14 +00:00
James Rich
3d51a48da2
feat(messaging): add IME Send action to message input (#5047) 2026-04-10 16:02:55 +00:00
renovate[bot]
6f5fa49b94
chore(deps): update actions/cache action to v5 (#5046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 11:07:46 -05:00
James Rich
ae5f021323
refactor(navigation): adopt sealed interface routes with subclassesOfSealed() (#5043) 2026-04-10 15:33:57 +00:00
James Rich
0355c7b8b3
fix(build): prevent DataDog asset transform from stripping fdroid release assets (#5044) 2026-04-10 10:18:02 -05:00
James Rich
1390a3cd4f
ci: cache Robolectric SDK jars to prevent flaky SocketException failures (#5045) 2026-04-10 10:05:07 -05:00
James Rich
93e0b9ca57
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5042) 2026-04-10 14:41:56 +00:00
renovate[bot]
5c58709b0f
chore(deps): update core/proto/src/main/proto digest to a4c649b (#5041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 06:44:45 -05:00
James Rich
decda75852
style: update ic_no_cell and ic_place vector drawables (#5040) 2026-04-10 11:30:48 +00:00
James Rich
17e7c76583
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5035) 2026-04-10 10:44:08 +00:00
renovate[bot]
aeef34f88c
chore(deps): update compose.multiplatform.material3 to v1.11.0-alpha06 (#5037)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 06:08:02 -05:00
renovate[bot]
978ce19f93
chore(deps): update compose.multiplatform to v1.11.0-beta02 (#5036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 06:06:37 -05:00
renovate[bot]
1db4e03076
chore(deps): update org.jetbrains.androidx.navigation3:navigation3-ui to v1.1.0-rc01 (#5039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 06:06:11 -05:00
renovate[bot]
e23fab2667
chore(deps): update jetbrains.lifecycle to v2.11.0-alpha03 (#5038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 06:04:01 -05:00
James Rich
ebf3b8272c
fix(service): resolve MeshService crash from eager notification channel init (#5034) 2026-04-10 03:24:03 +00:00
James Rich
dba037466e
refactor(icons): migrate to self-hosted VectorDrawable XMLs via MeshtasticIcons (#5030) 2026-04-10 01:35:52 +00:00
James Rich
5e57efeb06
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5031) 2026-04-09 19:48:16 -05:00
James Rich
a1e94aa439
fix(ci): add concurrency group to Check PR Labels workflow (#5032) 2026-04-09 19:47:30 -05:00
renovate[bot]
f07624be88
chore(deps): update actions/github-script action to v9 (#5029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 23:53:58 +00:00
James Rich
d5a9e32b32
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5025) 2026-04-09 23:46:20 +00:00
James Rich
9c0e9b82d6
feat(charts): adopt Vico best practices, add sensor data, and migrate TracerouteLog (#5026) 2026-04-09 23:44:59 +00:00
renovate[bot]
e01c4abae7
chore(deps): update markdown renderer (mike penz) to v14.0.1 (#5028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 23:44:23 +00:00
renovate[bot]
20d934459a
chore(deps): update firebase to v3.0.7 (#5027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 23:44:08 +00:00
renovate[bot]
ad7003ed90
chore(deps): update kotlin ecosystem to v1.11.0 (#5024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 13:37:47 -05:00
James Rich
14b381c1eb
fix: harden reliability, clean up KMP compliance, and improve code quality (#5023) 2026-04-09 18:21:46 +00:00
James Rich
537029a71c
fix(ci): correct repo guards, labels, and prompts in triage/moderation workflows (#5022) 2026-04-09 12:48:55 -05:00
renovate[bot]
015ab5c0fb
chore(deps): update com.google.firebase:firebase-bom to v34.12.0 (#5021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:40:16 -05:00
James Rich
1649e46dd5
chore(deps): remove 7 unused dependencies across modules (#5017) 2026-04-09 12:35:28 -05:00
renovate[bot]
0576364c11
chore(deps): update koin to v4.2.1 (#5019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:26:31 -05:00
renovate[bot]
eb79421209
chore(deps): update plugin com.gradle.common-custom-user-data-gradle-plugin to v2.6.0 (#5016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:26:15 -05:00
James Rich
2ce110dffe
fix: scope labeler trigger to reduce rate limiting and fix bugfix typo (#5020) 2026-04-09 12:25:51 -05:00
James Rich
750c4ea928
fix: use payload labels in pr_enforce_labels.yml to avoid rate limiting (#5018) 2026-04-09 12:16:15 -05:00
James Rich
013a9afc96
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5014) 2026-04-09 16:34:33 +00:00
James Rich
975df02437
fix(tak): resolve frequent TAK client disconnections (#5015) 2026-04-09 16:24:50 +00:00
James Rich
ad08a6c7b7
feat(settings): add DNS support and fix UDP protocol toggle (#5013) 2026-04-09 14:23:31 +00:00
James Rich
150ee3f1a4
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5012) 2026-04-09 13:37:09 +00:00
James Rich
60cc2f4237
fix: resolve bugs across connection, PKI, admin, packet flow, and stability subsystems (#5011) 2026-04-09 13:20:06 +00:00
renovate[bot]
cd9f1c0600
chore(deps): update markdown renderer (mike penz) to v14 (major) (#5001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:08:19 +00:00
renovate[bot]
8dfb642deb
chore(deps): update vico to v3.1.0 (#4999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:08:13 +00:00
James Rich
87d507eb6e
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4997) 2026-04-09 12:08:01 +00:00
renovate[bot]
f817297ebe
chore(deps): update androidx room to v3.0.0-alpha03 (#5007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:54 +00:00
renovate[bot]
f33518de6d
chore(deps): update markdown renderer (mike penz) to v0.40.2 (#5000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:49 +00:00
renovate[bot]
cc2fb45366
chore(deps): update datadog to v1.25.0 (#5003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:43 +00:00
renovate[bot]
5f53bfa300
chore(deps): update androidx.annotation:annotation to v1.10.0 (#5009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:36 +00:00
renovate[bot]
38a19e5599
chore(deps): update io.nlopez.compose.rules:detekt to v0.5.7 (#5008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:31 +00:00
renovate[bot]
547d349b48
chore(deps): update core/proto/src/main/proto digest to e30092e (#5006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:07:25 +00:00
Copilot
15419aba6c
fix: resolve correct node public key in sendSharedContact and favoriteNode (#5005)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-04-08 12:54:47 +00:00
James Rich
72f4697d0d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4993) 2026-04-04 19:51:47 -05:00
James Rich
b3be9e2c38
fix: improve PKI message routing and resolve database migration racecondition (#4996) 2026-04-05 00:37:20 +00:00
renovate[bot]
d0e3b682ab
chore(deps): update kotest to v6.1.11 (#4991)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Deploy Documentation / build-docs (push) Waiting to run
Deploy Documentation / deploy (push) Blocked by required conditions
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 13:43:51 -05:00
James Rich
6af3ad6f0c
refactor(service): harden KMP service layer — database init, connection reliability, handler decomposition (#4992) 2026-04-04 18:07:44 +00:00
James Rich
e111b61e4e
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4985)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Deploy Documentation / build-docs (push) Waiting to run
Deploy Documentation / deploy (push) Blocked by required conditions
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-04-04 10:51:51 +00:00
renovate[bot]
1442e9354e
chore(deps): update core/proto/src/main/proto digest to 349c1d5 (#4990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 10:51:21 +00:00
renovate[bot]
5673eb90f3
chore(deps): update plugin com.gradle.common-custom-user-data-gradle-plugin to v2.5.0 (#4987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 10:51:09 +00:00
renovate[bot]
53d21b4193
chore(deps): update koin.plugin to v0.6.2 (#4986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 10:51:01 +00:00
James Rich
fda96e2f8c
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4975)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-04-03 09:13:23 -05:00
renovate[bot]
e468818c82
chore(deps): update org.junit.platform:junit-platform-launcher to v6 (#4983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 09:09:57 -05:00
renovate[bot]
919da2904e
chore(deps): update junit5 to v6 (major) (#4982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 09:08:03 -05:00
renovate[bot]
9544df2bb9
chore(deps): update org.junit.platform:junit-platform-launcher to v1.14.3 (#4981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 09:02:31 -05:00
renovate[bot]
db5403b436
chore(deps): update junit5 to v5.14.3 (#4980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 08:59:43 -05:00
James Rich
fc86c696cd
feat(wifi-provision): add mPWRD-OS branding and disclaimer banner (#4978) 2026-04-03 13:47:15 +00:00
James Rich
51251ab16a
feat(ci): shard test suite and enable JUnit 5 parallel execution (#4977) 2026-04-03 13:08:49 +00:00
James Rich
7e041c00e1
feat(wifi): introduce BLE-based WiFi provisioning for nymea-compatible devices (#4968)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-04-02 17:31:17 +00:00
James Rich
1fee6c4431
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4973)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-04-01 18:40:29 -05:00
James Rich
176340be5f
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4972) 2026-04-01 16:49:55 -05:00
renovate[bot]
05229fd652
chore(deps): update xmlutil to v0.91.3 (#4971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 16:48:57 -05:00
James Rich
0167063497
feat(analytics): expand DataDog RUM integration and align with iOS parity (#4970) 2026-04-01 20:27:28 +00:00
James Rich
e249461e3c
feat(tak): introduce built-in Local TAK Server and mesh integration (#4951)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-04-01 20:21:25 +00:00
James Rich
d1ca8ec527
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4967)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-04-01 10:56:33 -05:00
James Rich
fefe74d217
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4965) 2026-04-01 12:45:23 +00:00
James Rich
89547afe6b
Refactor and unify firmware update logic across platforms (#4966) 2026-04-01 12:14:26 +00:00
James Rich
d8e295cafb
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4964)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-31 21:15:06 -05:00
James Rich
7c9d007a1f
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4962) 2026-03-31 21:26:24 +00:00
James Rich
464a12b9f7
chore: standardize resources and update documentation for Navigation 3 (#4961) 2026-03-31 21:25:37 +00:00
James Rich
1faa802fe6
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4958)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-31 14:07:18 +00:00
James Rich
6a5115b897
Refactor navigation to use NodeDetail route and fix radio settings (#4960) 2026-03-31 14:03:02 +00:00
James Rich
c75c9b34d6
feat: implement XModem file transfers and enhance BLE connection robustness (#4959)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-31 03:49:31 +00:00
Roman Vlasenko
ae4465d7c8
fix(strings): Fix public key description (#4957)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-30 20:34:54 +00:00
renovate[bot]
38848b4ea4
chore(deps): update kotest to v6.1.10 (#4956)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 16:03:58 +00:00
James Rich
69f147a1d8
ci: Upgrade to JDK 21 and centralize CI configuration with new workflows (#4948)
Some checks failed
Dependency Submission / dependency-submission (push) Has been cancelled
Main CI (Verify & Build) / validate-and-build (push) Has been cancelled
Main Push Changelog / Generate main push changelog (push) Has been cancelled
2026-03-27 17:06:19 +00:00
renovate[bot]
445ec27fa4
chore(deps): update ktor to v3.4.2 (#4947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 16:49:00 +00:00
James Rich
ad6e12445b
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4943)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-27 14:51:32 +00:00
James Rich
f2d09ff79d
Refactor nav3 architecture and enhance adaptive layouts (#4944)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-27 14:43:44 +00:00
James Rich
3feec759a1
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4939)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-27 02:45:08 +00:00
renovate[bot]
791601dec2
chore(deps): update wire to v6.2.0 (#4940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 02:45:01 +00:00
James Rich
0bc7ca820b
ci(github): add conditional desktop build to release workflows (#4938)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-26 15:03:27 -05:00
James Rich
1c1c208d48
chore(ci): implement tiered GitHub Actions runner strategy (#4937)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 18:18:03 +00:00
James Rich
e106badec7
build: update JVM toolchain and CI Java distribution (#4936) 2026-03-26 12:25:05 -05:00
James Rich
d979269552
fix(ci): update APP_VERSION_NAME output reference in workflows (#4935) 2026-03-26 12:01:53 -05:00
James Rich
141b54ff9c
feat: migrate to Material 3 Expressive APIs (#4934)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 16:42:46 +00:00
renovate[bot]
c259c76550
chore(deps): update codecov/codecov-action action to v6 (#4933)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 15:26:11 +00:00
James Rich
518096ddc8
build: update Compose Multiplatform and migrate lifecycle dependencies (#4932)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 15:18:45 +00:00
renovate[bot]
4be0fb9c65
chore(deps): update org.jetbrains.androidx.navigation3:navigation3-ui to v1.1.0-beta01 (#4931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 09:08:36 -05:00
renovate[bot]
0d2c2e8a98
chore(deps): update jetbrains.lifecycle to v2.11.0-alpha02 (#4930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 13:52:16 +00:00
James Rich
3b485a0109
build: enable -Xjvm-default=all compiler flag (#4929)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 13:16:04 +00:00
renovate[bot]
e1fa05f63f
chore(deps): update dokka to v2.2.0 (#4928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 08:16:35 -05:00
James Rich
ff6e1813ea
chore(github): update GitHub token reference in workflows (#4926)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-25 21:32:14 -05:00
James Rich
da5b28443e
ci: change jdk from jetbrains to temurin in non-release workflows (#4925) 2026-03-25 21:24:29 -05:00
James Rich
7484cc69b4
refactor(ui): remove labels from navigation suite items (#4924) 2026-03-25 21:14:17 -05:00
James Rich
36290fc94b
build(github): add GITHUB_TOKEN to setup-java actions (#4923) 2026-03-25 20:53:18 -05:00
James Rich
6f95435cfc
ci: refine workflow permissions and version parsing logic (#4922)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 01:16:22 +00:00
James Rich
a005231d94
Refactor map layer management and navigation infrastructure (#4921)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-26 00:29:24 +00:00
James Rich
b608a04ca4
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4912)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-25 16:37:36 -05:00
renovate[bot]
d62481a532
chore(deps): update actions/deploy-pages action to v5 (#4920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 16:37:26 -05:00
renovate[bot]
e81e84a544
chore(deps): update androidx (general) (#4919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 16:36:59 -05:00
renovate[bot]
eabab0b137
chore(deps): update kotlin ecosystem to v0.9.8 (#4917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 16:36:47 -05:00
renovate[bot]
705f617adb
chore(deps): update plugin com.gradle.develocity to v4.4.0 (#4918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 16:36:31 -05:00
renovate[bot]
c55d25bade
chore(deps): update androidx.room3:room3-gradle-plugin to v3.0.0-alpha02 (#4916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:00:14 +00:00
renovate[bot]
f622fae74f
chore(deps): update androidx (general) (#4915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 17:59:25 +00:00
James Rich
c470b9a366
Revise security policy for supported versions and reporting
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Updated the security policy to clarify supported versions and reporting process.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-25 10:18:51 -05:00
James Rich
8ce17defb7
refactor: remove demoscenario and enhance BLE connection stability (#4914)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-25 14:24:42 +00:00
James Rich
6516287c62
refactor: BLE transport and UI for Kotlin Multiplatform unification (#4911)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-25 02:15:51 +00:00
James Rich
b0e91a390c
feat: implement unified deep link routing for Kotlin Multiplatform (#4910)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-25 00:21:24 +00:00
James Rich
553ca2f8ed
feat: implement global SnackbarManager and consolidate common UI setup (#4909)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-24 22:31:40 +00:00
James Rich
9b8ac6a460
build(desktop): enable ProGuard minification and tree-shaking (#4904)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-24 22:22:37 +00:00
James Rich
0c3ab92908
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4907) 2026-03-24 22:21:59 +00:00
renovate[bot]
3a9f611fc0
chore(deps): update wire to v6.1.0 (#4906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 22:21:46 +00:00
renovate[bot]
cd328b236d
chore(deps): update kotest to v6.1.9 (#4908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 22:21:40 +00:00
James Rich
b45bc9be90
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4905) 2026-03-24 15:49:49 +00:00
renovate[bot]
41c82abc9e
chore(deps): update kotest to v6.1.8 (#4902)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 14:07:41 +00:00
James Rich
96060a0a4d
refactor: coroutine dispatchers and modernize testing infrastructure (#4901)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-24 01:31:48 +00:00
James Rich
664ebf218e
refactor: null safety, update date/time libraries, and migrate tests (#4900)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 23:17:50 +00:00
James Rich
f826cac6c8
refactor(di): specify disk cache directory for ImageLoader (#4899)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 21:05:50 +00:00
James Rich
a0b4c56505
feat: optimistically persist local configs and channels (#4898)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 19:57:45 +00:00
James Rich
57242d905c
refactor: Consolidate UI preference handling (#4895) 2026-03-23 19:36:02 +00:00
renovate[bot]
b4afe22030
chore(deps): update gradle/actions action to v6 (#4894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 18:41:41 +00:00
James Rich
82466be072
feat: Integrate AlertHost into desktop application and add UI tests (#4893) 2026-03-23 18:19:41 +00:00
renovate[bot]
d2c9036d24
chore(deps): update org.jetbrains.kotlinx:atomicfu to v0.32.1 (#4892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 12:37:21 -05:00
James Rich
7b327215f3
refactor: adaptive UI components for Navigation 3 (#4891) 2026-03-23 12:35:02 -05:00
James Rich
b3b38acc0b
feat: Migrate networking to Ktor and enhance multiplatform support (#4890)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 16:48:10 +00:00
James Rich
acb328dae3
refactor(desktop): remove native MenuBar from main window (#4888)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 12:20:07 +00:00
James Rich
cb89b111de
refactor(messaging): fix contact key derivation in ContactsViewModel (#4887)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-23 12:14:01 +00:00
James Rich
5a287f7133
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4884)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-22 15:56:10 +00:00
Dmitry
ee146df334
Add InlineMap implementation for F-Droid build (#4877) 2026-03-22 15:55:58 +00:00
James Rich
d5d4aa4577
refactor(service): update string formatting for local stats notif (#4885)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-22 15:55:30 +00:00
renovate[bot]
883ab5f7da
chore(deps): update core/proto/src/main/proto digest to cb1f893 (#4882)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 12:56:23 +00:00
James Rich
55729c9e2d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4880) 2026-03-22 12:52:15 +00:00
James Rich
27bcbcb7be
fix(settings): remove redundant regex option in DebugViewModel (#4881) 2026-03-22 07:41:52 -05:00
James Rich
fa98d20256
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4874)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
2026-03-22 00:59:35 -05:00
James Rich
c38bfc64de
Refactor command handling, enhance tests, and improve discovery logic (#4878)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-22 05:42:27 +00:00
James Rich
d136b162a4
feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876) 2026-03-21 23:19:13 +00:00
James Rich
f04924ded5
chore: Enhance CI coverage reporting and add main branch workflow (#4873) 2026-03-21 16:54:17 +00:00
James Rich
a331f96576
fix: specify jetbrains in gradle-daemon-jvm.properties (#4872) 2026-03-21 15:27:21 +00:00
James Rich
88d11aafec
fix: Update messaging feature with contact item keys and MQTT limits (#4871) 2026-03-21 14:25:23 +00:00
renovate[bot]
d61c0c9a67
chore(deps): update core/proto/src/main/proto digest to ee7df89 (#4868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 13:07:44 +00:00
James Rich
3bd8669cbe
fix: Implement reconnection logic and stabilize BLE connection flow (#4870) 2026-03-21 12:49:03 +00:00
James Rich
6e50db0b91
docs: Unify notification channel management and migrate unit tests (#4867) 2026-03-20 22:58:47 +00:00
renovate[bot]
b1e433e0dc
chore(deps): update androidx.sqlite:sqlite-bundled to v2.6.2 (#4785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 22:42:00 +00:00
renovate[bot]
05dc1703da
chore(deps): update org.jetbrains.kotlinx:atomicfu to v0.32.0 (#4866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 22:41:55 +00:00
James Rich
c4087c2ab7
feat: Migrate to Room 3.0 and update related documentation and tracks (#4865) 2026-03-20 21:40:08 +00:00
James Rich
6cdd10d936
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4863) 2026-03-20 13:35:13 +00:00
James Rich
0a3d2ec4b9
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4862) 2026-03-19 17:13:08 -05:00
James Rich
398e751a87
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4860) 2026-03-19 21:47:01 +00:00
James Rich
00697cc3c1
feat: KMP Debug Panel Migration and Update Documentation (#4859)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-19 19:07:03 +00:00
renovate[bot]
e36176bbf7
chore(deps): update io.github.g0dkar:qrcode-kotlin to v4.5.0 (#4858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 13:39:11 -05:00
renovate[bot]
89e38611d8
chore(deps): update com.google.firebase:firebase-bom to v34.11.0 (#4857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 13:36:43 -05:00
James Rich
1e55e554be
feat: Add KMP URI handling, import, and QR code generation support (#4856) 2026-03-19 13:36:19 -05:00
James Rich
4eb711ce58
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4853) 2026-03-19 12:36:58 -05:00
renovate[bot]
b7b973eaee
chore(deps): update org.jmdns:jmdns to v3.6.3 (#4855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 12:36:36 -05:00
James Rich
bc08093f6c
fix(map, settings): allow null IDs and implement request timeout (#4851) 2026-03-19 12:36:14 -05:00
James Rich
b982b145e6
feat: Implement KMP ServiceDiscovery for TCP devices (#4854) 2026-03-19 17:19:58 +00:00
renovate[bot]
a5d3914149
chore(deps): update jetbrains.lifecycle to v2.10.0 (#4852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 14:25:48 +00:00
renovate[bot]
ad06eafff7
chore(deps): update gradle to v9.4.1 (#4850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 14:15:38 +00:00
James Rich
cdba140eeb
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4846) 2026-03-19 12:30:01 +00:00
James Rich
06b9f8c77a
feat: Enhance test coverage (#4847)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-19 03:09:19 +00:00
James Rich
1b0dc75dfe
feat: Complete app module thinning and feature module extraction (#4844) 2026-03-19 00:21:18 +00:00
James Rich
dcbbc0823b
feat: Integrate Mokkery and Turbine into KMP testing framework (#4845) 2026-03-18 18:33:37 -05:00
James Rich
df3a094430
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4843) 2026-03-18 21:25:06 +00:00
renovate[bot]
5158d6c9d6
chore(deps): update static analysis to v8.4.0 (#4842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 21:24:59 +00:00
renovate[bot]
04a71c2743
chore(deps): update datadog to v3.8.0 (#4839)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 19:00:41 +00:00
James Rich
54b07d41de
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4840) 2026-03-18 18:54:03 +00:00
James Rich
d314ee2d8a
feat: mqtt (#4841) 2026-03-18 18:39:20 +00:00
Victorio Berra
eae5a6bdac
Add "Exclude MQTT" filter to Nodes view. (#4825)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
2026-03-18 15:39:59 +00:00
James Rich
1e9e838025
build: switch Java distribution from Zulu to JetBrains in GitHub Actions (#4838) 2026-03-18 14:36:50 +00:00
renovate[bot]
3263b0614f
chore(deps): update com.fazecast:jserialcomm to v2.11.4 (#4837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 08:28:25 -05:00
James Rich
59408ef46e
feat: Desktop USB serial transport (#4836)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-18 12:42:24 +00:00
renovate[bot]
06c990026f
chore(deps): update google maps compose to v8.2.2 (#4834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 03:18:02 +00:00
James Rich
49a6a1d4a9
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4833) 2026-03-18 03:17:50 +00:00
James Rich
cb95cace25
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4832) 2026-03-17 16:51:09 -05:00
James Rich
3bbb8a65ba
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4831) 2026-03-17 20:39:48 +00:00
renovate[bot]
afa7552141
chore(deps): update koin.plugin to v0.4.1 (#4763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 20:39:05 +00:00
James Rich
7d63f8b824
feat: build logic (#4829)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-17 20:35:39 +00:00
James Rich
807db83f53
feat: service extraction (#4828) 2026-03-17 14:06:01 -05:00
renovate[bot]
0d0bdf9172
chore(deps): update core/proto/src/main/proto digest to eba2d94 (#4830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:05:21 -05:00
renovate[bot]
0c3a841a80
chore(deps): update koin to v4.2.0 (#4827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 18:07:36 +00:00
renovate[bot]
190e62ce68
chore(deps): update datadog to v1.24.0 (#4826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 18:07:18 +00:00
James Rich
5eb6e501c0
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4822) 2026-03-17 14:25:38 +00:00
renovate[bot]
212acaecac
chore(deps): update core/proto/src/main/proto digest to bc8e638 (#4823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:25:30 +00:00
James Rich
a10fe61d0f
fix: resolve crashes and debug filter issues in Metrics and MapView (#4824)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-17 14:04:41 +00:00
James Rich
9ad28e924f
build: fix license generation and analytics build tasks (#4820)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-17 01:21:29 +00:00
James Rich
8c964a15ca
feat: Integrate notification management and preferences across platforms (#4819)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-17 01:17:34 +00:00
James Rich
0b2e89c46f
refactor: Replace Nordic, use Kable backend for Desktop and Android with BLE support (#4818)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-16 23:06:43 +00:00
James Rich
0e5f94579f
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4816) 2026-03-16 20:06:05 +00:00
James Rich
6e81ceec91
feat: Complete ViewModel extraction and update documentation (#4817) 2026-03-16 20:05:50 +00:00
Alexey Skobkin
80cae8e620
fix: fix wrong getChannelUrl() call causing loss of "add" flag and un… (#4809) 2026-03-16 09:03:17 -05:00
James Rich
5edb8abd05
feat: enhance map navigation and waypoint handling (#4814)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-16 13:48:00 +00:00
James Rich
802aa09aab
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4815) 2026-03-16 13:47:48 +00:00
renovate[bot]
2c52977683
chore(deps): update kotlin ecosystem to v2.3.20 (#4813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 13:07:41 +00:00
James Rich
4e64182afd
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4805) 2026-03-16 13:06:21 +00:00
James Rich
513dcc2f78
Revert "ci: Integrate Conveyor for cross-platform desktop packaging and simplify build" (#4804) 2026-03-14 13:25:28 -05:00
James Rich
e29fd596b6
ci: Integrate Conveyor for cross-platform desktop packaging and simplify build (#4802) 2026-03-14 12:44:55 -05:00
James Rich
fae6f83968
ci: Update Linux desktop distribution packaging and CI workflow (#4799) 2026-03-14 10:26:47 -05:00
James Rich
5610cc3924
ci(github-actions): install libfuse2t64 for Linux AppImage packaging (#4798) 2026-03-14 10:13:00 -05:00
James Rich
ac8119b086
ci(github): add Release environment to desktop release workflow (#4797) 2026-03-14 09:43:40 -05:00
James Rich
609d24a9e4
build(desktop): dynamically configure target formats based on host OS (#4796) 2026-03-14 09:38:34 -05:00
James Rich
b63192dccc
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4795) 2026-03-14 09:11:33 -05:00
James Rich
a8044a2402
build(desktop): refactor native distribution target formats (#4794) 2026-03-14 09:11:18 -05:00
James Rich
acf7aea098
feat(desktop): add enter-to-send functionality in messaging (#4793) 2026-03-14 08:43:25 -05:00
James Rich
bff87daaa7
ci(github-actions): include architecture in desktop artifact names (#4792) 2026-03-14 08:17:13 -05:00
James Rich
365e278335 ci(desktop): add ubuntu-24.04-arm to native distribution build 2026-03-14 07:32:54 -05:00
James Rich
8c6892a4da
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4791) 2026-03-14 06:44:36 -05:00
James Rich
832e785785 ci(release): update artifact glob pattern to be recursive
This commit updates the release workflow to ensure all files within the artifacts directory are correctly captured, regardless of nesting depth.

Specific changes include:
- Updated the `files` path in both draft and final release steps from `./artifacts/*/*` to `./artifacts/**/*` to support recursive file matching.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-14 06:44:03 -05:00
James Rich
06f002a198
refactor(settings): improve destination node handling in RadioConfigViewModel (#4790)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-14 02:55:46 +00:00
James Rich
2bfd225b68
build: Update desktop app icons, versioning, and packaging configuration (#4789) 2026-03-13 20:05:22 -05:00
James Rich
305466514a
build: remove PKG from desktop distribution targets (#4788) 2026-03-13 19:07:35 -05:00
James Rich
48740fe280
build(desktop): include java.net.http module in native distribution (#4787) 2026-03-13 19:02:29 -05:00
renovate[bot]
be70743ed6
chore(deps): update androidx.compose:compose-bom to v2026 (#4786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 18:13:26 -05:00
James Rich
427c0f3bbb
fix: fix animation stalls and update dependencies for stability (#4784)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 23:01:17 +00:00
James Rich
90844301e8
feat(desktop): expand supported native distribution formats (#4783) 2026-03-13 13:23:34 -05:00
James Rich
07ec771758
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4781) 2026-03-13 18:10:21 +00:00
James Rich
f45993ede2
feat(desktop): implement DI auto-wiring and validation (#4782)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 18:08:55 +00:00
renovate[bot]
8bb1e86511
chore(deps): update wire to v6.0.0 (#4778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 18:04:35 +00:00
James Rich
da11703ccd
ai: Establish conductor documentation and governance framework (#4780)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 16:38:25 +00:00
James Rich
b0f1f93c5a
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4777) 2026-03-13 16:33:41 +00:00
James Rich
5cc1e94a13
fix(ble): implement scanning for unbonded devices in common connections ui (#4779)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 16:33:30 +00:00
James Rich
afe1356430 build: streamline icon file configuration for desktop platforms
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-12 23:11:20 -05:00
James Rich
aacf5c69e9
Disable ProGuard for desktop release and add application icon (#4776) 2026-03-12 23:09:18 -05:00
James Rich
0ed9b6633b
build(ci): optimize release workflow and update Room configuration (#4775) 2026-03-12 22:46:01 -05:00
James Rich
eb3349fa11
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4773) 2026-03-12 22:15:20 -05:00
James Rich
20f358e01c
ci(release): pass app version to desktop build via environment variable (#4774) 2026-03-12 22:15:07 -05:00
James Rich
87e291f58d
build(desktop): enable ProGuard for release builds (#4772) 2026-03-12 21:57:29 -05:00
James Rich
3d93d0b4e3
build(github): switch Java distribution to Zulu across workflows (#4771) 2026-03-12 21:51:23 -05:00
James Rich
84bb6d24e4
docs: summarize KMP migration progress and architectural decisions (#4770)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 02:23:25 +00:00
James Rich
bdfd7b9251
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4766) 2026-03-13 01:51:02 +00:00
James Rich
3321c47200
ci: Update Dokka configuration and unify AboutLibraries JSON generation (#4767)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-13 01:49:11 +00:00
renovate[bot]
629d80ec65
chore(deps): update actions/upload-artifact action to v7 (#4768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 00:47:45 +00:00
renovate[bot]
3957b0823c
chore(deps): update dorny/paths-filter action to v4 (#4769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 00:47:40 +00:00
renovate[bot]
55cea44993
chore(deps): update jetbrains.adaptive to v1.3.0-alpha06 (#4764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 21:45:20 +00:00
James Rich
ac6bb5479b
feat: introduce Desktop target and expand Kotlin Multiplatform (KMP) architecture (#4761)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-12 21:14:49 +00:00
renovate[bot]
f4364cff9a
chore(deps): update google maps compose to v8.2.1 (#4758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:55:20 +00:00
renovate[bot]
3ccfcf644f
chore(deps): update androidx.datastore:datastore to v1.2.1 (#4755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:55:10 +00:00
renovate[bot]
6a1f3b197a
chore(deps): update com.squareup.okio:okio to v3.17.0 (#4759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:14:42 +00:00
renovate[bot]
c72e085f10
chore(deps): update koin to v4.2.0-rc2 (#4760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:14:03 +00:00
renovate[bot]
f70623db14
chore(deps): update androidx (general) (#4756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 13:53:02 +00:00
James Rich
a562f274bf
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4757) 2026-03-12 13:52:20 +00:00
James Rich
cfef01ccac
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4753) 2026-03-11 15:17:44 +00:00
James Rich
a902da4ca0
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4749) 2026-03-11 14:56:55 +00:00
renovate[bot]
7a1e1778f4
chore(deps): update compose.multiplatform to v1.11.0-alpha04 (#4751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 14:56:46 +00:00
renovate[bot]
2ef0547fb2
chore(deps): update ruby to v3.4.9 (#4752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 14:56:35 +00:00
James Rich
d076361c55
refactor: migrate core UI and features to KMP, adopt Navigation 3 (#4750)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-10 17:29:47 +00:00
James Rich
b1070321fe
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4748) 2026-03-09 20:45:41 -05:00
renovate[bot]
e3e010e3db
chore(deps): update vico to v3.0.3 (#4740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 01:22:08 +00:00
renovate[bot]
f86ba289d8
chore(deps): update core/proto/src/main/proto digest to cdde287 (#4742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 01:21:59 +00:00
James Rich
ee03b6d186
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4741) 2026-03-10 01:21:46 +00:00
James Rich
875cf1cff2
refactor: migrate from Hilt to Koin and expand KMP common modules (#4746)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-10 01:19:46 +00:00
James Rich
a5390a80e7
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4739) 2026-03-06 20:59:08 -06:00
James Rich
0ce322a0f5
feat: Migrate project to Kotlin Multiplatform (KMP) architecture (#4738)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-07 02:43:45 +00:00
James Rich
182ad933f4
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4737) 2026-03-06 16:39:05 -06:00
renovate[bot]
f663866d53
chore(deps): update kotlin ecosystem (#4736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 16:35:31 -06:00
James Rich
cffbd08806
refactor: migrate core modules to Kotlin Multiplatform and consolidat… (#4735)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-06 22:06:50 +00:00
renovate[bot]
f3775a601c
chore(deps): update datadog to v3.7.1 (#4734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 13:36:26 +00:00
James Rich
27e7dec69e
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4729) 2026-03-05 20:37:58 -06:00
James Rich
b9b68d2779
refactor: migrate preferences to DataStore and decouple core:domain for KMP (#4731) 2026-03-05 20:37:35 -06:00
James Rich
87fdaa26ff
refactor: enhance handshake stall guard and extend coverage to Stage 2 (#4730) 2026-03-05 16:06:21 -06:00
James Rich
dfab02bfb4
refactor(ble): increase default timeout for BLE profiling (#4728) 2026-03-05 14:56:49 -06:00
James Rich
a854c839e4
ci: Refine APK artifact paths and enable automatic release notes generation (#4727) 2026-03-05 14:51:47 -06:00
James Rich
9d9f95961d
ci: fix secrets context not allowed in if conditional (#4726) 2026-03-05 14:29:56 -06:00
James Rich
79a4a3671f
ci: fix internal builds release failing the workflow when secrets are missing (#4725) 2026-03-05 14:26:57 -06:00
James Rich
c9005432ea
ci: improve release cleanup and optimize build tasks (#4724) 2026-03-05 14:04:37 -06:00
James Rich
2e13b1ab17
ci: release flow tweaks (#4723) 2026-03-05 13:47:09 -06:00
James Rich
43f9aa0b50
ci: Remove environment from github-release job
Removed the environment specification for the github-release job.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-05 13:13:14 -06:00
James Rich
63984f0723
fix(widget): ensure local stats widget gets updates (#4722) 2026-03-05 19:00:30 +00:00
James Rich
68b2b6d88e
refactor(ble): improve connection lifecycle and enhance OTA reliability (#4721)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-05 18:58:34 +00:00
renovate[bot]
5a5aa1f026
chore(deps): update nordic.common to v2.9.2 (#4718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 12:46:01 -06:00
renovate[bot]
c1309545ea
chore(deps): update core/proto/src/main/proto digest to 2edc5ab (#4717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 18:18:52 +00:00
James Rich
af3f36b648
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4719) 2026-03-05 18:18:41 +00:00
James Rich
b0258d0cf1
feat: Add "Mark all as read" and unread message count indicators (#4720) 2026-03-05 18:18:34 +00:00
James Rich
6a1a612c38
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4716) 2026-03-05 12:57:22 +00:00
renovate[bot]
5fc7e46c29
chore(deps): update actions/upload-artifact action to v7 (#4714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 21:26:16 +00:00
James Rich
3e986032a5
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4709) 2026-03-04 21:14:18 +00:00
renovate[bot]
1e0613d520
chore(deps): update gradle/actions action to v5 (#4715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 21:13:13 +00:00
renovate[bot]
5b43dcb636
chore(deps): update actions/checkout action to v6 (#4712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 21:12:54 +00:00
James Rich
02e01bb331
ci: optimize, secure, and modernize CI pipeline (#4711) 2026-03-04 17:34:46 +00:00
github-actions[bot]
bd9c730c25 chore(release): prepare v2.7.14-internal.6 [skip ci]
- Bump base version to 2.7.14
- Sync translations and assets
2026-03-04 12:02:04 +00:00
renovate[bot]
6f393a56ec
chore(deps): update gradle to v9.4.0 (#4708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 05:59:39 -06:00
James Rich
7812e7df72
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4706) 2026-03-04 11:27:54 +00:00
renovate[bot]
7cdfff9ae8
chore(deps): update androidx.sqlite:sqlite-bundled to v2.6.2 (#4704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 11:27:34 +00:00
James Rich
7067806444
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4705) 2026-03-04 03:07:27 +00:00
James Rich
6a858acb4a
refactor: migrate :core:database to Room Kotlin Multiplatform (#4702) 2026-03-03 20:44:34 -06:00
renovate[bot]
744db2d5bd
chore(deps): update wire to v6.0.0-alpha03 (#4701)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-04 00:52:25 +00:00
James Rich
034c85d191
ci: update github-release permissions and environment settings (#4700) 2026-03-03 12:53:55 -06:00
James Rich
fe67219207
refactor: simplify traceroute tracking and unify cooldown button logic (#4699)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-03 18:34:12 +00:00
James Rich
17dcbed6b1
build: apply instrumented test dependencies conditionally (#4698) 2026-03-03 10:29:26 -06:00
renovate[bot]
8f055fda93
chore(deps): update datadog to v3.7.0 (#4697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 10:28:16 -06:00
renovate[bot]
be8f756694
chore(deps): update agp to v9.1.0 (#4694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 16:14:21 +00:00
James Rich
c234ace312
fix: ui tweaks (#4696)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-03 15:37:40 +00:00
renovate[bot]
657553f830
chore(deps): update com.android.tools:common to v32.1.0 (#4695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 15:32:41 +00:00
James Rich
05e2c5d457
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4692) 2026-03-03 15:19:40 +00:00
renovate[bot]
9dc1319845
chore(deps): update ktor to v3.4.1 (#4691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 07:56:31 -06:00
renovate[bot]
f4c2a37913
chore(deps): update core/proto/src/main/proto digest to a229208 (#4690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 07:55:58 -06:00
James Rich
4d21278514
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4689) 2026-03-03 13:16:23 +00:00
renovate[bot]
66d4cfb8c3
chore(deps): update static analysis to v8.3.0 (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:16:12 +00:00
James Rich
2c49db8041
feat/decoupling (#4685)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-03 13:15:28 +00:00
James Rich
40244f8337
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4686) 2026-03-03 00:34:50 +00:00
renovate[bot]
0fc3fd280e
chore(deps): update app.cash.turbine:turbine to v1.2.1 (#4682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 00:34:13 +00:00
renovate[bot]
bb37c66353
chore(deps): update co.touchlab:kermit to v2.1.0 (#4684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 21:57:48 +00:00
James Rich
728c300315
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4683) 2026-03-02 21:47:19 +00:00
James Rich
8c6bd8ab7a
feat: settings rework part 2, domain and usecase abstraction, tests (#4680)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-02 18:15:33 +00:00
renovate[bot]
5f31df96d8
chore(deps): update androidx.compose:compose-bom-alpha to v2026.02.01 (#4673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 18:15:23 +00:00
renovate[bot]
9ba4d50e60
chore(deps): update vico to v3.0.2 (#4675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 14:52:46 +00:00
James Rich
362ab6357c
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4672) 2026-03-02 14:52:29 +00:00
James Rich
fdd07f893f
feat: settings rework (#4678)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-03-02 14:51:05 +00:00
James Rich
b2b21e10e2
feat: upcoming support for tak and trafficmanagement configs, device hw (#4671)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
2026-02-27 17:44:19 +00:00
James Rich
a07992530c
feat: Improve edge-to-edge and display cutout handling (#4669) 2026-02-27 17:33:43 +00:00
James Rich
225dc232b6
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4667) 2026-02-27 17:25:41 +00:00
James Rich
145c256972
Merge pull request #4668
* Disable generate_release_notes in release workflow
2026-02-27 11:22:36 -06:00
1833 changed files with 95136 additions and 46129 deletions

27
.copilotignore Normal file
View file

@ -0,0 +1,27 @@
# Ignore build artifacts and generated files from Copilot indexing
# This saves context window tokens and prevents Copilot from hallucinating off of minified code.
# Build directories
**/build/**
.gradle/
.idea/
# Android generated files
**/generated/**
.cxx/
.externalNativeBuild/
# Git history & worktrees
.git/
.worktrees/
# Protobuf (Prevents Copilot from suggesting raw protobuf byte buffers)
core/proto/
# Environment and secrets
local.properties
secrets.properties
*.jks
# Agent References (Prevents pollution of project space with external code)
.agent_refs/

5
.gemini/settings.json Normal file
View file

@ -0,0 +1,5 @@
{
"context": {
"fileName": ["AGENTS.md", "GEMINI.md"]
}
}

View file

@ -1,19 +0,0 @@
name: 'Calculate Version Code'
description: 'Calculates the Android versionCode based on the Git commit count plus an offset.'
outputs:
versionCode:
description: "The calculated version code"
value: ${{ steps.calculate_version.outputs.VERSION_CODE }}
runs:
using: 'composite'
steps:
- name: Calculate Version Code
id: calculate_version
shell: bash
run: |
# This action assumes that the repo has been checked out with `fetch-depth: 0`
GIT_COMMIT_COUNT=$(git rev-list --count HEAD)
OFFSET=30630
VERSION_CODE=$((GIT_COMMIT_COUNT + OFFSET))
echo "Calculated versionCode: $VERSION_CODE (from $GIT_COMMIT_COUNT commits + $OFFSET offset)"
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_OUTPUT

40
.github/actions/gradle-setup/action.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Gradle Setup
description: Setup Java and Gradle for KMP builds
inputs:
cache_read_only:
description: 'Whether Gradle cache is read-only'
default: 'true'
jdk_distribution:
description: 'JDK distribution (temurin or jetbrains)'
default: 'temurin'
gradle_encryption_key:
description: 'Encryption key for Gradle remote cache'
required: false
runs:
using: composite
steps:
- name: Copy CI Gradle properties
shell: bash
run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: ${{ inputs.jdk_distribution }}
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ inputs.cache_read_only }}
cache-encryption-key: ${{ inputs.gradle_encryption_key }}
cache-cleanup: on-success
add-job-summary: always
gradle-home-cache-includes: |
caches
notifications
~/.m2/repository/org/robolectric

52
.github/ci-gradle.properties vendored Normal file
View file

@ -0,0 +1,52 @@
#
# CI-specific Gradle properties.
#
# This file is copied to ~/.gradle/gradle.properties by the gradle-setup
# composite action, overriding the dev-oriented values in the repo-root
# gradle.properties. Inspired by the nowinandroid & sqldelight patterns.
#
# ── Daemon ────────────────────────────────────────────────────────────
# Single-use CI runners never reuse a daemon, so the startup cost is pure
# overhead. Disabling it also avoids "daemon disappeared" warnings.
org.gradle.daemon=false
# ── Memory ────────────────────────────────────────────────────────────
# Standard GitHub runners have 7 GB RAM. Keep Gradle + Kotlin daemon
# within budget (4g Gradle + 2g Kotlin daemon + 1g OS/tooling headroom).
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8
kotlin.daemon.jvm.options=-Xmx2g -XX:+UseParallelGC
# ── Parallelism ───────────────────────────────────────────────────────
org.gradle.parallel=true
org.gradle.workers.max=4
# ── Caching & Configuration ──────────────────────────────────────────
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configureondemand=false
org.gradle.vfs.watch=false
org.gradle.isolated-projects=true
# ── Kotlin ────────────────────────────────────────────────────────────
# Incremental compilation is wasted on fresh CI checkouts (no prior build
# state to diff against). Disabling avoids the overhead of maintaining
# incremental state that will never be reused.
kotlin.incremental=false
kotlin.code.style=official
kotlin.parallel.tasks.in.project=true
# ── KSP ──────────────────────────────────────────────────────────────
# In CI, KSP incremental processing adds overhead without benefit (fresh
# checkouts). Keep intermodule incremental off (no prior state).
ksp.incremental=false
ksp.run.in.process=true
# ── Android ──────────────────────────────────────────────────────────
android.experimental.lint.analysisPerComponent=true
# Disable unused build features to reduce build time
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
# ── Misc ─────────────────────────────────────────────────────────────
org.gradle.welcome=never

View file

@ -0,0 +1,27 @@
# GitHub Copilot Commit Message Instructions
<role>
You are an expert Git maintainer enforcing Conventional Commits.
</role>
<instructions>
1. **Format:** Use the Conventional Commits format: `<type>(<scope>): <subject>` (Replace angle brackets with actual text, do NOT output angle brackets).
2. **Types allowed:**
- `feat` (new feature for the user, not a new feature for build script)
- `fix` (bug fix for the user, not a fix to a build script)
- `docs` (changes to the documentation)
- `style` (formatting, missing semi colons, etc; no production code change)
- `refactor` (refactoring production code, e.g. KMP migration, extracting to commonMain)
- `test` (adding missing tests, refactoring tests; no production code change)
- `chore` (updating grunt tasks etc; no production code change)
3. **Scope:** Use the module or logical component as the scope (e.g., `ui`, `navigation`, `ble`, `firmware`, `deps`, `ai`).
4. **Subject line:**
- Use the imperative, present tense: "change" not "changed" nor "changes".
- Do not capitalize the first letter.
- Do not use a period (.) at the end.
- Keep it under 50 characters if possible.
5. **Body (Optional but recommended for large diffs):**
- Leave one blank line after the subject.
- Explain *why* the change was made, not just *what* changed.
- If migrating to KMP or extracting to `commonMain`, explicitly state "Decoupled from Android framework".
</instructions>

View file

@ -1,183 +1,6 @@
# Copilot Instructions for Meshtastic-Android # Meshtastic Android - GitHub Copilot Guide
## Repository Summary > **Note:** The canonical instructions for all AI Agents have been deduplicated.
Meshtastic-Android is a native Android client application for the Meshtastic mesh networking project. It enables users to communicate via off-grid, decentralized mesh networks using LoRa radios. The app is written in Kotlin and follows modern Android development practices. You MUST immediately read and internalize the unified instructions located at the root of the repository in `AGENTS.md`.
After reading `AGENTS.md`, consult the `.skills/` directory for task-specific playbooks.
**Key Repository Details:**
- **Language:** Kotlin (primary), with some Java and AIDL files
- **Build System:** Gradle with Kotlin DSL
- **Size:** ~3MB source code across 3 modules
- **Target Platform:** Android API 26+ (Android 8.0+), targeting API 36
- **Architecture:** Modern Android with Jetpack Compose, Hilt DI, Room database
- **Product Flavors:** `fdroid` (F-Droid) and `google` (Google Play Store)
- **Build Types:** `debug` and `release`
## Essential Build & Test Commands
**ALWAYS run these commands in the exact order specified to avoid build failures:**
### Prerequisites Setup
1. **JDK Requirement:** JDK 17 is required (compatible with most developer environments)
2. **Secrets Configuration:** Copy `secrets.defaults.properties` to `local.properties` and update:
```properties
MAPS_API_KEY=your_google_maps_api_key_here
datadogApplicationId=your_datadog_app_id
datadogClientToken=your_datadog_client_token
```
3. **Clean Environment:** Always start with `./gradlew clean` for fresh builds
### Build Commands (Validated Working Order)
```bash
# 1. ALWAYS clean first for reliable builds
./gradlew clean
# 2. Check code formatting (run before making changes)
./gradlew spotlessCheck
# 3. Apply automatic code formatting fixes
./gradlew spotlessApply
# 4. Run static code analysis/linting
./gradlew detekt
# 5. Build debug APKs for both flavors (takes 3-5 minutes)
./gradlew assembleDebug
# 6. Build specific flavor variants
./gradlew assembleFdroidDebug # F-Droid debug build
./gradlew assembleGoogleDebug # Google debug build
./gradlew assembleFdroidRelease # F-Droid release build
./gradlew assembleGoogleRelease # Google release build
# 7. Run local unit tests (takes 2-3 minutes)
./gradlew test
# 8. Run specific flavor unit tests
./gradlew testFdroidDebug
./gradlew testGoogleDebug
# 9. Run instrumented tests (requires Android device/emulator, takes 5-10 minutes)
./gradlew connectedAndroidTest
# 10. Run lint checks for both flavors
./gradlew lintFdroidDebug lintGoogleDebug
```
### Time Requirements
- Clean build: 3-5 minutes
- Unit tests: 2-3 minutes
- Instrumented tests: 5-10 minutes
- Detekt analysis: 1-2 minutes
- Spotless formatting: 30 seconds
### Environment Setup
**Required Tools:**
- Android SDK API 36 (compile target)
- JDK 17 (Preferred for consistency across project and plugins)
- Gradle 9.0+ (downloaded automatically by wrapper)
**Optional but Recommended:**
- Install pre-push Git hook: `./gradlew spotlessInstallGitPrePushHook --no-configuration-cache`
## Project Architecture & Layout
### Module Structure
```
├── app/ # Main Android application
│ ├── src/main/ # Main source code
│ ├── src/test/ # Unit tests
│ ├── src/androidTest/ # Instrumented tests
│ ├── src/fdroid/ # F-Droid specific code
│ └── src/google/ # Google Play specific code
├── core/ # Core library modules
├── network/ # HTTP API networking library
├── mesh_service_example/ # AIDL service usage example
├── build-logic/ # Build configuration convention plugins
└── config/ # Linting and formatting configs
├── detekt/ # Detekt static analysis rules
└── spotless/ # Code formatting configuration
```
### Key Configuration Files
- `config.properties` - Version constants and build config
- `app/build.gradle.kts` - Main app build configuration
- `config/detekt/detekt.yml` - Static analysis rules
- `config/spotless/.editorconfig` - Code formatting rules
- `gradle.properties` - Gradle build settings
- `secrets.defaults.properties` - Template for secrets (copy to `local.properties`)
### Architecture Components
- **UI Framework:** Jetpack Compose with Material 3
- **State Management:** Unidirectional Data Flow with ViewModels
- **Dependency Injection:** Hilt
- **Navigation:** Jetpack Navigation Compose
- **Local Data:** Room database + DataStore preferences
- **Remote Data:** Custom Bluetooth/WiFi protocol + HTTP API (network module)
- **Background Work:** WorkManager
- **Communication:** AIDL service interface (`IMeshService.aidl`)
## Continuous Integration
### GitHub Workflows (.github/workflows/)
- **pull-request.yml** - Runs on every PR: build, detekt, tests
- **reusable-android-build.yml** - Shared build logic: spotless, detekt, lint, assemble, test
- **reusable-android-test.yml** - Instrumented tests on Android emulators (API 26, 35)
### CI Commands (Must Pass)
```bash
# Exact commands run in CI that must pass:
./gradlew :app:spotlessCheck :app:detekt :app:lintFdroidDebug :app:lintGoogleDebug :app:assembleDebug :app:testFdroidDebug :app:testGoogleDebug --configuration-cache --scan
./gradlew :app:connectedFdroidDebugAndroidTest :app:connectedGoogleDebugAndroidTest --configuration-cache --scan
```
### Validation Steps
1. **Code Style:** Spotless check (auto-fixable with `spotlessApply`)
2. **Static Analysis:** Detekt with custom rules in `config/detekt/detekt.yml`
3. **Lint Checks:** Android lint for both flavors
4. **Unit Tests:** JUnit tests in `app/src/test/`
5. **UI Tests:** Compose UI tests in `app/src/androidTest/`
## Common Issues & Solutions
### Build Failures
- **Gradle version error:** Ensure JDK 17 (Compatible version)
- **Missing secrets:** Copy `secrets.defaults.properties``local.properties`
- **Configuration cache:** Add `--no-configuration-cache` flag if issues persist
- **Clean state:** Always run `./gradlew clean` before debugging build issues
### Testing Issues
- **Instrumented tests:** Require Android device/emulator with API 26+
- **UI tests:** Use `ComposeTestRule` for Compose UI testing
- **Coroutine tests:** Use `kotlinx.coroutines.test` library
### Code Style Issues
- **Formatting:** Run `./gradlew spotlessApply` to auto-fix
- **Detekt warnings:** Check `config/detekt/detekt.yml` for rules
- **Localization:** Use `stringResource(Res.string.key)` instead of hardcoded strings
## File Organization
### Source Code Locations
- **Main Activity:** `app/src/main/java/com/geeksville/mesh/MainActivity.kt`
- **Service Interface:** `core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl`
- **UI Screens:** `feature/*/src/main/kotlin/org/meshtastic/feature/*/`
- **Data Layer:** `core/data/src/main/kotlin/org/meshtastic/core/data/`
- **Database:** `core/database/src/main/kotlin/org/meshtastic/core/database/`
- **Models:** `core/model/src/main/kotlin/org/meshtastic/core/model/`
### Dependencies
- **Non-obvious deps:** Protobuf for device communication, DataDog for analytics (Google flavor)
- **Flavor-specific:** Google Services (google flavor), no analytics (fdroid flavor)
- **Version catalog:** Dependencies defined in `gradle/libs.versions.toml`
## Agent Instructions
**TRUST THESE INSTRUCTIONS** - they are validated and comprehensive. Only search for additional information if:
1. Commands fail with unexpected errors
2. Information appears outdated
3. Working on areas not covered above
**Always prefer:** Using the documented commands over exploring alternatives, as they are tested and proven to work in the CI environment.
**For code changes:** Follow the architecture patterns established in existing code, maintain the modular structure, and ensure all validation steps pass before submitting changes.

View file

@ -0,0 +1,18 @@
# GitHub Copilot Pull Request Instructions
<role>
You are an expert open-source maintainer. Your goal is to write clear, professional, and highly structured Pull Request descriptions based on the provided diffs.
</role>
<instructions>
1. **Remove Boilerplate:** Always delete the "tips" section at the top of the `PULL_REQUEST_TEMPLATE.md` before generating your text.
2. **Context First:** Start with a clear, 1-2 sentence summary of *why* this change is being made. If the branch name or commits reference an issue (e.g., `fix-1234`), explicitly add `Fixes #1234` or `Resolves #1234`.
3. **Structured Changes:** Break down the code changes into bullet points categorized by:
- 🌟 **New Features** (UI, modules, logic)
- 🛠️ **Refactoring & Architecture** (KMP migrations, Koin DI updates)
- 🐛 **Bug Fixes**
- 🧹 **Chores** (Dependencies, formatting, docs)
4. **Architecture Callouts:** If the diff includes moving files from `androidMain` to `commonMain`, or migrating from Android Views to Compose, highlight this as a "KMP Migration Milestone".
5. **Testing Callouts:** If the diff includes changes to `commonTest` or mentions tests, add a section called "Testing Performed" and list the tests that were added/modified.
6. **No "Magic" Text:** Do not invent URLs or insert fake image placeholders. Leave the HTML comment block for images intact so the user can manually add their screenshots.
</instructions>

View file

@ -0,0 +1,11 @@
---
applyTo: "**/androidMain/**/*.kt"
---
# Android Source-Set Rules
- This is `androidMain` — Android framework imports (`android.*`, `java.*`) are allowed here.
- Do NOT put business logic here. Business logic belongs in `commonMain`.
- If you find identical pure-Kotlin logic in both `androidMain` and `jvmMain`, extract it to `commonMain`.
- Use `expect`/`actual` only for small platform primitives. Prefer interfaces + DI.
- Keep `expect` declarations in `FileIo.kt` and shared helpers in `FileIoUtils.kt` to avoid JVM duplicate class errors.

View file

@ -0,0 +1,10 @@
---
applyTo: "build-logic/**/*.kt"
---
# Build-Logic Convention Plugin Rules
- Prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs).
- Avoid `afterEvaluate` unless there is no viable lazy alternative.
- Check `gradle/libs.versions.toml` for version catalog aliases before adding new ones.
- Convention plugins: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`.

View file

@ -0,0 +1,14 @@
---
applyTo: "**/*.yml"
excludeAgent: "code-review"
---
# CI Workflow Rules
- Prefer explicit Gradle task paths (`app:lintFdroidDebug`) over shorthand (`lintDebug`).
- CI uses `.github/ci-gradle.properties` — don't assume local `gradle.properties` values.
- CI passes `-Pci=true` to enable full processor usage via `maxParallelForks`.
- Use `fetch-depth: 0` only where needed (spotless ratcheting, version code). Use `fetch-depth: 1` otherwise.
- Desktop build matrix: `macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`.
- Lightweight jobs (labelers, triage, stale): use `ubuntu-24.04-arm` runners.
- Gradle-heavy jobs: use `ubuntu-24.04` runners.

View file

@ -0,0 +1,20 @@
---
applyTo: "**/commonMain/**/*.kt"
---
# KMP commonMain Rules
- NEVER import `java.*` or `android.*` in `commonMain`.
- Use `org.meshtastic.core.common.util.ioDispatcher` instead of `Dispatchers.IO`.
- Use Okio (`BufferedSource`/`BufferedSink`) instead of `java.io.*`.
- Use `kotlinx.coroutines.sync.Mutex` instead of `java.util.concurrent.locks.*`.
- Use `atomicfu` or Mutex-guarded `mutableMapOf()` instead of `ConcurrentHashMap`.
- Use `jetbrains-*` catalog aliases for lifecycle/navigation dependencies.
- Use `compose-multiplatform-*` catalog aliases for CMP dependencies.
- Never use plain `androidx.compose` dependencies in `commonMain`.
- Strings: use `stringResource(Res.string.key)` from `core:resources`. No hardcoded strings.
- CMP `stringResource` only supports `%N$s` and `%N$d` — pre-format floats with `NumberFormatter.format()`.
- Use `MetricFormatter` from `core:common` for display strings (temperature, voltage, percent, signal). Avoid scattered `formatString("%.1f°C", val)` calls.
- Check `gradle/libs.versions.toml` before adding dependencies.
- Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. Keep `runCatching` only in cleanup/teardown code.
- Use `kotlinx.coroutines.CancellationException`, not `kotlin.coroutines.cancellation.CancellationException`.

35
.github/labeler.yml vendored
View file

@ -1,35 +0,0 @@
# Auto Labeler rulse using https://github.com/actions/labeler
#
# 'fix' in title/branch -> bug
# 'feat' in title/branch -> enhancement
# 'repo' in title/branch OR changes to ~/.github/ -> repo
# 'bug_fallthrough' for everything else except auto
#
# - [ ] need to look at title. waiting on https://github.com/actions/labeler/pull/866
# Add 'enhancement' label to any PR where the head branch name contains `feat`
enhancement:
- head-branch: [feat, Feat, FEAT]
# Add 'repo' label to any PR where the head branch name contains `repo`
# or files in the .github dir
repo:
- any:
- head-branch: [repo, Repo, REPO, ci, CI]
- changed-files:
- any-glob-to-any-file: .github
# Add 'bug' label to any PR where the head branch name contains `fix` or `bug` as the prefix.
bugfix:
- head-branch: [^fix, ^bug, ^Fix, ^FIX, ^Bug, ^BUG]
# Add `refactor` label to any PR where the head branch name contains `refactor` or `Refactor` as the prefix.
refactor:
- head-branch: [^refactor, ^Refactor]
# our fallback - bug except repo, feat, or automated pipelines
# bug_fallthrough:
# - all:
# - head-branch: ['^((?!feat).)*$', '^((?!repo).)*$', '^((?!renovate).)*$', '^((?!scheduled).)*$']

12
.github/lsp.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"lspServers": {
"kotlin": {
"command": "kotlin-language-server",
"args": [],
"fileExtensions": {
".kt": "kotlin",
".kts": "kotlin"
}
}
}
}

221
.github/renovate.json vendored
View file

@ -49,236 +49,31 @@
"automerge": true "automerge": true
}, },
{ {
"description": "Meshtastic Protobufs changelog link",
"matchPackageNames": [ "matchPackageNames": [
"https://github.com/meshtastic/protobufs.git" "https://github.com/meshtastic/protobufs.git"
], ],
"changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}", "changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}",
"groupName": "Meshtastic Protobufs",
"groupSlug": "meshtastic-protobufs",
"automerge": true "automerge": true
}, },
{ {
"description": "Group all AndroidX dependencies (excluding more specific AndroidX groups)", "description": "Group CMP and the androidx.compose artifacts that track it so Renovate bumps them together (see PR #5180)",
"groupName": "AndroidX (General)", "groupName": "compose-multiplatform",
"groupSlug": "androidx-general",
"matchPackageNames": [ "matchPackageNames": [
"/^androidx\\./", "/^org\\.jetbrains\\.compose/",
"!/^androidx\\.room/", "androidx.compose.runtime:runtime-tracing",
"!/^androidx\\.lifecycle/", "androidx.compose.ui:ui-test-manifest"
"!/^androidx\\.navigation/",
"!/^androidx\\.datastore/",
"!/^androidx\\.compose\\.material3\\.adaptive/",
"!/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/",
"!/^androidx\\.test\\.espresso/",
"!/^androidx\\.test\\.ext/",
"!/^androidx\\.compose\\.ui:ui-test-junit4$/",
"!/^androidx\\.hilt/"
] ]
}, },
{ {
"description": "Group Kotlin standard library, coroutines, and serialization", "description": "Restrict sensitive infrastructure to manual minor updates",
"groupName": "Kotlin Ecosystem",
"groupSlug": "kotlin",
"matchPackageNames": [
"/^org\\.jetbrains\\.kotlin/",
"/^org\\.jetbrains\\.kotlinx/"
]
},
{
"description": "Group Dagger and Hilt dependencies",
"groupName": "Dagger & Hilt",
"groupSlug": "hilt",
"matchPackageNames": [
"/^com\\.google\\.dagger/",
"/^androidx\\.hilt/"
]
},
{
"description": "Group Accompanist libraries",
"groupName": "Accompanist",
"groupSlug": "accompanist",
"matchPackageNames": [
"/^com\\.google\\.accompanist/"
]
},
{
"description": "Group JVM testing libraries (JUnit, Mockito, Robolectric)",
"groupName": "JVM Testing Libraries",
"groupSlug": "jvm-testing",
"matchPackageNames": [
"/^junit:junit$/",
"/^org\\.mockito:/",
"/^org\\.robolectric:robolectric$/"
],
"automerge": true
},
{
"description": "Group AndroidX Testing libraries",
"groupName": "AndroidX Testing",
"groupSlug": "androidx-testing",
"matchPackageNames": [
"/^androidx\\.test\\.espresso/",
"/^androidx\\.test\\.ext/",
"/^androidx\\.compose\\.ui:ui-test-junit4$/"
],
"automerge": true
},
{
"description": "Group Static Analysis tools (Detekt, Spotless)",
"groupName": "Static Analysis",
"groupSlug": "static-analysis",
"matchPackageNames": [
"/^io\\.gitlab\\.arturbosch\\.detekt/",
"/^io\\.nlopez\\.compose\\.rules/",
"/^com\\.diffplug\\.spotless/"
],
"automerge": true
},
{
"description": "Group Square networking libraries (OkHttp, Retrofit)",
"groupName": "Square Networking",
"groupSlug": "square-network",
"matchPackageNames": [
"/^com\\.squareup\\.okhttp3/",
"/^com\\.squareup\\.retrofit2/"
],
"automerge": true
},
{
"description": "Group Coil image loading library",
"groupName": "Coil",
"groupSlug": "coil",
"matchPackageNames": [
"/^io\\.coil-kt\\.coil3/"
],
"automerge": true
},
{
"description": "Group ZXing barcode scanning libraries",
"groupName": "ZXing",
"groupSlug": "zxing",
"matchPackageNames": [
"/^com\\.journeyapps:zxing-android-embedded/",
"/^com\\.google\\.zxing:core/"
],
"automerge": true
},
{
"description": "Group Eclipse Paho MQTT client libraries",
"groupName": "MQTT Paho Client",
"groupSlug": "mqtt-paho",
"matchPackageNames": [
"/^org\\.eclipse\\.paho/"
],
"automerge": true
},
{
"description": "Group Mike Penz Markdown renderer libraries",
"groupName": "Markdown Renderer (Mike Penz)",
"groupSlug": "markdown-renderer-mikepenz",
"matchPackageNames": [
"/^com\\.mikepenz/"
],
"automerge": true
},
{
"description": "Group Firebase libraries",
"groupName": "Firebase",
"groupSlug": "firebase",
"matchPackageNames": [
"/^com\\.google\\.firebase/"
],
"automerge": true
},
{
"description": "Group Datadog libraries",
"groupName": "Datadog",
"groupSlug": "datadog",
"matchPackageNames": [
"/^com\\.datadoghq/"
],
"automerge": true
},
{
"description": "Group OpenStreetMap (OSM) libraries",
"groupName": "OSM Libraries",
"groupSlug": "osm-libraries",
"matchPackageNames": [
"/^org\\.osmdroid/",
"/^com\\.github\\.MKergall\\.osmbonuspack/",
"/^mil\\.nga/"
],
"automerge": true
},
{
"description": "Group Google Maps Compose libraries",
"groupName": "Google Maps Compose",
"groupSlug": "google-maps-compose",
"matchPackageNames": [
"/^com\\.google\\.android\\.gms:play-services-location/",
"/^com\\.google\\.maps\\.android/"
],
"automerge": true
},
{
"description": "Group Google Protobuf runtime libraries",
"groupName": "Protobuf Runtime",
"groupSlug": "protobuf-runtime",
"matchPackageNames": [
"/^com\\.google\\.protobuf/",
"!https://github.com/meshtastic/protobufs.git"
]
},
{
"description": "Group AndroidX Room libraries",
"groupName": "AndroidX Room",
"groupSlug": "androidx-room",
"matchPackageNames": [
"/^androidx\\.room/"
],
"automerge": true
},
{
"description": "Group AndroidX Lifecycle libraries",
"groupName": "AndroidX Lifecycle",
"groupSlug": "androidx-lifecycle",
"matchPackageNames": [
"/^androidx\\.lifecycle/"
]
},
{
"description": "Group AndroidX Navigation libraries",
"groupName": "AndroidX Navigation",
"groupSlug": "androidx-navigation",
"matchPackageNames": [
"/^androidx\\.navigation/"
]
},
{
"description": "Group AndroidX DataStore libraries",
"groupName": "AndroidX DataStore",
"groupSlug": "androidx-datastore",
"matchPackageNames": [
"/^androidx\\.datastore/"
]
},
{
"description": "Group AndroidX Adaptive UI libraries",
"groupName": "AndroidX Adaptive UI",
"groupSlug": "androidx-adaptive-ui",
"matchPackageNames": [
"/^androidx\\.compose\\.material3\\.adaptive/",
"/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/"
]
},
{
"description": "Restrict sensitive infrastructure to patch updates only (manual minor)",
"matchUpdateTypes": [ "matchUpdateTypes": [
"minor" "minor"
], ],
"matchPackageNames": [ "matchPackageNames": [
"/^org\\.jetbrains\\.kotlin/", "/^org\\.jetbrains\\.kotlin/",
"/^org\\.jetbrains\\.kotlinx/", "/^org\\.jetbrains\\.kotlinx/",
"/^org\\.jetbrains\\.compose/",
"/^com\\.google\\.dagger/", "/^com\\.google\\.dagger/",
"/^androidx\\.hilt/", "/^androidx\\.hilt/",
"/^com\\.google\\.protobuf/", "/^com\\.google\\.protobuf/",

View file

@ -1,107 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
# push:
# branches: [ "main" ]
# pull_request:
# branches: [ "main" ]
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
if: github.repository == 'meshtastic/Meshtastic-Android'
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: java-kotlin
build-mode: autobuild
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
- name: Java Setup
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View file

@ -20,6 +20,11 @@ on:
required: true required: true
type: boolean type: boolean
default: false default: false
build_desktop:
description: 'Whether to build the desktop distribution'
required: true
type: boolean
default: false
permissions: permissions:
contents: write contents: write
@ -29,7 +34,7 @@ permissions:
jobs: jobs:
determine-tags: determine-tags:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
outputs: outputs:
tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }} tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }}
release_name: ${{ steps.calculate_tags.outputs.release_name }} release_name: ${{ steps.calculate_tags.outputs.release_name }}
@ -106,112 +111,6 @@ jobs:
fi fi
shell: bash shell: bash
- name: Update External Assets (Firmware, Hardware, Protos)
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
run: |
# Update Submodules (Protobufs)
echo "Updating core/proto submodule..."
git submodule update --init --remote core/proto
# Update Firmware List
firmware_file_path="app/src/main/assets/firmware_releases.json"
temp_firmware_file="/tmp/new_firmware_releases.json"
echo "Fetching latest firmware releases..."
curl -s --fail https://api.meshtastic.org/github/firmware/list > "$temp_firmware_file"
if ! jq empty "$temp_firmware_file" 2>/dev/null; then
echo "::error::Firmware API returned invalid JSON data. Aborting."
exit 1
else
if [ ! -f "$firmware_file_path" ] || ! jq --sort-keys . "$temp_firmware_file" | diff -q - <(jq --sort-keys . "$firmware_file_path"); then
echo "Changes detected in firmware list or local file missing. Updating $firmware_file_path."
cp "$temp_firmware_file" "$firmware_file_path"
else
echo "No changes detected in firmware list."
fi
fi
# Update Hardware List
hardware_file_path="app/src/main/assets/device_hardware.json"
temp_hardware_file="/tmp/new_device_hardware.json"
echo "Fetching latest device hardware data..."
curl -s --fail https://api.meshtastic.org/resource/deviceHardware > "$temp_hardware_file"
if ! jq empty "$temp_hardware_file" 2>/dev/null; then
echo "::error::Hardware API returned invalid JSON data. Aborting."
exit 1
else
if [ ! -f "$hardware_file_path" ] || ! jq --sort-keys . "$temp_hardware_file" | diff -q - <(jq --sort-keys . "$hardware_file_path"); then
echo "Changes detected in hardware list or local file missing. Updating $hardware_file_path."
cp "$temp_hardware_file" "$hardware_file_path"
else
echo "No changes detected in hardware list."
fi
fi
- name: Sync with Crowdin
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
uses: crowdin/github-action@v2
with:
base_url: 'https://meshtastic.crowdin.com/api/v2'
config: 'crowdin.yml'
crowdin_branch_name: 'main'
upload_sources: true
upload_sources_args: '--preserve-hierarchy'
upload_translations: false
download_translations: true
download_translations_args: '--preserve-hierarchy'
create_pull_request: false
push_translations: false
push_sources: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Commit Release Assets (Translations, Data, Config)
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
env:
FINAL_TAG: ${{ steps.calculate_tags.outputs.final_tag }}
run: |
# Calculate Version Code
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
COMMIT_COUNT=$(git rev-list --count HEAD)
# +1 because we are about to add a commit
VERSION_CODE=$((COMMIT_COUNT + OFFSET + 1))
echo "Calculated Version Code: $VERSION_CODE"
# Update VERSION_NAME_BASE in config.properties
sed -i "s/^VERSION_NAME_BASE=.*/VERSION_NAME_BASE=${{ inputs.base_version }}/" config.properties
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Add updated data files
git add config.properties
git add app/src/main/assets/firmware_releases.json || true
git add app/src/main/assets/device_hardware.json || true
git add core/proto || true
# Add updated translations (fastlane metadata and strings)
git add fastlane/metadata/android || true
git add "**/strings.xml" || true
# Only commit if there are changes
if ! git diff --cached --quiet; then
git commit -m "chore(release): prepare $FINAL_TAG [skip ci]
- Bump base version to ${{ inputs.base_version }}
- Sync translations and assets"
git push origin HEAD:${{ github.ref_name }}
else
echo "No changes to commit."
fi
shell: bash
- name: Create and Push Release Tag - name: Create and Push Release Tag
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }} if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
env: env:
@ -230,6 +129,7 @@ jobs:
tag_name: ${{ needs.determine-tags.outputs.final_tag }} tag_name: ${{ needs.determine-tags.outputs.final_tag }}
channel: ${{ inputs.channel }} channel: ${{ inputs.channel }}
base_version: ${{ inputs.base_version }} base_version: ${{ inputs.base_version }}
build_desktop: ${{ inputs.build_desktop }}
secrets: inherit secrets: inherit
call-promote-workflow: call-promote-workflow:
@ -244,3 +144,23 @@ jobs:
base_version: ${{ inputs.base_version }} base_version: ${{ inputs.base_version }}
from_channel: ${{ needs.determine-tags.outputs.from_channel }} from_channel: ${{ needs.determine-tags.outputs.from_channel }}
secrets: inherit secrets: inherit
cleanup-on-failure:
needs: [determine-tags, call-release-workflow]
if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }}
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Delete Failed or Cancelled Tag
env:
FINAL_TAG: ${{ needs.determine-tags.outputs.final_tag }}
run: |
if [ -n "$FINAL_TAG" ]; then
echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..."
git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted."
else
echo "No tag was created to delete."
fi

View file

@ -10,19 +10,20 @@ permissions:
jobs: jobs:
dependency-submission: dependency-submission:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
if: github.repository == 'meshtastic/Meshtastic-Android' if: github.repository == 'meshtastic/Meshtastic-Android'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: actions/setup-java@v5 - uses: actions/setup-java@v5
with: with:
distribution: jetbrains distribution: temurin
java-version: 17 java-version: 21
token: ${{ github.token }}
- name: Generate and submit dependency graph - name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v5 uses: gradle/actions/dependency-submission@v6
with: with:
build-scan-publish: true build-scan-publish: true
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use" build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
build-scan-terms-of-use-agree: "yes" build-scan-terms-of-use-agree: "yes"

View file

@ -6,6 +6,16 @@ on:
push: push:
branches: branches:
- main - main
paths:
# Only rebuild docs when source code changes (Dokka generates from KDoc)
- 'app/src/**'
- 'core/**/src/**'
- 'feature/**/src/**'
- 'desktop/src/**'
- 'build-logic/**'
- 'build.gradle.kts'
- 'settings.gradle.kts'
- '.github/workflows/docs.yml'
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@ -29,16 +39,16 @@ permissions:
pages: write pages: write
id-token: write id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # Allow only one concurrent deployment; cancel queued runs since only the latest
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. # main state matters for documentation.
concurrency: concurrency:
group: "pages" group: "pages"
cancel-in-progress: false cancel-in-progress: true
jobs: jobs:
build-docs: build-docs:
if: github.repository == 'meshtastic/Meshtastic-Android' if: github.repository == 'meshtastic/Meshtastic-Android'
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
@ -47,20 +57,16 @@ jobs:
submodules: 'recursive' submodules: 'recursive'
ref: ${{ inputs.ref || '' }} ref: ${{ inputs.ref || '' }}
- name: Set up JDK 17 - name: Gradle Setup
uses: actions/setup-java@v5 uses: ./.github/actions/gradle-setup
with: with:
java-version: '17' gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
distribution: 'jetbrains'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Build Dokka HTML documentation - name: Build Dokka HTML documentation
run: ./gradlew dokkaGeneratePublicationHtml run: ./gradlew dokkaGeneratePublicationHtml
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v4 uses: actions/upload-pages-artifact@v5
with: with:
path: build/dokka/html path: build/dokka/html
@ -69,9 +75,9 @@ jobs:
environment: environment:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
needs: build-docs needs: build-docs
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v5

26
.github/workflows/main-check.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Main CI (Verify & Build)
on:
push:
branches: [ main ]
paths-ignore:
- '**/*.md'
- 'docs/**'
permissions:
contents: read
concurrency:
group: main-${{ github.ref }}
cancel-in-progress: true
jobs:
validate-and-build:
if: github.repository == 'meshtastic/Meshtastic-Android'
uses: ./.github/workflows/reusable-check.yml
with:
run_lint: true
run_unit_tests: false
run_desktop_builds: false
upload_artifacts: true
secrets: inherit

View file

@ -5,6 +5,10 @@ on:
branches: branches:
- main - main
permissions:
contents: write
pull-requests: read
concurrency: concurrency:
group: main-push-${{ github.ref }} group: main-push-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -12,7 +16,7 @@ concurrency:
jobs: jobs:
main-push-changelog: main-push-changelog:
name: Generate main push changelog name: Generate main push changelog
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
@ -35,6 +39,10 @@ jobs:
fromTag: ${{ steps.last_prod_tag.outputs.tag }} fromTag: ${{ steps.last_prod_tag.outputs.tag }}
toTag: ${{ github.sha }} toTag: ${{ github.sha }}
outputFile: main-push-changelog.md outputFile: main-push-changelog.md
fetchViaCommits: true
fetchReviewers: false
fetchReleaseInformation: false
fetchReviews: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,6 +4,9 @@ on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
permissions:
contents: read
concurrency: concurrency:
group: build-mq-${{ github.ref }} group: build-mq-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -13,14 +16,15 @@ jobs:
if: github.repository == 'meshtastic/Meshtastic-Android' if: github.repository == 'meshtastic/Meshtastic-Android'
uses: ./.github/workflows/reusable-check.yml uses: ./.github/workflows/reusable-check.yml
with: with:
api_levels: '[26, 35]' # Comprehensive testing for Merge Queue run_lint: true
flavors: '["google", "fdroid"]' run_unit_tests: true
upload_artifacts: false upload_artifacts: false
secrets: inherit secrets: inherit
check-workflow-status: check-workflow-status:
name: Check Workflow Status name: Check Workflow Status
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
permissions: {}
needs: needs:
- android-check - android-check
if: always() if: always()

View file

@ -14,8 +14,8 @@ concurrency:
jobs: jobs:
triage: triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.issue.user.type != 'Bot' }}
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
@ -38,7 +38,7 @@ jobs:
- name: Apply quality label if needed - name: Apply quality label if needed
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8 uses: actions/github-script@v9
env: env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }} QUALITY_LABEL: ${{ steps.quality.outputs.response }}
with: with:
@ -80,7 +80,7 @@ jobs:
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
- name: Determine if completeness check should be skipped - name: Determine if completeness check should be skipped
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
uses: actions/github-script@v8 uses: actions/github-script@v9
id: check-skip id: check-skip
with: with:
script: | script: |
@ -98,20 +98,20 @@ jobs:
continue-on-error: true continue-on-error: true
with: with:
prompt: | prompt: |
Analyze this GitHub issue for completeness and determine if it needs labels. Analyze this GitHub issue for the Meshtastic Android app and determine if it needs labels.
If this looks like a bug on the device/firmware (crash, reboot, lockup, radio issues, GPS issues, display issues, power/sleep issues), request device logs and explain how to get them: If this looks like a bug in the Android app (crash, ANR, UI glitch, connection failure, Bluetooth issues, notification problems, map issues), request app logs and explain how to get them:
Web Flasher logs: Android app debug logs:
- Go to https://flasher.meshtastic.org - Open the Meshtastic app, go to Settings > Debug > Save Logs
- Connect the device via USB and click Connect - Reproduce the problem, then share/attach the exported log file
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
Meshtastic CLI logs: Android logcat (if app logs are insufficient):
- Run: meshtastic --port <serial-port> --noproto - Connect phone via USB with USB debugging enabled
- Reproduce the problem, then copy/paste the terminal output - Run: adb logcat -s Meshtastic:* *:E
- Reproduce the problem, then copy/paste the relevant output
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. Also request key context if missing: Android version, phone model, app version, Meshtastic device model, firmware version, connection type (BLE/USB/TCP), steps to reproduce, expected vs actual.
Respond ONLY with JSON: Respond ONLY with JSON:
{ {
@ -120,7 +120,7 @@ jobs:
"label": "needs-logs" | "needs-info" | "none" "label": "needs-logs" | "needs-info" | "none"
} }
Use "needs-logs" if this is a device bug AND no logs are attached. Use "needs-logs" if this is an app bug AND no logs are attached.
Use "needs-info" if basic info like firmware version or steps to reproduce are missing. Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
Use "none" if the issue is complete or is a feature request. Use "none" if the issue is complete or is a feature request.
@ -131,7 +131,7 @@ jobs:
- name: Process analysis result - name: Process analysis result
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
uses: actions/github-script@v8 uses: actions/github-script@v9
id: process id: process
env: env:
AI_RESPONSE: ${{ steps.analysis.outputs.response }} AI_RESPONSE: ${{ steps.analysis.outputs.response }}
@ -165,7 +165,7 @@ jobs:
- name: Apply triage label - name: Apply triage label
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
uses: actions/github-script@v8 uses: actions/github-script@v9
env: env:
LABEL_NAME: ${{ steps.process.outputs.label }} LABEL_NAME: ${{ steps.process.outputs.label }}
with: with:
@ -191,7 +191,7 @@ jobs:
- name: Comment on issue - name: Comment on issue
if: steps.process.outputs.should_comment == 'true' if: steps.process.outputs.should_comment == 'true'
uses: actions/github-script@v8 uses: actions/github-script@v9
env: env:
COMMENT_BODY: ${{ steps.process.outputs.comment_body }} COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
with: with:

View file

@ -15,19 +15,19 @@ concurrency:
jobs: jobs:
triage: triage:
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.pull_request.user.type != 'Bot' }}
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
# Step 1: Check if PR already has automation/type labels (skip if so) # Step 1: Check if PR already has automation/type labels (skip if so)
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
- name: Check existing labels - name: Check existing labels
uses: actions/github-script@v8 uses: actions/github-script@v9
id: check-labels id: check-labels
with: with:
script: | script: |
const skipLabels = new Set(['automation']); const skipLabels = new Set(['automation', 'release']);
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor']);
const prLabels = context.payload.pull_request.labels.map(l => l.name); const prLabels = context.payload.pull_request.labels.map(l => l.name);
const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
@ -44,13 +44,16 @@ jobs:
uses: actions/ai-inference@v2 uses: actions/ai-inference@v2
id: quality id: quality
continue-on-error: true continue-on-error: true
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with: with:
max-tokens: 20 max-tokens: 20
prompt: | prompt: |
Is this GitHub pull request spam, AI-generated slop, or low quality? Is this GitHub pull request spam, AI-generated slop, or low quality?
Title: ${{ github.event.pull_request.title }} Title: ${{ env.PR_TITLE }}
Body: ${{ github.event.pull_request.body }} Body: ${{ env.PR_BODY }}
Respond with exactly one of: spam, ai-generated, needs-review, ok Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
@ -58,7 +61,7 @@ jobs:
- name: Apply quality label if needed - name: Apply quality label if needed
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
uses: actions/github-script@v8 uses: actions/github-script@v9
id: quality-label id: quality-label
env: env:
QUALITY_LABEL: ${{ steps.quality.outputs.response }} QUALITY_LABEL: ${{ steps.quality.outputs.response }}
@ -87,32 +90,35 @@ jobs:
core.setOutput('is_spam', 'true'); core.setOutput('is_spam', 'true');
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) # Step 3: Auto-label PR type (bugfix/enhancement/refactor)
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
- name: Classify PR for labeling - name: Classify PR for labeling
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '')
uses: actions/ai-inference@v2 uses: actions/ai-inference@v2
id: classify id: classify
continue-on-error: true continue-on-error: true
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with: with:
max-tokens: 30 max-tokens: 30
prompt: | prompt: |
Classify this pull request into exactly one category. Classify this pull request for the Meshtastic Android app into exactly one category.
Return exactly one of: bugfix, hardware-support, enhancement Return exactly one of: bugfix, enhancement, refactor
Use bugfix if it fixes a bug, crash, or incorrect behavior. Use bugfix if it fixes a bug, crash, or incorrect behavior.
Use hardware-support if it adds or improves support for a specific hardware device/variant. Use enhancement if it adds a new feature, improves performance, or adds new functionality.
Use enhancement if it adds a new feature, improves performance, or refactors code. Use refactor if it restructures code without changing behavior, cleans up code, or improves architecture.
Title: ${{ github.event.pull_request.title }} Title: ${{ env.PR_TITLE }}
Body: ${{ github.event.pull_request.body }} Body: ${{ env.PR_BODY }}
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
model: openai/gpt-4o-mini model: openai/gpt-4o-mini
- name: Apply type label - name: Apply type label
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
uses: actions/github-script@v8 uses: actions/github-script@v9
env: env:
TYPE_LABEL: ${{ steps.classify.outputs.response }} TYPE_LABEL: ${{ steps.classify.outputs.response }}
with: with:
@ -120,8 +126,8 @@ jobs:
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
const labelMeta = { const labelMeta = {
'bugfix': { color: 'd73a4a', description: 'Bug fix' }, 'bugfix': { color: 'd73a4a', description: 'Bug fix' },
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
}; };
const meta = labelMeta[label]; const meta = labelMeta[label];
if (!meta) return; if (!meta) return;

View file

@ -9,7 +9,8 @@ on:
jobs: jobs:
spam-detection: spam-detection:
runs-on: ubuntu-latest if: github.repository == 'meshtastic/Meshtastic-Android'
runs-on: ubuntu-24.04-arm
permissions: permissions:
issues: write issues: write
pull-requests: write pull-requests: write

View file

@ -18,7 +18,7 @@ permissions:
jobs: jobs:
cleanup_prereleases: cleanup_prereleases:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
environment: Release environment: Release
steps: steps:
- name: Checkout code - name: Checkout code

View file

@ -4,29 +4,34 @@ on:
pull_request: pull_request:
types: [edited, labeled] types: [edited, labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions: permissions:
pull-requests: read pull-requests: read
contents: read contents: read
jobs: jobs:
check-label: check-label:
runs-on: ubuntu-latest # Skip bot PRs — they already have labels from the workflows/bots that create them
if: >-
github.event.pull_request.user.login != 'renovate[bot]' &&
github.event.pull_request.user.login != 'github-actions[bot]' &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
github.event.pull_request.head.ref != 'scheduled-updates' &&
github.event.pull_request.head.ref != 'l10n_main'
runs-on: ubuntu-24.04-arm
steps: steps:
- name: Check for PR labels - name: Check for PR labels
uses: actions/github-script@v8 uses: actions/github-script@v9
with: with:
script: | script: |
// Always fetch the latest labels from the GitHub API to avoid stale context // Extract labels from the payload directly to avoid extra API calls
const prNumber = context.payload.pull_request.number; const latestLabels = context.payload.pull_request.labels.map(label => label.name);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const latestLabels = pr.labels.map(label => label.name);
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor']; const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor'];
console.log('Labels from payload:', latestLabels);
const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label)); const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label));
console.log('Latest labels:', latestLabels);
if (!hasRequiredLabel) { if (!hasRequiredLabel) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
} }

View file

@ -65,9 +65,9 @@ permissions:
jobs: jobs:
prepare-build-info: prepare-build-info:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
outputs: outputs:
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }} APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
steps: steps:
- name: Checkout code - name: Checkout code
@ -77,9 +77,14 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Determine Version Name from Tag - name: Prep APP_VERSION_NAME
id: get_version_name id: prep_version
run: echo "APP_VERSION_NAME=$(echo ${{ inputs.tag_name }} | sed 's/-.*//' | sed 's/v//')" >> $GITHUB_OUTPUT env:
INPUT_TAG_NAME: ${{ inputs.tag_name }}
run: |
VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
echo "Parsed Version: $VERSION_NAME"
- name: Extract VERSION_CODE_OFFSET from config.properties - name: Extract VERSION_CODE_OFFSET from config.properties
id: get_version_code_offset id: get_version_code_offset
@ -97,7 +102,7 @@ jobs:
shell: bash shell: bash
promote-release: promote-release:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
environment: Release environment: Release
needs: [ prepare-build-info ] needs: [ prepare-build-info ]
steps: steps:
@ -111,7 +116,7 @@ jobs:
user-fraction: ${{ (inputs.channel == 'production' && '0.1') || (inputs.channel == 'open' && '0.5') || '1.0' }} user-fraction: ${{ (inputs.channel == 'production' && '0.1') || (inputs.channel == 'open' && '0.5') || '1.0' }}
update-github-release: update-github-release:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
needs: [ prepare-build-info, promote-release ] needs: [ prepare-build-info, promote-release ]
steps: steps:
- name: Checkout code - name: Checkout code
@ -134,6 +139,7 @@ jobs:
gh release edit ${{ inputs.tag_name }} \ gh release edit ${{ inputs.tag_name }} \
--tag ${{ inputs.final_tag }} \ --tag ${{ inputs.final_tag }} \
--title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \ --title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \
--draft=false \
--prerelease=${{ inputs.channel != 'production' }} --prerelease=${{ inputs.channel != 'production' }}
- name: Notify Discord - name: Notify Discord

View file

@ -12,7 +12,7 @@ on:
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
permissions: permissions:
contents: read contents: read
packages: write packages: write
@ -23,25 +23,25 @@ jobs:
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Set up JDK 17 - name: Gradle Setup
uses: actions/setup-java@v5 uses: ./.github/actions/gradle-setup
with: with:
java-version: '17' gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Configure Version - name: Configure Version
id: version id: version
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
VERSION_SUFFIX: ${{ inputs.version_suffix }}
run: | run: |
if [[ "${{ github.event_name }}" == "release" ]]; then if [[ "$EVENT_NAME" == "release" ]]; then
echo "VERSION_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV echo "VERSION_NAME=$RELEASE_TAG" >> $GITHUB_ENV
else else
# Use a timestamp-based version for manual/branch builds to avoid collisions # Use a timestamp-based version for manual/branch builds to avoid collisions
# or use the base version + suffix # or use the base version + suffix
BASE_VERSION=$(grep "VERSION_NAME_BASE" config.properties | cut -d'=' -f2) BASE_VERSION=$(grep "VERSION_NAME_BASE" config.properties | cut -d'=' -f2)
echo "VERSION_NAME=${BASE_VERSION}${{ inputs.version_suffix }}" >> $GITHUB_ENV echo "VERSION_NAME=${BASE_VERSION}${VERSION_SUFFIX}" >> $GITHUB_ENV
fi fi
- name: Publish to GitHub Packages - name: Publish to GitHub Packages

View file

@ -1,15 +1,67 @@
name: "Pull Request Labeler" name: "Pull Request Labeler"
on: on:
- pull_request_target pull_request_target:
# Do not execute arbitary code on this workflow. types: [opened, synchronize]
# Do not execute arbitrary code on this workflow.
# See warnings at https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target # See warnings at https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs: jobs:
labeler: labeler:
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
- id: label-the-PR - name: Auto-label PR
uses: actions/labeler@v6 uses: actions/github-script@v9
with:
script: |
const branch = context.payload.pull_request.head.ref;
const labels = new Set();
// enhancement: branch contains feat
if (/feat/i.test(branch)) labels.add('enhancement');
// bugfix: branch starts with fix or bug
if (/^(fix|bug)/i.test(branch)) labels.add('bugfix');
// refactor: branch starts with refactor
if (/^refactor/i.test(branch)) labels.add('refactor');
// repo: branch contains repo or ci
if (/repo|ci/i.test(branch)) {
labels.add('repo');
} else {
// Also label 'repo' if .github files were changed (needs one API call)
try {
const files = await github.paginate(
github.rest.pulls.listFiles,
{ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request.number, per_page: 100 },
(res) => res.data.map(f => f.filename)
);
if (files.some(f => f.startsWith('.github/'))) labels.add('repo');
} catch (e) {
core.warning(`Could not list PR files (rate limited?): ${e.message}`);
}
}
if (labels.size > 0) {
const labelArray = [...labels];
core.info(`Applying labels: ${labelArray.join(', ')}`);
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labelArray,
});
} catch (e) {
core.warning(`Could not apply labels (rate limited?): ${e.message}`);
}
} else {
core.info('No labels matched for this PR.');
}

View file

@ -1,72 +1,134 @@
name: Pull Request CI
on: on:
pull_request: pull_request:
branches: branches: [ main ]
- main
workflow_dispatch: permissions:
contents: read
concurrency: concurrency:
group: build-pr-${{ github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# 1. CHANGE DETECTION: Prevents unnecessary builds
check-changes: check-changes:
if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' )
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
outputs: outputs:
code_changed: ${{ steps.filter.outputs.code }} android: ${{ steps.filter.outputs.android }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v4
id: filter id: filter
with: with:
token: ''
filters: | filters: |
code: android:
- '**/*.kt' # CI/workflow implementation
- '**/*.java' - '.github/workflows/**'
- '**/*.xml' - '.github/actions/**'
- '**/*.kts' # Product modules validated by reusable-check
- '**/*.properties' - 'app/**'
- 'baselineprofile/**'
- 'desktop/**'
- 'core/**'
- 'feature/**'
# Shared build infrastructure
- 'build-logic/**'
- 'config/**'
- 'gradle/**' - 'gradle/**'
# Root build entrypoints/config that can alter task graph or outputs
- 'build.gradle.kts'
- 'config.properties'
- 'compose_compiler_config.conf'
- 'gradle.properties'
- 'gradlew' - 'gradlew'
- 'gradlew.bat' - 'gradlew.bat'
- '**/src/**' - 'settings.gradle.kts'
- '.github/workflows/**' - 'test.gradle.kts'
android-check: # 1b. FILTER DRIFT CHECK: Ensures check-changes stays aligned with module roots
verify-check-changes-filter:
if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' )
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v6
- name: Verify module roots are represented in check-changes filter
run: |
python3 - <<'PY'
import re
from pathlib import Path
settings = Path('settings.gradle.kts').read_text()
workflow = Path('.github/workflows/pull-request.yml').read_text()
module_roots = {
module.split(':')[0]
for module in re.findall(r'":([^"]+)"', settings)
}
allowed_extra_roots = {'baselineprofile'}
expected_roots = module_roots | allowed_extra_roots
filter_paths = {
path.split('/')[0]
for path in re.findall(r"-\s*'([^']+/\*\*)'", workflow)
}
actual_module_roots = filter_paths & expected_roots
missing = sorted(expected_roots - actual_module_roots)
unexpected = sorted(actual_module_roots - expected_roots)
if missing or unexpected:
print('check-changes filter drift detected:')
if missing:
print(' Missing roots:', ', '.join(missing))
if unexpected:
print(' Unexpected roots:', ', '.join(unexpected))
raise SystemExit(1)
print('check-changes filter is aligned with settings.gradle module roots.')
PY
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
# We disable coverage and desktop builds for PRs to keep feedback fast
# (< 10 mins). Desktop compilation is already covered by the :desktop:test
# task in the shard-app test shard.
validate-and-build:
needs: check-changes needs: check-changes
if: needs.check-changes.outputs.code_changed == 'true' if: needs.check-changes.outputs.android == 'true'
uses: ./.github/workflows/reusable-check.yml uses: ./.github/workflows/reusable-check.yml
with: with:
api_levels: '[35]' # Only test latest API on PRs for speed run_lint: true
flavors: '["google","fdroid"]' run_unit_tests: true
run_coverage: false
run_desktop_builds: false
upload_artifacts: true
secrets: inherit secrets: inherit
skip-notice: # 3. WORKFLOW STATUS: Ensures required checks are satisfied
needs: check-changes
if: needs.check-changes.outputs.code_changed != 'true'
runs-on: ubuntu-latest
steps:
- name: Skip CI for non-code changes
run: echo "Skipping CI - no code changes detected (docs/config only)"
check-workflow-status: check-workflow-status:
name: Check Workflow Status name: Check Workflow Status
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
needs: permissions: {}
- check-changes needs: [check-changes, verify-check-changes-filter, validate-and-build]
- android-check
if: always() if: always()
steps: steps:
- name: Check Workflow Status - name: Check Workflow Status
run: | run: |
if [[ "${{ needs.check-changes.outputs.code_changed }}" != "true" ]]; then if [[ "${{ needs.verify-check-changes-filter.result }}" == "failure" || "${{ needs.verify-check-changes-filter.result }}" == "cancelled" ]]; then
echo "No code changes - CI jobs skipped as expected" echo "::error::check-changes filter verification failed"
exit 0 exit 1
fi fi
if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then # If changes were detected but build failed, fail the status check
if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then
echo "::error::Android Check failed" echo "::error::Android Check failed"
exit 1 exit 1
fi fi
echo "All jobs passed successfully" # If no changes were detected, this still succeeds to satisfy required status check
echo "Workflow status satisfied."

View file

@ -19,6 +19,11 @@ on:
description: 'The channel to create a release for or promote to' description: 'The channel to create a release for or promote to'
required: true required: true
type: string type: string
build_desktop:
description: 'Whether to build the desktop distribution'
required: false
type: boolean
default: false
secrets: secrets:
GSERVICES: GSERVICES:
required: true required: true
@ -44,6 +49,10 @@ on:
required: false required: false
GRADLE_CACHE_PASSWORD: GRADLE_CACHE_PASSWORD:
required: false required: false
INTERNAL_BUILDS_HOST:
required: false
INTERNAL_BUILDS_HOST_PAT:
required: false
concurrency: concurrency:
group: ${{ github.workflow }}-${{ inputs.tag_name }} group: ${{ github.workflow }}-${{ inputs.tag_name }}
@ -56,23 +65,12 @@ permissions:
attestations: write attestations: write
jobs: jobs:
run-lint:
uses: ./.github/workflows/reusable-check.yml
with:
run_lint: true
run_unit_tests: false
run_instrumented_tests: false
flavors: '["google"]'
upload_artifacts: false
secrets: inherit
prepare-build-info: prepare-build-info:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
outputs: outputs:
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }} APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
env: env:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
@ -83,22 +81,14 @@ jobs:
ref: ${{ inputs.tag_name }} ref: ${{ inputs.tag_name }}
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Set up JDK 17 - name: Prep APP_VERSION_NAME
uses: actions/setup-java@v5 id: prep_version
with: env:
java-version: '17' INPUT_TAG_NAME: ${{ inputs.tag_name }}
distribution: 'jetbrains' run: |
- name: Setup Gradle VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
uses: gradle/actions/setup-gradle@v5 echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
with: echo "Parsed Version: $VERSION_NAME"
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Determine Version Name from Tag
id: get_version_name
run: echo "APP_VERSION_NAME=$(echo ${{ inputs.tag_name }} | sed 's/-.*//' | sed 's/v//')" >> $GITHUB_OUTPUT
- name: Extract VERSION_CODE_OFFSET from config.properties - name: Extract VERSION_CODE_OFFSET from config.properties
id: get_version_code_offset id: get_version_code_offset
@ -116,11 +106,10 @@ jobs:
shell: bash shell: bash
release-google: release-google:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
needs: [prepare-build-info, run-lint] needs: [prepare-build-info]
environment: Release environment: Release
env: env:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
@ -131,18 +120,12 @@ jobs:
ref: ${{ inputs.tag_name }} ref: ${{ inputs.tag_name }}
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5 - name: Gradle Setup
uses: ./.github/actions/gradle-setup
with: with:
java-version: '17' gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
distribution: 'jetbrains' cache_read_only: 'false'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Load secrets - name: Load secrets
env: env:
@ -167,7 +150,7 @@ jobs:
- name: Setup Fastlane - name: Setup Fastlane
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.4.8' ruby-version: '3.4.9'
bundler-cache: true bundler-cache: true
- name: Build and Deploy Google Play to Internal Track with Fastlane - name: Build and Deploy Google Play to Internal Track with Fastlane
@ -192,27 +175,26 @@ jobs:
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: google-apk name: google-apk
path: app/build/outputs/apk/**/*.apk path: app/build/outputs/apk/google/release/*.apk
retention-days: 1 retention-days: 1
- name: Attest Google AAB provenance - name: Attest Google AAB provenance
if: always() if: success()
uses: actions/attest-build-provenance@v4 uses: actions/attest-build-provenance@v4
with: with:
subject-path: app/build/outputs/bundle/googleRelease/app-google-release.aab subject-path: app/build/outputs/bundle/googleRelease/app-google-release.aab
- name: Attest Google APK provenance - name: Attest Google APK provenance
if: always() if: success()
uses: actions/attest-build-provenance@v4 uses: actions/attest-build-provenance@v4
with: with:
subject-path: app/build/outputs/apk/**/*.apk subject-path: app/build/outputs/apk/google/release/*.apk
release-fdroid: release-fdroid:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
needs: [prepare-build-info, run-lint] needs: [prepare-build-info]
environment: Release environment: Release
env: env:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
@ -223,18 +205,12 @@ jobs:
ref: ${{ inputs.tag_name }} ref: ${{ inputs.tag_name }}
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5 - name: Gradle Setup
uses: ./.github/actions/gradle-setup
with: with:
java-version: '17' gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
distribution: 'jetbrains' cache_read_only: 'false'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Load secrets - name: Load secrets
env: env:
@ -248,7 +224,7 @@ jobs:
- name: Setup Fastlane - name: Setup Fastlane
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.4.8' ruby-version: '3.4.9'
bundler-cache: true bundler-cache: true
- name: Build F-Droid with Fastlane - name: Build F-Droid with Fastlane
@ -265,24 +241,86 @@ jobs:
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: fdroid-apk name: fdroid-apk
path: app/build/outputs/apk/**/*.apk path: app/build/outputs/apk/fdroid/release/*.apk
retention-days: 1 retention-days: 1
- name: Attest F-Droid APK provenance - name: Attest F-Droid APK provenance
if: always() if: success()
uses: actions/attest-build-provenance@v4 uses: actions/attest-build-provenance@v4
with: with:
subject-path: app/build/outputs/apk/**/*.apk subject-path: app/build/outputs/apk/fdroid/release/*.apk
github-release: release-desktop:
runs-on: ubuntu-latest if: ${{ inputs.build_desktop }}
needs: [prepare-build-info, release-google, release-fdroid] runs-on: ${{ matrix.os }}
needs: [prepare-build-info]
environment: Release
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
env:
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
ref: ${{ inputs.tag_name }} ref: ${{ inputs.tag_name }}
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive'
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: 'false'
- name: Install dependencies for AppImage
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libfuse2
- name: Package Native Distributions
env:
ORG_GRADLE_PROJECT_appVersionName: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
APPIMAGE_EXTRACT_AND_RUN: 1
run: ./gradlew :desktop:packageReleaseDistributionForCurrentOS -PaboutLibraries.release=true --no-daemon
- name: List Desktop Binaries
if: runner.os == 'Linux'
run: ls -R desktop/build/compose/binaries/main-release
- name: Upload Desktop Artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: desktop-${{ runner.os }}-${{ runner.arch }}
path: |
desktop/build/compose/binaries/main-release/*/*.dmg
desktop/build/compose/binaries/main-release/*/*.msi
desktop/build/compose/binaries/main-release/*/*.exe
desktop/build/compose/binaries/main-release/*/*.deb
desktop/build/compose/binaries/main-release/*/*.rpm
desktop/build/compose/binaries/main-release/*/*.AppImage
retention-days: 1
if-no-files-found: ignore
github-release:
if: ${{ !cancelled() && !failure() }}
runs-on: ubuntu-24.04-arm
needs: [prepare-build-info, release-google, release-fdroid, release-desktop]
env:
INTERNAL_BUILDS_HOST: ${{ secrets.INTERNAL_BUILDS_HOST }}
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag_name }}
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v8
@ -290,23 +328,26 @@ jobs:
path: ./artifacts path: ./artifacts
- name: Create or Update GitHub Release - name: Create or Update GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
tag_name: ${{ inputs.tag_name }} tag_name: ${{ inputs.tag_name }}
target_commitish: ${{ inputs.commit_sha || github.sha }}
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}) name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
generate_release_notes: true generate_release_notes: true
files: ./artifacts/*/* files: ./artifacts/**/*
draft: true draft: true
prerelease: true prerelease: true
- name: Create or Update internal GitHub Release - name: Create or Update internal GitHub Release
uses: softprops/action-gh-release@v2 continue-on-error: true
if: ${{ env.INTERNAL_BUILDS_HOST != '' }}
uses: softprops/action-gh-release@v3
with: with:
repository: ${{ secrets.INTERNAL_BUILDS_HOST }} repository: ${{ secrets.INTERNAL_BUILDS_HOST }}
token: ${{ secrets.INTERNAL_BUILDS_HOST_PAT }} token: ${{ secrets.INTERNAL_BUILDS_HOST_PAT }}
tag_name: ${{ inputs.tag_name }} tag_name: ${{ inputs.tag_name }}
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}) name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
generate_release_notes: true generate_release_notes: false
files: ./artifacts/*/* files: ./artifacts/**/*
draft: false draft: false
prerelease: true prerelease: true

View file

@ -9,18 +9,12 @@ on:
run_unit_tests: run_unit_tests:
type: boolean type: boolean
default: true default: true
run_instrumented_tests: run_coverage:
type: boolean
default: true
run_desktop_builds:
type: boolean type: boolean
default: true default: true
flavors:
type: string
default: '["google"]'
api_levels:
type: string
default: '[35]'
num_shards:
type: number
default: 1
upload_artifacts: upload_artifacts:
type: boolean type: boolean
default: true default: true
@ -42,164 +36,280 @@ on:
GRADLE_CACHE_PASSWORD: GRADLE_CACHE_PASSWORD:
required: false required: false
env:
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
# Fallback VERSION_CODE for the lint-check job itself (which computes the real
# value from git). Downstream jobs override this with the git-derived value.
VERSION_CODE: ${{ github.run_number }}
jobs: jobs:
check: # ── Lint & Static Analysis ──────────────────────────────────────────
runs-on: ubuntu-latest lint-check:
timeout-minutes: 60 runs-on: ubuntu-24.04
strategy: permissions:
fail-fast: true contents: read
matrix: timeout-minutes: 30
api_level: ${{ fromJson(inputs.api_levels) }} outputs:
flavor: ${{ fromJson(inputs.flavors) }} cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
env: version_code: ${{ steps.version_code.outputs.version_code }}
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' filter: 'blob:none'
submodules: true
- name: Set up JDK 17 - name: Determine cache read-only setting
uses: actions/setup-java@v5 id: cache_config
with: shell: bash
java-version: '17'
distribution: 'jetbrains'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
- name: Calculate Version Code
id: calculate_version_code
uses: ./.github/actions/calculate-version-code
- name: Determine Tasks
id: tasks
run: | run: |
TASKS="" if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.event_name }}" == "merge_group" ]] || [[ "${{ github.ref }}" == gh-readonly-queue/* ]]; then
# Only run Lint and Unit Tests on the first API level and first flavor in the matrix to save time and resources echo "cache_read_only=false" >> "$GITHUB_OUTPUT"
IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}') else
IS_FIRST_FLAVOR=$(echo '${{ inputs.flavors }}' | jq -r '.[0] == "${{ matrix.flavor }}"') echo "cache_read_only=true" >> "$GITHUB_OUTPUT"
if [ "$IS_FIRST_API" = "true" ] && [ "$IS_FIRST_FLAVOR" = "true" ]; then
[ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS spotlessCheck detekt "
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testDebugUnitTest "
fi fi
FLAVOR="${{ matrix.flavor }}" - name: Calculate version code from git commit count
if [ "$IS_FIRST_API" = "true" ]; then id: version_code
if [ "$FLAVOR" = "google" ]; then shell: bash
TASKS="$TASKS assembleGoogleDebug "
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testGoogleDebugUnitTest "
elif [ "$FLAVOR" = "fdroid" ]; then
TASKS="$TASKS assembleFdroidDebug "
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testFdroidDebugUnitTest "
fi
fi
# Instrumented Test Tasks
if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then
[ "$IS_FIRST_FLAVOR" = "true" ] && TASKS="$TASKS connectedDebugAndroidTest "
if [ "$FLAVOR" = "google" ]; then
TASKS="$TASKS connectedGoogleDebugAndroidTest "
elif [ "$FLAVOR" = "fdroid" ]; then
TASKS="$TASKS connectedFdroidDebugAndroidTest "
fi
fi
# Run coverage report if unit tests were executed
if [ "${{ inputs.run_unit_tests }}" = "true" ] && [ "$IS_FIRST_API" = "true" ]; then
if [ "$IS_FIRST_FLAVOR" = "true" ]; then
TASKS="$TASKS koverXmlReportDebug "
fi
if [ "$FLAVOR" = "google" ]; then
TASKS="$TASKS koverXmlReportGoogleDebug "
elif [ "$FLAVOR" = "fdroid" ]; then
TASKS="$TASKS koverXmlReportFdroidDebug "
fi
fi
echo "tasks=$TASKS" >> $GITHUB_OUTPUT
echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT
- name: Enable KVM group perms
if: inputs.run_instrumented_tests == true
run: | run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules COMMIT_COUNT=$(git rev-list --count HEAD)
sudo udevadm control --reload-rules OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2 || echo 0)
sudo udevadm trigger --name-match=kvm VERSION_CODE=$((COMMIT_COUNT + OFFSET))
echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
- name: Run Check (with Emulator) - name: Gradle Setup
if: inputs.run_instrumented_tests == true uses: ./.github/actions/gradle-setup
uses: reactivecircus/android-emulator-runner@v2
env:
VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
with: with:
api-level: ${{ matrix.api_level }} gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
arch: x86_64 cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan
- name: Run Check (no Emulator) - name: Lint, Analysis & KMP Smoke Compile
if: inputs.run_instrumented_tests == false if: inputs.run_lint == true
env: run: ./gradlew spotlessCheck detekt app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan
VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan
- name: Upload coverage results to Codecov - name: KMP Smoke Compile (lint skipped)
if: ${{ !cancelled() }} if: inputs.run_lint == false
uses: codecov/codecov-action@v5 run: ./gradlew kmpSmokeCompile -Pci=true --continue --scan
# ── Sharded Unit Tests ──────────────────────────────────────────────
# Tests are split into 3 shards that run in parallel:
# shard-core: core:* KMP module tests (allTests)
# shard-feature: feature:* KMP module tests (allTests)
# shard-app: Pure-Android/JVM tests (app, desktop, core:barcode, etc.)
test-shards:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 45
needs: lint-check
if: inputs.run_unit_tests == true
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
strategy:
fail-fast: false
matrix:
shard:
- name: shard-core
tasks: >-
:core:ble:allTests
:core:common:allTests
:core:data:allTests
:core:database:allTests
:core:domain:allTests
:core:model:allTests
:core:navigation:allTests
:core:network:allTests
:core:prefs:allTests
:core:repository:allTests
:core:service:allTests
:core:takserver:allTests
:core:testing:allTests
:core:ui:allTests
kover: >-
:core:ble:koverXmlReport
:core:common:koverXmlReport
:core:data:koverXmlReport
:core:database:koverXmlReport
:core:domain:koverXmlReport
:core:model:koverXmlReport
:core:navigation:koverXmlReport
:core:network:koverXmlReport
:core:prefs:koverXmlReport
:core:repository:koverXmlReport
:core:service:koverXmlReport
:core:takserver:koverXmlReport
:core:testing:koverXmlReport
:core:ui:koverXmlReport
- name: shard-feature
tasks: >-
:feature:connections:allTests
:feature:firmware:allTests
:feature:intro:allTests
:feature:map:allTests
:feature:messaging:allTests
:feature:node:allTests
:feature:settings:allTests
kover: >-
:feature:connections:koverXmlReport
:feature:firmware:koverXmlReport
:feature:intro:koverXmlReport
:feature:map:koverXmlReport
:feature:messaging:koverXmlReport
:feature:node:koverXmlReport
:feature:settings:koverXmlReport
- name: shard-app
tasks: >-
:app:testFdroidDebugUnitTest
:app:testGoogleDebugUnitTest
:desktop:test
:core:barcode:testFdroidDebugUnitTest
:core:barcode:testGoogleDebugUnitTest
kover: >-
:app:koverXmlReportFdroidDebug
:app:koverXmlReportGoogleDebug
:core:barcode:koverXmlReportFdroidDebug
:core:barcode:koverXmlReportGoogleDebug
:desktop:koverXmlReport
steps:
- name: Checkout code
uses: actions/checkout@v6
with: with:
token: ${{ secrets.CODECOV_TOKEN }} fetch-depth: 1
slug: meshtastic/Meshtastic-Android submodules: true
files: "**/build/reports/kover/report*.xml"
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Run Tests & Coverage (${{ matrix.shard.name }})
run: |
kover_tasks=""
if [[ "${{ inputs.run_coverage }}" == "true" ]]; then
kover_tasks="${{ matrix.shard.kover }}"
fi
./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue --scan
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v6
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
flags: ${{ matrix.shard.name }}
fail_ci_if_error: false
report_type: test_results report_type: test_results
files: "**/build/test-results/**/*.xml,**/build/outputs/androidTest-results/**/*.xml" files: "**/build/test-results/**/*.xml"
- name: Upload debug artifact - name: Upload coverage to Codecov
if: ${{ steps.tasks.outputs.is_first_api == 'true' && inputs.upload_artifacts }} if: ${{ !cancelled() && inputs.run_coverage }}
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
flags: ${{ matrix.shard.name }}
fail_ci_if_error: false
files: "**/build/reports/kover/report*.xml"
- name: Upload shard reports
if: ${{ always() && inputs.upload_artifacts }}
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.flavor }}Debug name: reports-${{ matrix.shard.name }}
path: app/build/outputs/apk/${{ matrix.flavor }}/debug/app-${{ matrix.flavor }}-debug.apk path: |
retention-days: 14 **/build/reports
**/build/test-results
retention-days: 7
# ── Android Build ────────────────────────────────────────────────────
android-check:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 60
needs: lint-check
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Build Android APKs
run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
- name: Upload debug artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: app-debug-apks
path: app/build/outputs/apk/*/debug/*.apk
retention-days: 7
- name: Report App Size - name: Report App Size
if: always() && steps.tasks.outputs.is_first_api == 'true' if: always()
run: | run: |
echo "### 📦 App Size Report" >> $GITHUB_STEP_SUMMARY echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
- name: Upload reports # ── Desktop Build ───────────────────────────────────────────────────
if: ${{ always() && inputs.upload_artifacts }} build-desktop:
name: Build Desktop Debug (${{ matrix.os }})
if: inputs.run_desktop_builds == true
runs-on: ${{ matrix.os }}
permissions:
contents: read
timeout-minutes: 60
needs: lint-check
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
env:
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
submodules: true
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
- name: Build Desktop
run: ./gradlew :desktop:createDistributable -Pci=true --scan
- name: Upload Desktop artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: reports-${{ matrix.flavor }}-api-${{ matrix.api_level }} name: desktop-app-${{ runner.os }}-${{ runner.arch }}
path: | path: desktop/build/compose/binaries/main/app/
**/build/reports
**/build/test-results
**/build/outputs/androidTest-results
retention-days: 7 retention-days: 7

View file

@ -2,12 +2,12 @@ name: Scheduled Updates (Firmware, Hardware, Translations)
on: on:
schedule: schedule:
- cron: '0 * * * *' # Run every hour - cron: '0 */4 * * *' # Run every 4 hours (was hourly — reduced to cut cascade CI cost)
workflow_dispatch: # Allow manual triggering workflow_dispatch: # Allow manual triggering
jobs: jobs:
update_assets: update_assets:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
if: github.repository == 'meshtastic/Meshtastic-Android' if: github.repository == 'meshtastic/Meshtastic-Android'
permissions: permissions:
contents: write # To commit files and push branches contents: write # To commit files and push branches
@ -81,22 +81,11 @@ jobs:
- name: Fix file permissions - name: Fix file permissions
run: sudo chown -R $USER:$USER . run: sudo chown -R $USER:$USER .
- name: Set up JDK 17 - name: Gradle Setup
uses: actions/setup-java@v5 uses: ./.github/actions/gradle-setup
with: with:
java-version: '17' gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
distribution: 'jetbrains' cache_read_only: 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
- name: Update Graphs - name: Update Graphs
run: ./gradlew graphUpdate run: ./gradlew graphUpdate
@ -143,7 +132,8 @@ jobs:
check-workflow-status: check-workflow-status:
name: Check Workflow Status name: Check Workflow Status
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
permissions: {}
needs: needs:
- update_assets - update_assets
if: always() if: always()

View file

@ -12,7 +12,7 @@ permissions:
jobs: jobs:
stale_issues: stale_issues:
name: Close Stale Issues name: Close Stale Issues
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
if: github.repository == 'meshtastic/Meshtastic-Android' if: github.repository == 'meshtastic/Meshtastic-Android'
steps: steps:
@ -20,7 +20,7 @@ jobs:
uses: actions/stale@v10.2.0 uses: actions/stale@v10.2.0
with: with:
days-before-stale: 30 days-before-stale: 30
stale-issue-message: This issue hasn not had any comment or update in the last 30 days. If it is still relevant, please post update comments. If no comments are made, this issue will be closed in 7 days. stale-issue-message: This issue has not had any comment or update in the last 30 days. If it is still relevant, please post update comments. If no comments are made, this issue will be closed in 7 days.
exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies' exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies'
exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies' exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies'
operations-per-run: 100 operations-per-run: 100

8
.gitignore vendored
View file

@ -37,6 +37,9 @@ keystore.properties
/fastlane/play-store-credentials.json /fastlane/play-store-credentials.json
**/google-services.json **/google-services.json
# Generated library definitions
**/src/main/resources/aboutlibraries.json
/fastlane/report.xml /fastlane/report.xml
/build-logic/convention/build/* /build-logic/convention/build/*
@ -48,3 +51,8 @@ wireless-install.sh
# Git worktrees # Git worktrees
.worktrees/ .worktrees/
/firebase-debug.log.jdk/
firebase-debug.log
.agent_plans/
.agent_refs/
.agent_artifacts/

1
.jdk Symbolic link
View file

@ -0,0 +1 @@
/home/james/.jdks/ms-17.0.18

295
.pr5167.diff Normal file
View file

@ -0,0 +1,295 @@
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
new file mode 100644
index 0000000000..2a27b96906
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.common.di
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.ioDispatcher
+
+/**
+ * A process-wide [CoroutineScope] that outlives individual ViewModels and UI components.
+ *
+ * Use this scope for fire-and-forget cleanup work that must continue after a ViewModel's own scope has been cancelled
+ * (for example, deleting temporary files in `onCleared()`). Backed by a [SupervisorJob] so failures in one child do not
+ * cancel siblings, and by [ioDispatcher] so work runs off the main thread.
+ *
+ * Prefer scoping work to a more specific scope (like `viewModelScope`) whenever possible; this scope is an escape hatch
+ * and should be used sparingly.
+ */
+interface ApplicationCoroutineScope : CoroutineScope
+
+@Single(binds = [ApplicationCoroutineScope::class])
+internal class ApplicationCoroutineScopeImpl : ApplicationCoroutineScope {
+ override val coroutineContext = SupervisorJob() + ioDispatcher
+}
diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
index 231c84d401..5365ab95e2 100644
--- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
+++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
@@ -37,12 +37,12 @@ import androidx.lifecycle.compose.LifecycleEventEffect
import co.touchlab.kermit.Logger
import com.eygraber.uri.toAndroidUri
import com.eygraber.uri.toKmpUri
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.ioDispatcher
import java.net.URLEncoder
@Composable
@@ -146,7 +146,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
val context = LocalContext.current
return remember(context) {
{ uri, maxChars ->
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught")
try {
val androidUri = uri.toAndroidUri()
diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
index 031e1fe35d..a938f92ea6 100644
--- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
+++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
@@ -20,10 +20,10 @@ package org.meshtastic.core.ui.util
import androidx.compose.runtime.Composable
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.Desktop
import java.awt.FileDialog
import java.awt.Frame
@@ -89,7 +89,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
/** JVM — Reads text from a file URI. */
@Composable
actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars ->
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught")
try {
val file = File(URI(uri.toString()))
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
index dc1c459716..f8ff9fcac8 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
@@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.database.entity.FirmwareRelease
@@ -91,6 +92,7 @@ class FirmwareUpdateViewModel(
private val firmwareUpdateManager: FirmwareUpdateManager,
private val usbManager: FirmwareUsbManager,
private val fileHandler: FirmwareFileHandler,
+ private val applicationScope: ApplicationCoroutineScope,
) : ViewModel() {
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
@@ -124,12 +126,10 @@ class FirmwareUpdateViewModel(
override fun onCleared() {
super.onCleared()
- // viewModelScope is already cancelled when onCleared() runs, so launch cleanup in a
- // standalone scope. SupervisorJob prevents the coroutine from propagating failures to a
- // shared parent, and NonCancellable on the launch keeps cleanup running even if the scope
- // is cancelled concurrently.
- @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
- kotlinx.coroutines.GlobalScope.launch(NonCancellable) {
+ // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the
+ // application-wide scope (SupervisorJob + ioDispatcher). NonCancellable keeps cleanup
+ // running even if something tries to cancel it mid-flight.
+ applicationScope.launch(NonCancellable) {
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
}
}
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
index 4c48a1ced5..030d84effd 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
@@ -108,6 +108,7 @@ class FirmwareUpdateIntegrationTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
@Test
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
index 7032ed4088..a8eddff838 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
@@ -124,6 +124,7 @@ class FirmwareUpdateViewModelTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
@Test
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
new file mode 100644
index 0000000000..3ef5c44ef4
--- /dev/null
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.firmware
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
+
+internal class TestApplicationCoroutineScope(dispatcher: CoroutineDispatcher) :
+ ApplicationCoroutineScope,
+ CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
index acb1545bdd..23a0d03ab2 100644
--- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
+++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
@@ -116,6 +116,7 @@ class FirmwareUpdateViewModelFileTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
// -----------------------------------------------------------------------
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
index c251b4d5ef..315ad1da85 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
@@ -27,6 +27,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_export_failed
import org.meshtastic.core.resources.debug_export_success
@@ -48,7 +49,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
}
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<DebugViewModel.UiMeshLog>) =
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
try {
if (logs.isEmpty()) {
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") }
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
index 9afde85e5f..a28a576788 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
@Composable
actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit {
@@ -41,7 +41,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
return { fileName -> exportLauncher.launch(fileName) }
}
-private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(Dispatchers.IO) {
+private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(ioDispatcher) {
try {
context.contentResolver.openOutputStream(targetUri)?.use { os -> os.write(data) }
Logger.i { "TAK data package exported successfully to $targetUri" }
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
index 5b63cc90a3..a9a7285593 100644
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.debugging
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@@ -41,7 +41,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
return@launch
}
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
// Run file dialog to ask user where to save
val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE)
fileDialog.file = fileName
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
index 9fb71379fc..bfbb85bc0d 100644
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.tak
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@@ -44,7 +44,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
if (directory != null && file != null) {
val targetFile = File(directory, file)
val data = dataPackageProvider()
- withContext(Dispatchers.IO) { targetFile.writeBytes(data) }
+ withContext(ioDispatcher) { targetFile.writeBytes(data) }
Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" }
}
}

View file

@ -1 +1 @@
3.4.8 3.4.9

View file

@ -0,0 +1,66 @@
# Skill: Code Review
## Description
Perform comprehensive code reviews for `Meshtastic-Android`, ensuring changes adhere to KMP architecture, Kotlin Multiplatform conventions, MAD standards, and CMP best practices.
## Code Review Checklist
When reviewing code, meticulously verify the following categories. Flag any deviations and propose the canonical project pattern as a fix.
### 1. KMP Architecture & Source Set Boundaries
- [ ] **No Platform Bleed:** Ensure absolutely no `java.*` or `android.*` imports exist in `commonMain` source sets.
- [ ] **KMP Native Alternatives:** Verify the use of KMP alternatives for standard JVM libraries:
- `java.util.concurrent.locks.*` -> `kotlinx.coroutines.sync.Mutex`
- `java.util.concurrent.ConcurrentHashMap` -> `atomicfu` or Mutex-guarded `mutableMapOf()`
- `java.io.*` -> `Okio` (`BufferedSource`/`BufferedSink`)
- `java.util.Locale` -> Kotlin `uppercase()`/`lowercase()` (purged from `commonMain`)
- [ ] **Coroutine Safety:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` silently swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction). Use `kotlinx.coroutines.CancellationException` (not `kotlin.coroutines.cancellation.CancellationException`).
- [ ] **Shared Helpers:** If `androidMain` and `jvmMain` contain identical pure-Kotlin logic, mandate extracting it to a shared function in `commonMain`.
- [ ] **File Naming Conflicts:** For `expect`/`actual` declarations, ensure files sharing the same package namespace have distinct names (e.g., keep `expect` in `LogExporter.kt` and shared helpers in `LogFormatter.kt`) to avoid duplicate class errors on the JVM target.
- [ ] **Interface & DI Over `expect`/`actual`:** Check that `expect`/`actual` is reserved for small platform primitives. Interfaces + DI should be preferred for larger capabilities.
### 2. UI & Compose Multiplatform (CMP)
- [ ] **Compose Multiplatform Resources:** Ensure NO hardcoded strings. Must use `core:resources` (e.g., `stringResource(Res.string.key)` or asynchronous `getStringSuspend(Res.string.key)` for ViewModels/Coroutines). NEVER use blocking `getString()` in a coroutine.
- [ ] **String Formatting:** CMP only supports `%N$s` and `%N$d`. Flag any float formats (`%N$.1f`) in Compose string resources; they must be pre-formatted using `NumberFormatter.format()` from `core:common`. Use `MetricFormatter` for metric-specific displays (temperature, voltage, current, percent, humidity, pressure, SNR, RSSI).
- [ ] **Centralized Dialogs & Alerts:** Flag inline alert-rendering logic. Mandate the use of `AlertHost(alertManager)` or `SharedDialogs` from `core:ui/commonMain`.
- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features. No inline placeholders in feature modules.
- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktop/tablet breakpoints (≥ 1200dp).
### 3. Navigation & State
- [ ] **Shared Navigation Graphs:** Feature navigation graphs must be defined as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(...)`). Flag any graphs defined in platform-specific source sets.
- [ ] **Navigation Host:** Ensure `MeshtasticNavDisplay` (from `core:ui/commonMain`) is used as the host instead of invoking `NavDisplay` directly. Host modules should not configure `entryDecorators` themselves.
- [ ] **ViewModel Scoping:** ViewModels obtained via `koinViewModel()` must be inside `entry<T>` blocks to correctly tie to the backstack lifetime.
### 4. Dependency Injection (Koin Annotations)
- [ ] **Annotation Usage:** Ensure Koin is configured via annotations (`@Single`, `@Factory`, `@KoinViewModel`).
- [ ] **Root Assembly:** Confirm that the root Koin DI graph is only assembled in host shells (`app` and `desktop`).
### 5. Networking, DB & I/O
- [ ] **Ktor Strictly:** Check that Ktor is used for all HTTP networking. Flag and reject any usage of OkHttp.
- [ ] **HTTP Configuration:** Verify timeouts and base URLs use `HttpClientDefaults` from `core:network`. Never hardcode timeouts in feature modules. `DefaultRequest` sets the base URL; feature API services use relative paths.
- [ ] **Image Loading (Coil):** Coil must use `coil-network-ktor3` in host modules. Feature modules should ONLY depend on `libs.coil` (coil-compose) and never configure fetchers.
- [ ] **Room KMP:** Ensure `factory = { MeshtasticDatabaseConstructor.initialize() }` is used in `Room.databaseBuilder`. DAOs and Entities must reside in `commonMain`.
- [ ] **Room Patterns:** Verify use of `@Upsert` for insert-or-update logic. Check for `LIMIT 1` on single-row queries. Flag N+1 query patterns (loops calling single-row queries) — batch with chunked `WHERE IN` instead.
- [ ] **Bluetooth (BLE):** All Bluetooth communication must be routed through `core:ble` using Kable abstractions.
### 6. Dependency Catalog Aliases
- [ ] **JetBrains vs. AndroidX:**
- In `commonMain`: Must use `jetbrains-*` aliases (e.g., `jetbrains-lifecycle-*`, `jetbrains-navigation3-ui`).
- In `androidMain`: Can use `androidx-*` or `jetbrains-*` as appropriate, but do not mix them up in `commonMain`.
- [ ] **Compose Multiplatform:** Ensure `compose-multiplatform-*` aliases are used instead of plain `androidx.compose` in all KMP modules.
### 7. Testing
- [ ] **Test Placement:** New Compose UI tests must go in `commonTest` using `runComposeUiTest {}` from `androidx.compose.ui.test.v2` (not the deprecated v1 `androidx.compose.ui.test` package) + `kotlin.test.Test`. Do not add `androidTest` (instrumented) tests.
- [ ] **Shared Test Utilities:** Test fakes, doubles, and utilities should be placed in `core:testing`.
- [ ] **Libraries:** Verify usage of `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking.
- [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues.
### 8. ProGuard / R8 Rules
- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `app/proguard-rules.pro` (R8) and `desktop/proguard-rules.pro` (ProGuard). The two files must stay aligned.
- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktop:runRelease` succeed.
## Review Output Guidelines
1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern.
2. **Reference the Docs:** Cite `AGENTS.md` and project architecture playbooks to justify change requests (e.g., "Per AGENTS.md, `java.io.*` cannot be used in `commonMain`; please migrate to Okio").
3. **Enforce Build Health:** Remind authors to run `./gradlew test allTests` locally to verify changes, especially since KMP `test` tasks are ambiguous.
4. **Praise Good Patterns:** Acknowledge correct usage of complex architecture requirements, like proper Navigation 3 scene transitions or elegant `commonMain` helper extractions.

View file

@ -0,0 +1,61 @@
# Skill: Compose Multiplatform (CMP) UI
## Description
Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.
## 1. UI Components & Layouts
- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktop/tablets.
- **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups.
- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features.
- **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`.
- **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `app` or `desktop` shells. Do not tightly couple Google Maps/osmdroid dependencies to `commonMain`.
## 2. Strings & Resources
- **Multiplatform Resources:** MUST use `core:resources` (e.g., `stringResource(Res.string.your_key)`). Never use hardcoded strings.
- **ViewModels/Coroutines:** Use the asynchronous `getStringSuspend(Res.string.your_key)`. NEVER use blocking `getString()` in a coroutine context.
- **Formatting Constraints:** CMP `stringResource` only supports `%N$s` (string) and `%N$d` (integer).
- **No Float formatting:** Formats like `%N$.1f` pass through unsubstituted. Pre-format in Kotlin using `NumberFormatter.format(value, decimalPlaces)` from `core:common` and pass as a string argument (`%N$s`):
```kotlin
val formatted = NumberFormatter.format(batteryLevel, 1) // "73.5"
stringResource(Res.string.battery_percent, formatted) // uses %1$s
```
- **Percent Literals:** Use bare `%` (not `%%`) for literal percent signs in CMP-consumed strings.
### String Formatting Decision Tree
Choose the right tool for the job:
| Scenario | Tool | Example |
|----------|------|---------|
| **Metric display** (temp, voltage, %, signal) | `MetricFormatter.*` | `MetricFormatter.temperature(25.0f, isFahrenheit)``"77.0°F"` |
| **Simple number + unit** | `NumberFormatter` + interpolation | `"${NumberFormatter.format(val, 1)} dB"` |
| **Localized template from strings.xml** | `stringResource(Res.string.key, preFormattedArgs)` | `stringResource(Res.string.battery, formatted)` |
| **Non-composable template** (notifications, plain functions) | `formatString(template, args)` | `formatString(template, label, value)` |
| **Hex formatting** | `formatString` | `formatString("!%08x", nodeNum)` |
| **Date/time** | `DateFormatter` | `DateFormatter.format(instant)` |
**Rules:**
1. **NEVER use `%.Nf` in strings.xml** — CMP cannot substitute them. Use `%N$s` and pre-format floats.
2. **Prefer `MetricFormatter`** over scattered `formatString("%.1f°C", temp)` calls.
3. **`formatString` (pure Kotlin)** is a pure-Kotlin `commonMain` implementation for: hex formats, multi-arg templates fetched at runtime, and chart axis formatters. Located in `core:common` `Formatter.kt`.
4. **`NumberFormatter`** always uses `.` as decimal separator — intentional for mesh networking precision.
- **Workflow to Add a String:**
1. Add to `core/resources/src/commonMain/composeResources/values/strings.xml`.
2. Use the generated `org.meshtastic.core.resources.<key>` symbol.
3. Validate UI presentation.
## 3. Tooling & Capabilities
- **Image Loading:** Use `libs.coil` (Coil Compose) in feature modules. Configuration/Networking for Coil (`coil-network-ktor3`) happens strictly in the `app` and `desktop` host modules.
- **QR Codes:** Use `rememberQrCodePainter` from `core:ui/commonMain` powered by `qrcode-kotlin`. No ZXing or Android Bitmap APIs in shared code.
## 4. Compose Previews
- **Preview in commonMain:** CMP 1.11+ supports `@Preview` in `commonMain` via `compose-multiplatform-ui-tooling-preview`. Place preview functions alongside their composables.
- **Import:** Use `androidx.compose.ui.tooling.preview.Preview`. The JetBrains-prefixed import (`org.jetbrains.compose.ui.tooling.preview.Preview`) is deprecated.
## 5. Dialog & State Patterns
- **Dialog State Preservation:** Use `rememberSaveable` for dialog state (search queries, selected tabs, expanded flags) to preserve across configuration changes. Boolean and String types are auto-saveable — no custom `Saver` needed.
## Reference Anchors
- **Shared Strings:** `core/resources/src/commonMain/composeResources/values/strings.xml`
- **Platform abstraction contract:** `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt`
- **Provider wiring:** `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt`

View file

@ -0,0 +1,41 @@
# Skill: Implement a Feature
## Description
A step-by-step workflow for implementing a new feature in the Meshtastic-Android codebase, ensuring KMP compatibility and proper architecture.
## Workflow
### 1. Update Dependencies & Aliases
- Check `gradle/libs.versions.toml` before adding libraries.
- Use `jetbrains-*` aliases for lifecycle/navigation/adaptive dependencies in `commonMain`.
- Use `compose-multiplatform-*` aliases for CMP dependencies.
### 2. Define the State & ViewModels
- Follow MVI/UDF patterns.
- Extend shared ViewModel logic in `feature/<name>/src/commonMain/kotlin/org/meshtastic/feature/<name>/<Name>ViewModel.kt`.
- Use `stateInWhileSubscribed` (from `core:ui`) for sharing state flows.
- Keep the ViewModel free of Android framework dependencies.
### 3. Build the UI
- Use Jetpack Compose Multiplatform (CMP).
- Define strings in `core:resources` (see the `compose-ui` skill).
- Support adaptive layouts (Large/XL breakpoints).
### 4. Wire Navigation & DI
- Define typed route objects in `core:navigation`.
- Export the navigation graph as an extension function on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.myFeatureGraph()`).
- Add the required DI bindings via Koin Annotations (`@Factory`, `@Single`, `@KoinViewModel`) in `commonMain`.
- **CRITICAL:** Ensure the module is registered in the app root graphs (`AppKoinModule.kt`, `DesktopKoinModule.kt`) and the navigation is injected into the root entry provider in the host shell.
### 5. Validate Platform Separation
- If you need a platform-specific API (like camera or specific mapping SDK), define an interface in `commonMain`, implement it in the host shell, and inject it via `CompositionLocal` or Koin.
### 6. Verify Locally
- Run the baseline checks (see `testing-ci` skill):
```bash
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
```
- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro`, then verify release builds:
```bash
./gradlew assembleFdroidRelease :desktop:runRelease
```

View file

@ -0,0 +1,61 @@
# Skill: KMP Architecture & Source-Set Bridging
## Description
Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstractions, networking, database, and platform integration rules.
## 1. Source-Set Boundaries
- **`commonMain`:** All business logic, DB entities, API network logic, ViewModels, and UI rendering. NO `java.*` or `android.*` imports.
- **`androidMain`:** Android framework integration (`Context`, system services, NFC hardware, BLE Android bindings).
- **`jvmMain` / `jvmAndroidMain`:** Shared JVM code between Android and Desktop. Uses the `meshtastic.kmp.jvm.android` convention plugin to bridge `jvm` and `android` source sets without manual `dependsOn` hacks.
- **`app` / `desktop`:** Host shells. Responsible for Koin DI root wiring, `MainKoinModule`, host-level UI themes, and running the `MeshtasticNavDisplay`.
## 2. Bridging Strategies
- **Interface + DI (Preferred):** Expose an interface in `core:repository` or `core:ui` (e.g. `LocationRepository`, `MapViewProvider`), implement it in `androidMain` or the host `app`, and bind it via Koin or `CompositionLocal`.
- **`expect`/`actual` (Restricted):** Use only when a platform API cannot be abstracted cleanly (e.g. low-level File I/O mappings, `uppercase()` Locale helpers). Avoid deep class hierarchies using `expect`/`actual`.
- **Naming:** Keep `expect` in `FileIo.kt`, but put shared helpers in `FileIoUtils.kt` to prevent JVM duplicate class errors.
- **Shared Helpers:** Do not duplicate pure Kotlin logic between `androidMain` and `jvmMain`. Extract to a `commonMain` helper.
## 3. Core Libraries & Constraints
- **Concurrency:** `kotlinx.coroutines`. Use `org.meshtastic.core.common.util.ioDispatcher` over `Dispatchers.IO` directly. Inject `CoroutineDispatchers` from `core:di` into classes that need dispatchers — never reference `Dispatchers.IO`/`Main`/`Default` directly in business logic.
- **Error Handling:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction loops).
- **Standard Library Replacements:**
- `ConcurrentHashMap` -> `atomicfu` or Mutex-guarded `mutableMapOf()`.
- `java.util.concurrent.locks.*` -> `kotlinx.coroutines.sync.Mutex`.
- `java.io.*` -> `Okio` (`BufferedSource`/`BufferedSink`).
- **Networking:** Pure **Ktor**. No OkHttp. Ktor `Logging` plugin for debugging.
- **HTTP Configuration:** Use `HttpClientDefaults` from `core:network` for shared base URL (`API_BASE_URL`), timeouts, and retry constants. Both Android (`NetworkModule`) and Desktop (`DesktopKoinModule`) HttpClient instances must use these. Feature API services use relative paths; `DefaultRequest` sets the base URL.
- **BLE:** Route through `core:ble` using **Kable**.
- **Room KMP:** Use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder`.
## 4. Hierarchy & Source-Set Conventions
- **Hierarchy template first:** Prefer Kotlin's default hierarchy template and convention plugins over manual `dependsOn(...)` graphs. Manual source-set wiring should be reserved for cases the template cannot model.
- **`expect`/`actual` restraint:** Prefer interfaces + DI for platform capabilities; use `expect`/`actual` for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract to `commonMain`. Examples: `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BaseRadioTransportFactory`.
## 5. Dependency Catalog Aliases
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in `commonMain`.
- **Dependencies:** Always check `gradle/libs.versions.toml` before assuming a library is available.
## 6. I/O & Serialization
- **Okio standard:** This project standardizes on Okio (`BufferedSource`/`BufferedSink`). JetBrains recommends `kotlinx-io` (built on Okio), but this project has not migrated. Do not introduce `kotlinx-io` without an explicit decision.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Room Patterns:**
- Use `@Upsert` for insert-or-update operations instead of manual `INSERT OR IGNORE` + `UPDATE` logic.
- Use `LIMIT 1` on `@Query` methods that expect a single row.
- Prevent N+1 queries: batch operations with `@Upsert fun putAll(items: List<T>)` or chunked `WHERE IN` queries (chunk size ≤ 999 to respect SQLite bind parameter limit).
## 7. Build-Logic Conventions
- In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.
## 8. Onboarding a New Target (Desktop/iOS)
1. Ensure all new logic compiles against the KMP core (`jvm()`, `iosArm64()`, etc.).
2. Do not use platform-specific constructs in `commonMain` or you break the iOS/Desktop builds.
3. Test using `kmpSmokeCompile` to verify cross-platform compilation.
4. For desktop wiring, copy the pattern in `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations.
## Reference Anchors
- **Shared Okio I/O:** `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt`
- **Desktop DI Stubs:** `desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt`
- **Version Catalog:** `gradle/libs.versions.toml`
- **Convention Plugins:** `build-logic/convention/`

View file

@ -0,0 +1,56 @@
# Skill: DI and Navigation 3 Architecture
## Description
This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Navigation 3 (1.1.x) architecture, constraints, and anti-patterns within the Meshtastic-Android KMP codebase.
## Dependency Injection (Koin)
### Guidelines
1. **Annotations First:** Use `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations directly in `commonMain` shared modules to encapsulate dependency graphs per feature.
2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktop/.../DesktopKoinModule.kt`.
3. **No Platform Bleed:** Don't put Android framework dependencies (`Context`, `Activity`, `Application`) into shared `commonMain` business logic. Inject interfaces instead.
4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs.
### Anti-Patterns
- **A1 Module Compile Safety:** Do **not** enable `compileSafety`. It is a single boolean that enables A1 per-module checks — there is no separate A3 full-graph mode. Runtime graph verification is handled by `KoinVerificationTest` and `DesktopKoinTest` instead.
- **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values.
### Koin Startup Pattern (K2 Compiler Plugin)
The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The canonical startup uses the plugin's typed `startKoin<T>()` stub, which the plugin transforms at compile time via IR:
```kotlin
// Bootstrap class — separate from @Module, references the root module graph
@KoinApplication(modules = [AppKoinModule::class])
object AndroidKoinApp
// In Application.onCreate()
startKoin<AndroidKoinApp> {
androidContext(this@MeshUtilApplication)
workManagerFactory()
}
```
- `@KoinApplication` goes on a **dedicated bootstrap object**, not on a `@Module` class.
- `startKoin<T>()` (from `org.koin.plugin.module.dsl`) is a compiler plugin stub — if the plugin isn't applied, it throws `NotImplementedError`.
- `stopKoin()` uses the standard runtime API (`org.koin.core.context.stopKoin`).
- `compileSafety` must stay **disabled** — it enables A1 per-module checks that break our inverted-dependency architecture. There is no separate A3 full-graph flag.
## Navigation 3
### Guidelines
1. **Types:** Use Navigation 3 types consistently (`NavKey`, `NavBackStack`, `EntryProviderScope`).
2. **Typed Routes:** Keep route definitions in `core:navigation/src/commonMain/.../Routes.kt` as `@Serializable sealed interface` hierarchies. Don't use ad-hoc strings.
3. **Graph Assembly:** Define feature navigation graphs as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(backStack)`).
4. **Host Integration:** Use `MeshtasticNavDisplay` (from `core:ui/commonMain`) as the Navigation 3 host. Do not configure decorators manually inside feature modules.
5. **Back Handlers:** Use `NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose` for back gestures in multiplatform code. Do not use Android's `BackHandler`.
6. **Deep Links:** Use `DeepLinkRouter.route()` in `core:navigation` to synthesize typed backstacks from RESTful paths.
### Anti-Patterns
- **Single Backstack for Multiple Tabs:** Do **not** use a single `NavBackStack` list for multiple tabs. Use `MultiBackstack` (from `core:navigation`).
- **Decorator Reuse Across Tabs:** Do **not** reuse the same `NavEntryDecorator` instances across different backstacks. When rendering an active tab in `MeshtasticNavDisplay`, you **must** supply a fresh set of decorators (using `remember(backStack) { ... }`) bound to the active backstack instance to prevent permanent `ViewModelStore` destruction.
- **Custom Backstack Mutation:** Do **not** mutate back navigation with custom stacks disconnected from the app backstack. Mutate `NavBackStack<NavKey>` directly with `add(...)` and `removeLastOrNull()`.
## Reference Anchors
- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
- **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`

View file

@ -0,0 +1,79 @@
# Skill: New Branch Bootstrap
## Description
Canonical recipe for spinning up a fresh working branch off the latest upstream `main`. Use this skill
whenever the user says things like *"make a new branch off fetched origin/main"*, *"peel off a fresh
branch"*, *"dust off #NNNN"*, or otherwise signals the start of a new unit of work.
This replaces the ad-hoc prose that used to be retyped at the start of every session.
## When to Use
- Starting any new feature, fix, chore, or refactor.
- Rebasing a stale PR onto current `main` (see [Rebase variant](#rebase-variant)).
- Reproducing a CI failure from a clean baseline.
## Preconditions (verify before branching)
1. **Clean worktree.** If `git status --porcelain` is non-empty, ask the user before proceeding.
2. **Upstream remote present.** `git remote -v` must list `upstream` pointing at
`meshtastic/Meshtastic-Android`. If only `origin` exists on a fork, treat `origin` as upstream.
3. **Submodules initialised.** `core/proto/src/main/proto` must be populated — see AGENTS.md
workspace bootstrap rules.
4. **Secrets bootstrapped.** If `local.properties` is missing, copy `secrets.defaults.properties`
(required for `google` flavor builds).
## Standard Recipe
```bash
# 1. Fetch latest upstream
git fetch upstream --prune --tags
# 2. Create the branch from upstream/main (never from a local stale main)
git switch -c <branch-name> upstream/main
# 3. Ensure submodules track the new base
git submodule update --init --recursive
# 4. Sanity check
git --no-pager log -1 --oneline
```
## Branch Naming
Use conventional-commit style prefixes that match the PR title convention in AGENTS.md
`<git_and_prs>`:
| Prefix | Use for |
| :--- | :--- |
| `feat/<scope>` | New user-visible behavior |
| `fix/<scope>` | Bug fixes |
| `refactor/<scope>` | Code structure changes, no behavior change |
| `chore/<scope>` | Tooling, deps, CI, cleanup |
| `docs/<scope>` | Documentation only |
Keep the slug short and kebab-case, e.g. `fix/r8-animation-release`, `chore/koin-application-migration`.
## Rebase Variant
When the user says *"rebase #NNNN"* or *"dust off PR NNNN"*:
```bash
git fetch upstream --prune
gh pr checkout <NNNN> # checks out the PR head locally
git rebase upstream/main
git submodule update --init --recursive
# Resolve conflicts, then:
git push --force-with-lease
```
Never use plain `--force`. Always `--force-with-lease` to avoid clobbering collaborator pushes.
## Post-Branch Checklist
- [ ] Branch name follows conventional prefix.
- [ ] Submodules up to date.
- [ ] `local.properties` exists.
- [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap).
- [ ] Optional: run `./gradlew assembleDebug` once to catch environment regressions before editing.
## Tip: Prefer `/delegate` for Long Audits
If the user's opening prompt is a sweeping audit or investigation (e.g. *"audit changes since
v2.7.13 for regressions"*, *"investigate why animations are broken on release"*), consider
suggesting `/delegate` — the GitHub cloud agent can execute the branch + investigation + PR
end-to-end while the user keeps working locally. See AGENTS.md `<copilot_cli_workflow>`.

View file

@ -0,0 +1,83 @@
# Skill: Project Overview & Codebase Map
## Description
Module directory, namespacing conventions, environment setup, and troubleshooting for Meshtastic-Android.
- **Build System:** Gradle (Kotlin DSL). JDK 21 REQUIRED. Target SDK: API 36. Min SDK: API 26.
- **Flavors:** `fdroid` (OSS only) · `google` (Maps + DataDog analytics)
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`.
## Codebase Map
| Directory | Description |
| :--- | :--- |
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`). |
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
| `docs/` | Architecture docs and agent playbooks. See `docs/kmp-status.md` and `docs/roadmap.md` for current status. |
| `core/model` | Domain models and common data structures. |
| `core:proto` | Protobuf definitions (Git submodule). |
| `core:common` | Low-level utilities, I/O abstractions (Okio), and common types. |
| `core:database` | Room KMP database implementation. |
| `core:datastore` | Multiplatform DataStore for preferences. |
| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). |
| `core:domain` | Pure KMP business logic and UseCases. |
| `core:data` | Core manager implementations and data orchestration. |
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). |
| `core:di` | Common DI qualifiers and dispatchers. |
| `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies. `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence. |
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
| `core:api` | Public AIDL/API integration module for external clients. |
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
| `core:barcode` | Barcode scanning (Android-only). |
| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. |
| `core/ble/` | Bluetooth Low Energy stack using Kable. |
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
| `core/testing/` | Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules. |
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP except `widget`. Use `meshtastic.kmp.feature` convention plugin. |
| `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. |
| `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. |
| `desktop/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. |
## Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
- **Legacy:** Maintain the `com.geeksville.mesh` Application ID.
## Environment Setup
1. **JDK 21 MUST be used** to prevent Gradle sync/build failures.
2. **Secrets:** Copy `secrets.defaults.properties` to `local.properties`:
```properties
MAPS_API_KEY=dummy_key
datadogApplicationId=dummy_id
datadogClientToken=dummy_token
```
## Workspace Bootstrap (MUST run before any build)
Agents **MUST** perform these steps automatically at the start of every session before running any Gradle task. Do not wait for the user to tell you.
1. **Android SDK:** `ANDROID_HOME` may not be set in agent workspaces. Detect and export it:
```bash
# Check common macOS/Linux locations in order of preference
if [ -z "$ANDROID_HOME" ]; then
for dir in "$HOME/Library/Android/sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do
if [ -d "$dir" ]; then export ANDROID_HOME="$dir"; break; fi
done
fi
```
All `./gradlew` invocations must include `ANDROID_HOME` in the environment. If the SDK cannot be found, ask the user for the path.
2. **Proto submodule:** `core/proto/src/main/proto` is a Git submodule containing Protobuf definitions. It must be initialized or builds will fail with proto generation errors:
```bash
git submodule update --init
```
3. **Init secrets:** If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails:
```bash
[ -f local.properties ] || cp secrets.defaults.properties local.properties
```
## Troubleshooting
- **Build Failures:** Check `gradle/libs.versions.toml` for dependency conflicts.
- **Configuration Cache:** Add `--no-configuration-cache` if cache-related issues persist.
- **Koin Injection Failures:** Verify the component is included in `AppKoinModule`.

View file

@ -0,0 +1,85 @@
# Skill: Testing and CI Verification
## Description
Guidelines and commands for verifying code changes locally and understanding the Meshtastic-Android CI pipeline. Use this to determine which testing matrix is needed based on the change type.
## 1) Baseline local verification order
Run in a single invocation for routine changes to ensure code formatting, analysis, and basic compilation:
```bash
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
```
> **Why no `clean`?** Incremental builds are safe and significantly faster. Only use `clean` when debugging stale cache issues.
> **Why `test allTests` and not just `test`:**
> In KMP modules, the `test` task name is **ambiguous**. Gradle matches both `testAndroid` and
> `testAndroidHostTest` and refuses to run either, silently skipping KMP modules.
> `allTests` is the `KotlinTestReport` lifecycle task registered by the KMP plugin.
> Conversely, `allTests` does **not** cover pure-Android modules (`:app`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
## 2) Change-type verification matrix
- `docs-only` changes: Usually no Gradle run required, but run `spotlessCheck` if practical.
- `UI text/resource` changes: `spotlessCheck`, `detekt`, `assembleDebug`.
- `feature/commonMain logic` changes: `spotlessCheck`, `detekt`, `test allTests`, `assembleDebug`.
- `navigation/DI wiring` changes: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`, plus flavor unit tests if available.
- If touching any KMP module, also run `kmpSmokeCompile`.
- `worker/service/background` changes: Broad tests, targeted WorkManager checks.
- `BLE/networking/core repository`: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`.
## 3) Flavor checks
Run these when relevant to map, provider, or flavor-specific behavior:
```bash
./gradlew lintFdroidDebug lintGoogleDebug
./gradlew testFdroidDebug testGoogleDebug
```
## 4) CI Pipeline Architecture
CI is defined in `.github/workflows/reusable-check.yml` and structured as four parallel job groups:
1. **`lint-check`** — Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation (avoids 3x cold-start overhead). Uses `fetch-depth: 0` (full clone) for spotless ratcheting and version code calculation. Produces `cache_read_only` output and computed `version_code` for downstream jobs.
2. **`test-shards`** — A 3-shard matrix that runs unit tests in parallel (depends on `lint-check`):
- `shard-core`: `allTests` for all `core:*` KMP modules.
- `shard-feature`: `allTests` for all `feature:*` KMP modules.
- `shard-app`: Explicit test tasks for pure-Android/JVM modules (`app`, `desktop`, `core:barcode`).
Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags.
Downstream jobs use `fetch-depth: 1` and receive `VERSION_CODE` from lint-check via env var, enabling shallow clones.
3. **`android-check`** — Builds APKs for all flavors (depends on `lint-check`).
4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) that builds desktop distributions via `createDistributable` (depends on `lint-check`).
### Runner Strategy (Three Tiers)
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `lint-check`/`test-shards`/`android-check`, release builds, Dokka, publish, dependency-submission). Pin for reproducibility.
- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job and release packaging.
### CI Gradle Properties
`gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses `.github/ci-gradle.properties`, which the `gradle-setup` composite action copies to `~/.gradle/gradle.properties`. Key CI overrides:
- `org.gradle.daemon=false` (single-use runners)
- `kotlin.incremental=false` (fresh checkouts)
- `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon
- VFS watching disabled, workers capped at 4
- `org.gradle.isolated-projects=true` for better parallelism
- Disables unused Android build features (`resvalues`, `shaders`)
### CI Conventions
- **KMP Smoke Compile:** `./gradlew kmpSmokeCompile` is a lifecycle task (registered in `RootConventionPlugin`) that auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` checks `project.findProperty("ci") == "true"` and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`.
- **Detekt report formats:** Detekt.kt checks `project.findProperty("ci") == "true"` and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations.
- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{version}-sdk{level}` — update when bumping version or SDK level.
- **`mavenLocal()` gated:** Disabled by default to prevent CI cache poisoning. Pass `-PuseMavenLocal` for local JitPack testing.
- **JUnit parallel execution:** Enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races. Cross-module parallelism comes from Gradle forks (`maxParallelForks`).
- **`test-retry` plugin:** Applied to all module types (maxRetries=2, maxFailures=10).
- **`fail-fast: false`:** Test sharding does not cancel other shards on failure.
- **Explicit Gradle task paths:** Prefer `app:lintFdroidDebug` over shorthand `lintDebug` in CI.
- **Pull request CI:** Main-only (`.github/workflows/pull-request.yml` targets `main`).
- **Cache writes:** Trusted on `main` and merge queue runs; other refs use read-only cache.
- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.).
- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.

228
AGENTS.md
View file

@ -1,140 +1,108 @@
# Meshtastic Android - Agent Guide # Meshtastic Android - Unified Agent & Developer Guide
This file serves as a comprehensive guide for AI agents and developers working on the `Meshtastic-Android` codebase. Use this as your primary reference for understanding the architecture, conventions, and workflows. <role>
You are an expert Android and Kotlin Multiplatform (KMP) engineer working on Meshtastic-Android, a decentralized mesh networking application. You must maintain strict architectural boundaries, use Modern Android Development (MAD) standards, and adhere to Compose Multiplatform and JetBrains Navigation 3 patterns.
</role>
## 1. Project Overview <context_and_memory>
- **Project Goal:** Decouple business logic from the Android framework for seamless multi-platform execution (Android, Desktop, iOS) while maintaining a high-performance native Android experience.
- **Language & Tech:** Kotlin 2.3+ (JDK 21 REQUIRED), Gradle Kotlin DSL, Ktor, Okio, Room KMP.
- **Core Architecture:**
- `commonMain` is pure KMP. `androidMain` is strictly for Android framework bindings.
- App root DI and graph assembly live in the `app` and `desktop` host shells.
- **Skills Directory:** You **MUST** consult the relevant `.skills/` module before executing work:
- `.skills/project-overview/` - Codebase map, module directory, namespacing, environment setup, troubleshooting.
- `.skills/kmp-architecture/` - Bridging, expect/actual, source-sets, catalog aliases, build-logic conventions.
- `.skills/compose-ui/` - Adaptive UI, placeholders, string resources.
- `.skills/navigation-and-di/` - JetBrains Navigation 3 & Koin 4.2+ annotations.
- `.skills/testing-ci/` - Validation commands, CI pipeline architecture, CI Gradle properties.
- `.skills/implement-feature/` - Step-by-step feature workflow.
- `.skills/code-review/` - PR validation checklist.
- `.skills/new-branch/` - Canonical recipe for branching off upstream/main and rebasing stale PRs.
- **Active Status:** Read `docs/kmp-status.md` and `docs/roadmap.md` to understand the current KMP migration epoch.
</context_and_memory>
- **Type:** Native Android Application (Kotlin). <process>
- **Purpose:** Client interface for Meshtastic mesh radios. - **Workspace Bootstrap (MUST run first):** Before executing any Gradle task in a new workspace, agents MUST automatically:
- **Architecture:** Modern Android Development (MAD) principles. 1. **Find the Android SDK**`ANDROID_HOME` is often unset in agent worktrees. Probe `~/Library/Android/sdk`, `~/Android/Sdk`, and `/opt/android-sdk`. Export the first one found. If none exist, ask the user.
- **UI:** Jetpack Compose (Material 3). 2. **Init the proto submodule** — Run `git submodule update --init`. The `core/proto/src/main/proto` submodule contains Protobuf definitions required for builds.
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow. 3. **Init secrets** — If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails.
- **Dependency Injection:** Hilt. - **Think First:** Reason through the problem before writing code. For complex KMP tasks involving multiple modules or source sets, outline your approach step-by-step before executing.
- **Navigation:** Type-Safe Navigation (Jetpack Navigation). - **Plan Before Execution:** Use the git-ignored `.agent_plans/` directory to write markdown implementation plans (`plan.md`) and Mermaid diagrams (`.mmd`) for complex refactors before modifying code.
- **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms). - **Atomic Execution:** Follow your plan step-by-step. Do not jump ahead. Use TDD where feasible (write `commonTest` fakes first).
- **Baseline Verification:** Always instruct the user (or use your CLI tools) to run the baseline check before finishing:
```
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
```
> **Why both `test` and `allTests`?** In KMP modules, `test` is ambiguous and Gradle silently skips them. `allTests` is the KMP lifecycle task that covers KMP modules. Conversely, `allTests` does NOT cover pure-Android modules (`:app`, `:core:api`), so both tasks are required.
> For KMP cross-platform compilation, also run `./gradlew kmpSmokeCompile` (compiles all KMP modules for JVM + iOS Simulator — used by CI's `lint-check` job).
</process>
## 2. Codebase Map <agent_tools>
- **Codebase Search:** Use whatever search and navigation tools your environment provides (file search, grep/ripgrep, symbol lookup, semantic search, etc.) to map out project boundaries before coding. Prefer `rg` (ripgrep) over `grep` or `find` for raw text search.
- **Terminal Pagers:** When running shell commands like `git diff` or `git log`, ALWAYS use `--no-pager` (e.g., `git --no-pager diff`) to prevent getting stuck in an interactive prompt.
- **Fetch Up-to-Date Docs:** If your environment supports web search, MCP servers, or documentation lookup tools, actively query them for the latest documentation on Koin 4.x, JetBrains Navigation 3, and Compose Multiplatform 1.11.
- **Clone Reference Repos:** If documentation is insufficient, use shell commands to clone bleeding-edge KMP dependency repositories into the local `.agent_refs/` directory (git-ignored) to inspect their source and test suites. Recommended:
- `https://github.com/JetBrains/kotlin-multiplatform-dev-docs` (Official Docs)
- `https://github.com/InsertKoinIO/koin` (Koin Annotations 4.x)
- `https://github.com/JetBrains/compose-multiplatform` (Navigation 3, Adaptive UI)
- `https://github.com/JuulLabs/kable` (BLE)
- `https://github.com/coil-kt/coil` (Coil 3 KMP)
- `https://github.com/ktorio/ktor` (Ktor Networking)
- **Formatting Hooks:** Always run `./gradlew spotlessApply` as an automatic formatting hook to fix style violations after editing.
</agent_tools>
| Directory | Description | <documentation_sync>
| :--- | :--- | `AGENTS.md` is the single source of truth for agent instructions. Agent-specific files redirect here:
| `app/` | Main application module. Contains `MainActivity`, `AppNavigation`, and Hilt entry points. Uses package `com.geeksville.mesh`. | - `.github/copilot-instructions.md` — Copilot redirect to `AGENTS.md`.
| `core/` | Shared library modules. Most code here uses package `org.meshtastic.core.*`. | - `CLAUDE.md` — Claude Code entry point; imports `AGENTS.md` via `@AGENTS.md` and adds Claude-specific instructions.
| `core/ble/` | **New:** Bluetooth Low Energy stack using Nordic libraries. | - `GEMINI.md` — Gemini redirect to `AGENTS.md`. Gemini CLI also configured via `.gemini/settings.json` to read `AGENTS.md` directly.
| `core/strings/` | **Crucial:** Centralized string resources using Compose Multiplatform Resources. |
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`). Each is a standalone Gradle module. Uses package `org.meshtastic.feature.*`. |
| `build-logic/` | Custom Gradle convention plugins. Defines build logic for the entire project. |
| `gradle/libs.versions.toml` | **Version Catalog.** All dependencies and versions are defined here. |
| `core/proto/` | Protobuf definitions for communicating with the mesh radio. |
## 3. Development Guidelines Do NOT duplicate content into agent-specific files. When you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update `AGENTS.md`, `.skills/`, and `docs/kmp-status.md` as needed.
</documentation_sync>
### A. UI Development (Jetpack Compose) <rules>
- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate. - **No Lazy Coding:** DO NOT use placeholders like `// ... existing code ...`. Always provide complete, valid code blocks for the sections you modify to ensure correct diff application.
- **Strings:** - **No Framework Bleed:** NEVER import `java.*` or `android.*` in `commonMain`. Use KMP equivalents: `Okio` for `java.io.*`, `kotlinx.coroutines.sync.Mutex` for `java.util.concurrent.locks.*`, `atomicfu` or Mutex-guarded `mutableMapOf()` for `ConcurrentHashMap`. Use `org.meshtastic.core.common.util.ioDispatcher` instead of `Dispatchers.IO` directly.
- Do **not** use `app/src/main/res/values/strings.xml` for UI strings. - **Koin Annotations:** Use `@Single`, `@Factory`, and `@KoinViewModel` inside `commonMain` instead of manual constructor trees. Do not enable A1 module compile safety — A3 full-graph validation (`VerifyModule`) is the correct approach because interfaces and implementations live in separate modules. Always register new feature modules in **both** `AppKoinModule.kt` and `DesktopKoinModule.kt`; they are not auto-activated.
- Use the **Compose Multiplatform Resource** library in `core:resources`. - **CMP Over Android:** Use `compose-multiplatform` constraints. `stringResource` only supports `%N$s` and `%N$d` — pre-format floats with `NumberFormatter.format()` from `core:common` and pass as `%N$s`. In ViewModels/coroutines use `getStringSuspend(Res.string.key)`; never blocking `getString()`. Always use `MeshtasticNavDisplay` (not raw `NavDisplay`) as the navigation host, and `NavigationBackHandler` (not Android's `BackHandler`) for back gestures in shared code.
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`. - **ProGuard:** When adding a reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro` and verify release builds.
- **Usage:** - **Always Check Docs:** If unsure about an abstraction, search `core:ui/commonMain` or `core:navigation/commonMain` before assuming it doesn't exist.
```kotlin - **Privacy First:** Never log or expose PII, location data, or cryptographic keys. Meshtastic is used for sensitive off-grid communication — treat all user data with extreme caution.
import org.jetbrains.compose.resources.stringResource - **Dependency Discipline:** Never add a library without first checking `libs.versions.toml` and justifying its inclusion against the project's size and complexity goals. Prefer removing dependencies over adding them.
import org.meshtastic.core.resources.Res - **Zero Lint Tolerance:** A task is incomplete if `detekt` fails or `spotlessCheck` does not pass for touched modules.
import org.meshtastic.core.resources.your_string_key - **Read Before Refactoring:** When a pattern contradicts best practices, analyze whether it is legacy debt or a deliberate architectural choice before proposing a change.
</rules>
Text(text = stringResource(Res.string.your_string_key)) <copilot_cli_workflow>
``` These tips apply when the agent is the GitHub Copilot CLI. Other agent runtimes may ignore this
- **Dialogs:** section.
- Use the centralized `MeshtasticDialog` for all alerts and confirmation boxes.
- **Specialized Overloads:** Use `MeshtasticResourceDialog` (for resource-only content) or `MeshtasticTextDialog` (for mixed resource/text content) to reduce boilerplate.
- **Location:** Defined in `core/ui/src/main/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt`.
- **Previews:** Create `@Preview` functions for your Composables to ensure they render correctly.
### B. Architecture & State - **Delegate long autonomous work.** For sweeping audits, multi-hour investigations, or "fleet"
- **ViewModels:** Must be annotated with `@HiltViewModel`. prompts (*"investigate why X is broken on release"*, *"audit the diff since tag vX.Y.Z"*,
- **Injection:** Use `@Inject constructor(...)`. *"review the codebase for best practices against spec Z"*), prefer `/delegate` so the GitHub
- **Scopes:** Use `viewModelScope` for coroutines. Avoid `GlobalScope`. cloud agent opens a PR while the user keeps working locally. Don't tie up an interactive
- **Data Flow:** Expose UI state as `StateFlow<UiState>` or `Flow<UiState>`. session on work that can run unattended.
- **Use `/research` for "latest hotness" prompts.** When the user asks for *"the latest scoop"*
on Kotlin / KMP / Compose / Koin trends, the built-in `/research` slash command performs deep
research across GitHub and the web with better source grounding than an ad-hoc prompt.
- **Use `/plan` mode for "noodle it out" prompts.** When the user asks for an implementation
plan, a "walk me through next steps", or explicitly says "don't do anything yet" — switch to
plan mode (Shift+Tab or `/plan`). Plans persist in the session workspace and keep the agent
from prematurely editing files. Continue to write long-form plans and Mermaid diagrams to
`.agent_plans/` (git-ignored) for multi-module refactors.
- **`/share` audit and review outputs.** After large audits, PR safety reviews, or release-cycle
quality passes, offer `/share` to export the findings to a gist or markdown file. These
reports are valuable artifacts — don't let them die in session history.
- **Prefer `/rewind` or `ctrl+s` over retyping.** If a turn went sideways, `/rewind` reverts
file changes and the turn; `ctrl+s` submits while preserving the input for quick iteration.
Avoid re-issuing the same prompt verbatim.
- **New-branch flow lives in a skill.** When the user says "fresh branch off fetched origin/main"
or "rebase PR #NNNN", consult `.skills/new-branch/SKILL.md` rather than re-deriving the recipe.
</copilot_cli_workflow>
### C. Navigation <git_and_prs>
- The project uses **Type-Safe Navigation** (Kotlin Serialization). - **Commit Hygiene:** Squash fixup/polish/review-feedback commits before opening a PR. Each commit should represent a logical, self-contained unit of work — not a back-and-forth conversation.
- Routes are defined in `core/navigation` (e.g., `ContactsRoutes`, `SettingsRoutes`). - **PR Descriptions:** Keep PR descriptions concise and scannable. State *what changed* and *why*, not a per-commit play-by-play. Use a short summary paragraph followed by a bullet list of changes. Avoid tables, headers-per-commit, or verbose breakdowns. Reference the `meshtastic/firmware` repo PRs for tone and style.
- The main `NavHost` is located in `app/src/main/java/com/geeksville/mesh/ui/Main.kt`. - **PR Titles:** Use conventional commit format: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `chore(scope):`. Keep titles under ~72 characters.
</git_and_prs>
### D. Bluetooth (BLE)
- **Library:** Uses **Nordic Semiconductor's Kotlin BLE Library** and **Android Common Libraries**.
- **Location:** Core logic resides in `core/ble`.
- **Key Classes:** `BluetoothRepository`, `NordicBleInterface`, `BleConnection`.
- **Usage:** Use `BluetoothRepository` for scanning and bonding. Use `BleConnection` for managing connections. Avoid legacy `BluetoothAdapter` APIs directly.
- **Environment Mocking:** Use `LocalEnvironmentOwner` and `MockAndroidEnvironment` to test UI hardware reactions without a real device.
### E. Dependency Management
- **Never** hardcode versions in `build.gradle.kts` files.
- **Action:** Add the library and version to `gradle/libs.versions.toml`.
- **Action:** Apply plugins using the alias from the catalog (e.g., `alias(libs.plugins.meshtastic.android.library)`).
- **Alpha Libraries:** Do not be shy about using alpha libraries from Google if they provide necessary features.
### F. Build Variants (Flavors)
- **`google`**: Includes Google Play Services (Maps, Firebase, Crashlytics).
- **`fdroid`**: FOSS version. **Strictly segregate sensitive data** (Crashlytics, Firebase, etc.) out of this flavor.
- **Task Example:** `./gradlew assembleFdroidDebug`
### G. Kotlin Multiplatform (KMP) & Decoupling
- **Goal:** We are actively moving logic and models from Android-specific modules to KMP modules (`core:common`, `core:model`, `core:proto`) to support future cross-platform expansion.
- **Domain Models:** Always place domain models (Data Classes, Enums) in `commonMain` of the respective module.
- **Parceling:**
- Use the platform-agnostic `CommonParcelable` and `CommonParcelize` from `core:common`.
- Avoid direct imports of `android.os.Parcelable` or `kotlinx.parcelize.Parcelize` in `commonMain`.
- **Platform Abstractions:** Use `expect`/`actual` for platform-specific logic (e.g., `DateFormatter`, `RandomUtils`, `BuildUtils`).
- **AIDL Compatibility:** AIDL parcelable declarations for models moved to `commonMain` should be relocated to `:core:api` to ensure proper export to consumer modules.
## 4. Quality Assurance
### A. Code Style (Spotless)
- The project uses **Spotless** to enforce formatting.
- **Command:** `./gradlew spotlessApply`
- **Rule:** You **must** run this before submitting any code.
### B. Linting (Detekt)
- The project uses **Detekt** for static analysis.
- **Command:** `./gradlew detekt`
- **Rule:** Ensure zero regressions.
### C. Testing
- **Unit Tests:** JUnit 4/5 in `src/test/java`. Run with `./gradlew test`.
- **Compose UI Tests (JVM):** Preferred for component testing. Use **Robolectric** in `src/test/java`.
- **Important:** Annotate with `@Config(sdk = [34])` if using Java 17 to avoid SDK 35 compatibility issues.
- **Best Practice:** Pass mocked ViewModels to Composables instead of using Hilt in Robolectric tests.
- **Instrumented Tests:** For full E2E or Hilt integration tests, use `src/androidTest/java`. Run with `./gradlew connectedAndroidTest`.
- **Feature Test:** `./gradlew feature:settings:testGoogleDebug`
## 5. Agent Workflow
1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment.
2. **Plan:** Identify which modules (`core` or `feature`) need modification.
3. **Implement:**
- If adding a string, modify `core:resources`.
- If adding a dependency, modify `libs.versions.toml` first.
4. **Verify:**
- Run `./gradlew spotlessApply` (Essential!).
- Run `./gradlew detekt`.
- Run relevant tests (e.g., `./gradlew :feature:settings:testDebugUnitTest`).
## 6. Important Context
- **Protobuf:** Communication with the device uses Protobufs. The definitions are in `core/proto`. This is a Git submodule, but the build system handles it.
- **Legacy:** Some code in `app/` uses the `com.geeksville.mesh` package. Newer code in `core/` and `feature/` uses `org.meshtastic.*`. Respect the existing package structure of the file you are editing.
- **Versioning:** Do not manually edit `versionCode` or `versionName`. These are managed by the build system and CI/CD.
- **Database Safety:** When modifying critical database logic (e.g., `NodeInfoDao`), always ensure you have explicit test coverage for security edge cases (like PKC conflicts or key wiping). Refer to `core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt` for examples.
## 7. Troubleshooting
- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.resources.Res` and the specific string property, and that you have run a build to generate the resources.
- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly.
---
*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.*
### E. Resources and Assets
- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`.
- **Module Path:** `core/resources/src/commonMain/composeResources/`
- **Decentralization:** Feature-specific strings and assets can (and should) be housed in their respective feature module's `composeResources` directory to maintain modular boundaries and clean architectural dependency graphs. Crowdin localization handles globbing `/**/composeResources/values/strings.xml` perfectly.
- **Drawables:** Use `painterResource(Res.drawable.your_icon)` to access cross-platform drawables. Name them consistently (`ic_` for icons, `img_` for artwork). Avoid putting standard Drawables or Vectors in legacy Android `res/drawable` folders unless strictly required by a legacy library (like `OsmDroid` map markers) or the OS layer (like `app_icon.xml`).

9
CLAUDE.md Normal file
View file

@ -0,0 +1,9 @@
# Meshtastic Android - Claude Code Guide
@AGENTS.md
## Claude-Specific Instructions
- **Think First:** Always outline your step-by-step reasoning inside `<thinking>` tags before writing code or shell commands. Claude models perform significantly better on complex KMP tasks when they "think out loud" first.
- **Skills:** The `.skills/` directory contains task-specific instruction modules. Load them as needed — only the skill relevant to your current task.
- **Plan Mode:** Use plan mode for architectural changes spanning multiple modules. Write plans to `.agent_plans/` (git-ignored). The Copilot-CLI-specific `/plan`, `/delegate`, `/research`, and `/share` guidance in `AGENTS.md` does not apply to Claude Code — skip the `<copilot_cli_workflow>` section.

View file

@ -48,7 +48,7 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t
- **Unit tests** are located in the `src/test/` directory of each module. - **Unit tests** are located in the `src/test/` directory of each module.
- **Compose UI Tests (JVM)** are preferred for component testing and are also located in `src/test/` using **Robolectric**. - **Compose UI Tests (JVM)** are preferred for component testing and are also located in `src/test/` using **Robolectric**.
- Note: If using Java 17, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues. - Note: If using Java 21, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues.
- **Instrumented tests** (including full E2E UI tests) are located in `src/androidTest/`. For Compose UI, use the [Jetpack Compose Testing APIs](https://developer.android.com/jetpack/compose/testing). - **Instrumented tests** (including full E2E UI tests) are located in `src/androidTest/`. For Compose UI, use the [Jetpack Compose Testing APIs](https://developer.android.com/jetpack/compose/testing).
#### Guidelines for Testing #### Guidelines for Testing

6
GEMINI.md Normal file
View file

@ -0,0 +1,6 @@
# Meshtastic Android - Google Gemini Guide
> **Note:** The canonical instructions for all AI Agents have been deduplicated.
You MUST immediately read and internalize the unified instructions located at the root of the repository in `AGENTS.md`.
After reading `AGENTS.md`, consult the `.skills/` directory for task-specific playbooks.

View file

@ -3,13 +3,13 @@ GEM
specs: specs:
CFPropertyList (3.0.8) CFPropertyList (3.0.8)
abbrev (0.1.2) abbrev (0.1.2)
addressable (2.8.8) addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0) public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1213.0) aws-partitions (1.1240.0)
aws-sdk-core (3.242.0) aws-sdk-core (3.245.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
@ -17,11 +17,11 @@ GEM
bigdecimal bigdecimal
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger logger
aws-sdk-kms (1.121.0) aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.213.0) aws-sdk-s3 (1.219.0)
aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1) aws-sigv4 (1.12.1)
@ -29,7 +29,7 @@ GEM
babosa (1.0.4) babosa (1.0.4)
base64 (0.2.0) base64 (0.2.0)
benchmark (0.5.0) benchmark (0.5.0)
bigdecimal (4.0.1) bigdecimal (4.1.2)
claide (1.1.0) claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
@ -68,11 +68,11 @@ GEM
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.4)
faraday_middleware (1.2.1) faraday_middleware (1.2.1)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.4.0) fastimage (2.4.1)
fastlane (2.232.2) fastlane (2.233.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2) abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
@ -92,7 +92,7 @@ GEM
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0) faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0) fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
@ -122,10 +122,9 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1) xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0) fastlane-sirp (1.1.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.95.0) google-apis-androidpublisher_v3 (0.99.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0) google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
@ -139,15 +138,15 @@ GEM
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.17.0) google-apis-playcustomapp_v1 (0.17.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.59.0) google-apis-storage_v1 (0.61.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0) google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.1.1) google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.5.0) google-cloud-errors (1.6.0)
google-cloud-storage (1.58.0) google-cloud-storage (1.59.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2) google-apis-core (>= 0.18, < 2)
@ -169,13 +168,13 @@ GEM
httpclient (2.9.0) httpclient (2.9.0)
mutex_m mutex_m
jmespath (1.6.2) jmespath (1.6.2)
json (2.18.1) json (2.19.4)
jwt (2.10.2) jwt (2.10.2)
base64 base64
logger (1.7.0) logger (1.7.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
multi_json (1.19.1) multi_json (1.20.1)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.3.0) mutex_m (0.3.0)
nanaimo (0.4.0) nanaimo (0.4.0)
@ -185,13 +184,13 @@ GEM
os (1.1.4) os (1.1.4)
ostruct (0.6.3) ostruct (0.6.3)
plist (3.7.2) plist (3.7.2)
public_suffix (7.0.2) public_suffix (7.0.5)
rake (13.3.1) rake (13.4.2)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.4.1)
rexml (3.4.4) rexml (3.4.4)
rouge (3.28.0) rouge (3.28.0)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
@ -205,7 +204,6 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)

View file

@ -11,7 +11,7 @@
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) [![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) [![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
This is a tool for using Android with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware). This is a tool for using Android (and Compose Desktop) with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
This project is currently beta testing across various providers. If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic) . We would love to hear from you! This project is currently beta testing across various providers. If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic) . We would love to hear from you!
@ -51,23 +51,24 @@ You can generate the documentation locally to preview your changes.
1. **Run the Dokka task:** 1. **Run the Dokka task:**
```bash ```bash
./gradlew :app:dokkaHtml ./gradlew dokkaGeneratePublicationHtml
``` ```
2. **View the output:** 2. **View the output:**
The generated HTML files will be located in the `app/build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation. The generated HTML files will be located in the `build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
## Architecture ## Architecture
### Modern Android Development (MAD) ### Modern Android Development (MAD)
The app follows modern Android development practices: The app follows modern Android development practices, built on top of a shared Kotlin Multiplatform (KMP) Core:
- **UI:** Jetpack Compose (Material 3). - **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, targeting Android and Compose Desktop.
- **UI:** JetBrains Compose Multiplatform (Material 3) using Compose Multiplatform resources.
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow. - **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
- **Dependency Injection:** Hilt. - **Dependency Injection:** Koin with Koin Annotations (K2 Compiler Plugin).
- **Navigation:** Type-Safe Navigation (Jetpack Navigation). - **Navigation:** JetBrains Navigation 3 (Multiplatform routing with RESTful deep linking).
- **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms). - **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).
### Bluetooth Low Energy (BLE) ### Bluetooth Low Energy (BLE)
The BLE stack has been modernized to use **Nordic Semiconductor's Android Common Libraries** and **Kotlin BLE Library**. This provides a robust, Coroutine-based architecture for reliable device communication. See [core/ble/README.md](core/ble/README.md) for details. The BLE stack uses a multiplatform interface-driven architecture. Platform-agnostic interfaces live in `commonMain`, utilizing the **Kable** multiplatform BLE library to handle device communication across all supported targets (Android, Desktop). This provides a robust, Coroutine-based architecture for reliable device communication while remaining fully KMP compatible. See [core/ble/README.md](core/ble/README.md) for details.
## Translations ## Translations
@ -79,6 +80,8 @@ Developers can integrate with the Meshtastic Android app using our published API
For detailed integration instructions, see [core/api/README.md](core/api/README.md). For detailed integration instructions, see [core/api/README.md](core/api/README.md).
Additionally, the app includes a built-in **Local TAK Server** feature that can be enabled in settings. This runs a local TCP server on port 8089 to allow ATAK clients to connect directly and route their traffic over the mesh.
## Building the Android App ## Building the Android App
> [!WARNING] > [!WARNING]
> Debug and release builds can be installed concurrently. This is solely to enable smoother development, and you should avoid running both apps simultaneously. To ensure proper function, force quit the app not in use. > Debug and release builds can be installed concurrently. This is solely to enable smoother development, and you should avoid running both apps simultaneously. To ensure proper function, force quit the app not in use.

12
SECURITY.md Normal file
View file

@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| App Version | Supported |
| ---------------- | ------------------ |
| 2.7.x | :white_check_mark: |
| <= 2.6.x | :x: |
## Reporting a Vulnerability
We support the private reporting of potential security vulnerabilities. Please go to the Security tab to file a report with a description of the potential vulnerability and reproduction scripts (preferred) or steps, and our developers will review.

View file

@ -6,13 +6,13 @@ The `:app` module is the entry point for the Meshtastic Android application. It
## Key Components ## Key Components
### 1. `MainActivity` & `Main.kt` ### 1. `MainActivity` & `Main.kt`
The single Activity of the application. It hosts the `NavHost` and manages the root UI structure (Navigation Bar, Rail, etc.). The single Activity of the application. It hosts the shared `MeshtasticNavDisplay` navigation shell and manages the root UI structure (Navigation Bar, Rail, etc.).
### 2. `MeshService` ### 2. `MeshService`
The core background service that manages long-running communication with the mesh radio. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background. The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
### 3. Hilt Application ### 3. Koin Application
`MeshUtilApplication` is the Hilt entry point, providing the global dependency injection container. `MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
## Architecture ## Architecture
The module primarily serves as a "glue" layer, connecting: The module primarily serves as a "glue" layer, connecting:
@ -25,13 +25,13 @@ The module primarily serves as a "glue" layer, connecting:
```mermaid ```mermaid
graph TB graph TB
:app[app]:::android-application :app[app]:::android-application
:app -.-> :core:analytics
:app -.-> :core:ble :app -.-> :core:ble
:app -.-> :core:common :app -.-> :core:common
:app -.-> :core:data :app -.-> :core:data
:app -.-> :core:database :app -.-> :core:database
:app -.-> :core:datastore :app -.-> :core:datastore
:app -.-> :core:di :app -.-> :core:di
:app -.-> :core:domain
:app -.-> :core:model :app -.-> :core:model
:app -.-> :core:navigation :app -.-> :core:navigation
:app -.-> :core:network :app -.-> :core:network
@ -42,20 +42,27 @@ graph TB
:app -.-> :core:resources :app -.-> :core:resources
:app -.-> :core:ui :app -.-> :core:ui
:app -.-> :core:barcode :app -.-> :core:barcode
:app -.-> :core:takserver
:app -.-> :feature:intro :app -.-> :feature:intro
:app -.-> :feature:messaging :app -.-> :feature:messaging
:app -.-> :feature:connections
:app -.-> :feature:map :app -.-> :feature:map
:app -.-> :feature:node :app -.-> :feature:node
:app -.-> :feature:settings :app -.-> :feature:settings
:app -.-> :feature:firmware :app -.-> :feature:firmware
:app -.-> :feature:wifi-provision
:app -.-> :feature:widget
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;

View file

@ -29,11 +29,11 @@ plugins {
alias(libs.plugins.meshtastic.android.application) alias(libs.plugins.meshtastic.android.application)
alias(libs.plugins.meshtastic.android.application.flavors) alias(libs.plugins.meshtastic.android.application.flavors)
alias(libs.plugins.meshtastic.android.application.compose) alias(libs.plugins.meshtastic.android.application.compose)
alias(libs.plugins.meshtastic.hilt) id("meshtastic.koin")
alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.devtools.ksp)
alias(libs.plugins.secrets) alias(libs.plugins.secrets)
alias(libs.plugins.aboutlibraries) alias(libs.plugins.aboutlibraries)
id("dev.mokkery")
} }
val keystorePropertiesFile = rootProject.file("keystore.properties") val keystorePropertiesFile = rootProject.file("keystore.properties")
@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
} }
configure<ApplicationExtension> { configure<ApplicationExtension> {
namespace = configProperties.getProperty("APPLICATION_ID") namespace = "org.meshtastic.app"
signingConfigs { signingConfigs {
create("release") { create("release") {
@ -150,7 +150,7 @@ configure<ApplicationExtension> {
includeInBundle = false includeInBundle = false
} }
testInstrumentationRunner = "com.geeksville.mesh.TestRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
// Configure existing product flavors (defined by convention plugin) // Configure existing product flavors (defined by convention plugin)
@ -171,8 +171,6 @@ configure<ApplicationExtension> {
} else { } else {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
} }
isMinifyEnabled = true
isShrinkResources = true
isDebuggable = false isDebuggable = false
} }
} }
@ -210,15 +208,16 @@ project.afterEvaluate {
} }
dependencies { dependencies {
implementation(projects.core.analytics)
implementation(projects.core.ble) implementation(projects.core.ble)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.database) implementation(projects.core.database)
implementation(projects.core.datastore) implementation(projects.core.datastore)
implementation(projects.core.di) implementation(projects.core.di)
implementation(projects.core.domain)
implementation(projects.core.model) implementation(projects.core.model)
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
implementation(projects.core.network) implementation(projects.core.network)
implementation(projects.core.nfc) implementation(projects.core.nfc)
implementation(projects.core.prefs) implementation(projects.core.prefs)
@ -227,83 +226,113 @@ dependencies {
implementation(projects.core.resources) implementation(projects.core.resources)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.barcode) implementation(projects.core.barcode)
implementation(projects.core.takserver)
implementation(projects.feature.intro) implementation(projects.feature.intro)
implementation(projects.feature.messaging) implementation(projects.feature.messaging)
implementation(projects.feature.connections)
implementation(projects.feature.map) implementation(projects.feature.map)
implementation(projects.feature.node) implementation(projects.feature.node)
implementation(projects.feature.settings) implementation(projects.feature.settings)
implementation(projects.feature.firmware) implementation(projects.feature.firmware)
implementation(projects.feature.wifiProvision)
implementation(projects.feature.widget)
implementation(libs.androidx.compose.material3.adaptive) implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.jetbrains.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.jetbrains.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.navigationSuite)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.compose.material3) implementation(libs.compose.multiplatform.animation)
implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.compose.multiplatform.material3)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.compose.multiplatform.ui.tooling.preview)
implementation(libs.androidx.compose.ui.text) implementation(libs.compose.multiplatform.ui)
implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.glance.appwidget.preview) implementation(libs.androidx.glance.appwidget.preview)
implementation(libs.androidx.glance.material3) implementation(libs.androidx.glance.material3)
implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose) implementation(libs.jetbrains.navigation3.ui)
implementation(libs.androidx.paging.compose) implementation(libs.ktor.client.android)
implementation(libs.coil.network.okhttp) implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.logging)
implementation(libs.coil)
implementation(libs.coil.network.ktor3)
implementation(libs.coil.svg) implementation(libs.coil.svg)
implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.splashscreen)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.org.eclipse.paho.client.mqttv3)
implementation(libs.usb.serial.android) implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work) implementation(libs.koin.android)
ksp(libs.androidx.hilt.compiler) implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.androidx.workmanager)
implementation(libs.koin.annotations)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(libs.kermit) implementation(libs.kermit)
implementation(libs.kotlinx.datetime)
implementation(libs.nordic.client.android)
implementation(libs.nordic.common.core)
implementation(libs.nordic.common.permissions.ble)
implementation(libs.nordic.common.permissions.notification)
implementation(libs.nordic.common.scanner.ble)
implementation(libs.nordic.common.ui)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.glance.preview) debugImplementation(libs.androidx.glance.preview)
googleImplementation(libs.location.services) googleImplementation(libs.location.services)
googleImplementation(libs.play.services.maps) googleImplementation(libs.play.services.maps)
googleImplementation(libs.maps.compose)
googleImplementation(libs.maps.compose.utils)
googleImplementation(libs.maps.compose.widgets)
googleImplementation(libs.dd.sdk.android.logs)
googleImplementation(libs.dd.sdk.android.rum)
googleImplementation(libs.dd.sdk.android.session.replay)
googleImplementation(libs.dd.sdk.android.session.replay.material)
googleImplementation(libs.dd.sdk.android.timber)
googleImplementation(libs.dd.sdk.android.trace)
googleImplementation(libs.dd.sdk.android.trace.otel)
googleImplementation(platform(libs.firebase.bom))
googleImplementation(libs.firebase.analytics)
googleImplementation(libs.firebase.crashlytics)
fdroidImplementation(libs.osmdroid.android) fdroidImplementation(libs.osmdroid.android)
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") } fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
fdroidImplementation(libs.osmbonuspack)
androidTestImplementation(libs.androidx.test.runner) testImplementation(kotlin("test-junit"))
androidTestImplementation(libs.androidx.test.ext.junit) testImplementation(libs.androidx.work.testing)
androidTestImplementation(libs.hilt.android.testing) testImplementation(libs.koin.test)
androidTestImplementation(libs.kotlinx.coroutines.test) testRuntimeOnly(libs.junit.vintage.engine)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.nordic.client.android.mock)
androidTestImplementation(libs.nordic.core.mock)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.nordic.client.android.mock)
testImplementation(libs.nordic.client.core.mock)
testImplementation(libs.nordic.core.mock)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core) testImplementation(libs.androidx.test.core)
testImplementation(libs.androidx.compose.ui.test.junit4) testImplementation(libs.compose.multiplatform.ui.test)
testImplementation(libs.androidx.test.ext.junit) testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.androidx.glance.appwidget) testImplementation(libs.androidx.glance.appwidget)
} }
aboutLibraries { aboutLibraries {
export { excludeFields = listOf("generated") } // Run offline by default to avoid burning GitHub API calls on every build.
// Release builds pass -PaboutLibraries.release=true to fetch full license text + funding info.
val isReleaseBuild = providers.gradleProperty("aboutLibraries.release").map { it.toBoolean() }.getOrElse(false)
val ghToken = providers.environmentVariable("GITHUB_TOKEN")
offlineMode = !isReleaseBuild
collect {
fetchRemoteLicense = isReleaseBuild && ghToken.isPresent
fetchRemoteFunding = isReleaseBuild && ghToken.isPresent
if (ghToken.isPresent) {
gitHubApiToken = ghToken.get()
}
}
export {
excludeFields = listOf("generated")
outputFile = file("src/main/resources/aboutlibraries.json")
}
library { library {
duplicationMode = DuplicateMode.MERGE duplicationMode = DuplicateMode.MERGE
duplicationRule = DuplicateRule.SIMPLE duplicationRule = DuplicateRule.SIMPLE
} }
} }
// Ensure aboutlibraries.json is always up-to-date during the build.
// This is required since AboutLibraries v11+ no longer auto-exports.
tasks
.matching { it.name.startsWith("process") && it.name.endsWith("Resources") }
.configureEach { dependsOn("exportLibraryDefinitions") }

View file

@ -1,415 +0,0 @@
androidx.activity:activity-compose:1.12.3
androidx.activity:activity-ktx:1.12.3
androidx.activity:activity:1.12.3
androidx.annotation:annotation-experimental:1.5.1
androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.7.1
androidx.appcompat:appcompat:1.7.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.11.0-alpha04
androidx.compose.animation:animation-core-android:1.11.0-alpha04
androidx.compose.animation:animation-core:1.11.0-alpha04
androidx.compose.animation:animation:1.11.0-alpha04
androidx.compose.foundation:foundation-android:1.11.0-alpha04
androidx.compose.foundation:foundation-layout-android:1.11.0-alpha04
androidx.compose.foundation:foundation-layout:1.11.0-alpha04
androidx.compose.foundation:foundation:1.11.0-alpha04
androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha07
androidx.compose.material3.adaptive:adaptive:1.3.0-alpha07
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha13
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha13
androidx.compose.material3:material3-android:1.5.0-alpha13
androidx.compose.material3:material3:1.5.0-alpha13
androidx.compose.material:material-android:1.11.0-alpha04
androidx.compose.material:material-icons-core-android:1.7.8
androidx.compose.material:material-icons-core:1.7.8
androidx.compose.material:material-icons-extended-android:1.7.8
androidx.compose.material:material-icons-extended:1.7.8
androidx.compose.material:material-ripple-android:1.11.0-alpha04
androidx.compose.material:material-ripple:1.11.0-alpha04
androidx.compose.material:material:1.11.0-alpha04
androidx.compose.runtime:runtime-android:1.11.0-alpha04
androidx.compose.runtime:runtime-annotation-android:1.11.0-alpha04
androidx.compose.runtime:runtime-annotation:1.11.0-alpha04
androidx.compose.runtime:runtime-livedata:1.11.0-alpha04
androidx.compose.runtime:runtime-retain-android:1.11.0-alpha04
androidx.compose.runtime:runtime-retain:1.11.0-alpha04
androidx.compose.runtime:runtime-saveable-android:1.11.0-alpha04
androidx.compose.runtime:runtime-saveable:1.11.0-alpha04
androidx.compose.runtime:runtime-tracing:1.11.0-alpha04
androidx.compose.runtime:runtime:1.11.0-alpha04
androidx.compose.ui:ui-android:1.11.0-alpha04
androidx.compose.ui:ui-geometry-android:1.11.0-alpha04
androidx.compose.ui:ui-geometry:1.11.0-alpha04
androidx.compose.ui:ui-graphics-android:1.11.0-alpha04
androidx.compose.ui:ui-graphics:1.11.0-alpha04
androidx.compose.ui:ui-text-android:1.11.0-alpha04
androidx.compose.ui:ui-text:1.11.0-alpha04
androidx.compose.ui:ui-tooling-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-data-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-data:1.11.0-alpha04
androidx.compose.ui:ui-tooling-preview-android:1.11.0-alpha04
androidx.compose.ui:ui-tooling-preview:1.11.0-alpha04
androidx.compose.ui:ui-tooling:1.11.0-alpha04
androidx.compose.ui:ui-unit-android:1.11.0-alpha04
androidx.compose.ui:ui-unit:1.11.0-alpha04
androidx.compose.ui:ui-util-android:1.11.0-alpha04
androidx.compose.ui:ui-util:1.11.0-alpha04
androidx.compose.ui:ui:1.11.0-alpha04
androidx.compose:compose-bom-alpha:2026.01.01
androidx.compose:compose-bom:2026.01.00
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.constraintlayout:constraintlayout-core:1.0.0
androidx.constraintlayout:constraintlayout:2.1.0
androidx.coordinatorlayout:coordinatorlayout:1.1.0
androidx.core:core-ktx:1.17.0
androidx.core:core-location-altitude-external-protobuf:1.0.0-beta01
androidx.core:core-location-altitude-proto:1.0.0-beta01
androidx.core:core-location-altitude:1.0.0-beta01
androidx.core:core-splashscreen:1.2.0
androidx.core:core-viewtree:1.0.0
androidx.core:core:1.17.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.1.0
androidx.databinding:viewbinding:8.13.2
androidx.datastore:datastore-android:1.2.0
androidx.datastore:datastore-core-android:1.2.0
androidx.datastore:datastore-core-okio-jvm:1.2.0
androidx.datastore:datastore-core-okio:1.2.0
androidx.datastore:datastore-core:1.2.0
androidx.datastore:datastore-preferences-android:1.2.0
androidx.datastore:datastore-preferences-core-android:1.2.0
androidx.datastore:datastore-preferences-core:1.2.0
androidx.datastore:datastore-preferences-external-protobuf:1.2.0
androidx.datastore:datastore-preferences-proto:1.2.0
androidx.datastore:datastore-preferences:1.2.0
androidx.datastore:datastore:1.2.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.1.0
androidx.emoji2:emoji2-emojipicker:1.6.0
androidx.emoji2:emoji2-views-helper:1.6.0
androidx.emoji2:emoji2:1.6.0
androidx.exifinterface:exifinterface:1.4.1
androidx.fragment:fragment-ktx:1.6.2
androidx.fragment:fragment:1.6.2
androidx.graphics:graphics-path:1.0.1
androidx.graphics:graphics-shapes-android:1.0.1
androidx.graphics:graphics-shapes:1.0.1
androidx.hilt:hilt-common:1.3.0
androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0
androidx.hilt:hilt-lifecycle-viewmodel:1.3.0
androidx.hilt:hilt-work:1.3.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.10.0
androidx.lifecycle:lifecycle-common-jvm:2.10.0
androidx.lifecycle:lifecycle-common:2.10.0
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0
androidx.lifecycle:lifecycle-livedata-core:2.10.0
androidx.lifecycle:lifecycle-livedata-ktx:2.10.0
androidx.lifecycle:lifecycle-livedata:2.10.0
androidx.lifecycle:lifecycle-process:2.10.0
androidx.lifecycle:lifecycle-runtime-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0
androidx.lifecycle:lifecycle-runtime-compose:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0
androidx.lifecycle:lifecycle-runtime:2.10.0
androidx.lifecycle:lifecycle-service:2.10.0
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0
androidx.lifecycle:lifecycle-viewmodel:2.10.0
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
androidx.metrics:metrics-performance:1.0.0-beta03
androidx.navigation3:navigation3-runtime-android:1.0.0
androidx.navigation3:navigation3-runtime:1.0.0
androidx.navigation3:navigation3-ui-android:1.0.0
androidx.navigation3:navigation3-ui:1.0.0
androidx.navigation:navigation-common-android:2.9.7
androidx.navigation:navigation-common:2.9.7
androidx.navigation:navigation-compose-android:2.9.7
androidx.navigation:navigation-compose:2.9.7
androidx.navigation:navigation-fragment:2.9.7
androidx.navigation:navigation-runtime-android:2.9.7
androidx.navigation:navigation-runtime:2.9.7
androidx.navigationevent:navigationevent-android:1.0.2
androidx.navigationevent:navigationevent-compose-android:1.0.2
androidx.navigationevent:navigationevent-compose:1.0.2
androidx.navigationevent:navigationevent:1.0.2
androidx.paging:paging-common-android:3.4.0
androidx.paging:paging-common:3.4.0
androidx.paging:paging-compose-android:3.4.0
androidx.paging:paging-compose:3.4.0
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta11
androidx.privacysandbox.ads:ads-adservices:1.1.0-beta11
androidx.profileinstaller:profileinstaller:1.4.1
androidx.recyclerview:recyclerview:1.3.2
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common-jvm:2.8.4
androidx.room:room-common:2.8.4
androidx.room:room-paging-android:2.8.4
androidx.room:room-paging:2.8.4
androidx.room:room-runtime-android:2.8.4
androidx.room:room-runtime:2.8.4
androidx.savedstate:savedstate-android:1.4.0
androidx.savedstate:savedstate-compose-android:1.4.0
androidx.savedstate:savedstate-compose:1.4.0
androidx.savedstate:savedstate-ktx:1.4.0
androidx.savedstate:savedstate:1.4.0
androidx.slidingpanelayout:slidingpanelayout:1.2.0
androidx.sqlite:sqlite-android:2.6.2
androidx.sqlite:sqlite-framework-android:2.6.2
androidx.sqlite:sqlite-framework:2.6.2
androidx.sqlite:sqlite:2.6.2
androidx.startup:startup-runtime:1.2.0
androidx.tracing:tracing-ktx:1.2.0
androidx.tracing:tracing-perfetto:1.0.1
androidx.tracing:tracing:1.2.0
androidx.transition:transition:1.6.0
androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager2:viewpager2:1.1.0-beta02
androidx.viewpager:viewpager:1.0.0
androidx.window:window-core-android:1.5.0
androidx.window:window-core:1.5.0
androidx.window:window:1.5.0
androidx.work:work-runtime-ktx:2.11.1
androidx.work:work-runtime:2.11.1
co.touchlab:kermit-android:2.0.8
co.touchlab:kermit-core-android:2.0.8
co.touchlab:kermit-core:2.0.8
co.touchlab:kermit:2.0.8
com.caverock:androidsvg-aar:1.4
com.datadoghq:dd-sdk-android-compose:3.6.0
com.datadoghq:dd-sdk-android-core:3.6.0
com.datadoghq:dd-sdk-android-internal:3.6.0
com.datadoghq:dd-sdk-android-logs:3.6.0
com.datadoghq:dd-sdk-android-okhttp:3.6.0
com.datadoghq:dd-sdk-android-rum:3.6.0
com.datadoghq:dd-sdk-android-session-replay-compose:3.6.0
com.datadoghq:dd-sdk-android-session-replay:3.6.0
com.datadoghq:dd-sdk-android-timber:3.6.0
com.datadoghq:dd-sdk-android-trace-api:3.6.0
com.datadoghq:dd-sdk-android-trace-internal:3.6.0
com.datadoghq:dd-sdk-android-trace-otel:3.6.0
com.datadoghq:dd-sdk-android-trace:3.6.0
com.github.mik3y:usb-serial-for-android:3.10.0
com.google.accompanist:accompanist-drawablepainter:0.37.3
com.google.accompanist:accompanist-permissions:0.37.3
com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0
com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.9.0
com.google.android.gms:play-services-location:21.3.0
com.google.android.gms:play-services-maps:20.0.0
com.google.android.gms:play-services-measurement-api:23.0.0
com.google.android.gms:play-services-measurement-base:23.0.0
com.google.android.gms:play-services-measurement-impl:23.0.0
com.google.android.gms:play-services-measurement-sdk-api:23.0.0
com.google.android.gms:play-services-measurement-sdk:23.0.0
com.google.android.gms:play-services-measurement:23.0.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.4.0
com.google.android.material:material:1.13.0
com.google.auto.value:auto-value-annotations:1.6.3
com.google.code.findbugs:jsr305:3.0.2
com.google.code.gson:gson:2.13.2
com.google.dagger:dagger-lint-aar:2.59
com.google.dagger:dagger:2.59
com.google.dagger:hilt-android:2.59
com.google.dagger:hilt-core:2.59
com.google.errorprone:error_prone_annotations:2.41.0
com.google.firebase:firebase-analytics:23.0.0
com.google.firebase:firebase-annotations:17.0.0
com.google.firebase:firebase-bom:34.8.0
com.google.firebase:firebase-common:22.0.1
com.google.firebase:firebase-components:19.0.0
com.google.firebase:firebase-config-interop:16.0.1
com.google.firebase:firebase-crashlytics:20.0.4
com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0
com.google.firebase:firebase-encoders:17.0.0
com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:19.0.1
com.google.firebase:firebase-measurement-connector:20.0.1
com.google.firebase:firebase-sessions:3.0.4
com.google.guava:failureaccess:1.0.3
com.google.guava:guava:33.5.0-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:3.1
com.google.maps.android:android-maps-utils:4.0.0
com.google.maps.android:maps-compose-utils:8.0.0
com.google.maps.android:maps-compose-widgets:8.0.0
com.google.maps.android:maps-compose:8.0.0
com.google.maps.android:maps-ktx:6.0.0
com.google.maps.android:maps-utils-ktx:6.0.0
com.google.re2j:re2j:1.7
com.google.zxing:core:3.5.4
com.jakewharton.timber:timber:5.0.1
com.journeyapps:zxing-android-embedded:4.3.0
com.lyft.kronos:kronos-android:0.0.1-alpha11
com.lyft.kronos:kronos-java:0.0.1-alpha11
com.mikepenz:aboutlibraries-compose-core-android:13.2.1
com.mikepenz:aboutlibraries-compose-core:13.2.1
com.mikepenz:aboutlibraries-compose-m3-android:13.2.1
com.mikepenz:aboutlibraries-compose-m3:13.2.1
com.mikepenz:aboutlibraries-core-android:13.2.1
com.mikepenz:aboutlibraries-core:13.2.1
com.mikepenz:multiplatform-markdown-renderer-android:0.39.2
com.mikepenz:multiplatform-markdown-renderer-m3-android:0.39.2
com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2
com.mikepenz:multiplatform-markdown-renderer:0.39.2
com.patrykandpatrick.vico:compose-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m2-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m2:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m3-android:3.0.0-beta.3
com.patrykandpatrick.vico:compose-m3:3.0.0-beta.3
com.patrykandpatrick.vico:compose:3.0.0-beta.3
com.squareup.okhttp3:logging-interceptor:5.3.2
com.squareup.okhttp3:okhttp-android:5.3.2
com.squareup.okhttp3:okhttp:5.3.2
com.squareup.okio:okio-jvm:3.16.4
com.squareup.okio:okio:3.16.4
com.squareup.wire:wire-runtime-jvm:5.2.1
com.squareup.wire:wire-runtime:5.2.1
io.coil-kt.coil3:coil-android:3.3.0
io.coil-kt.coil3:coil-compose-android:3.3.0
io.coil-kt.coil3:coil-compose-core-android:3.3.0
io.coil-kt.coil3:coil-compose-core:3.3.0
io.coil-kt.coil3:coil-compose:3.3.0
io.coil-kt.coil3:coil-core-android:3.3.0
io.coil-kt.coil3:coil-core:3.3.0
io.coil-kt.coil3:coil-network-core-android:3.3.0
io.coil-kt.coil3:coil-network-core:3.3.0
io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0
io.coil-kt.coil3:coil-network-okhttp:3.3.0
io.coil-kt.coil3:coil-svg-android:3.3.0
io.coil-kt.coil3:coil-svg:3.3.0
io.coil-kt.coil3:coil:3.3.0
io.ktor:ktor-client-content-negotiation-jvm:3.4.0
io.ktor:ktor-client-content-negotiation:3.4.0
io.ktor:ktor-client-core-jvm:3.4.0
io.ktor:ktor-client-core:3.4.0
io.ktor:ktor-client-okhttp-jvm:3.4.0
io.ktor:ktor-client-okhttp:3.4.0
io.ktor:ktor-events-jvm:3.4.0
io.ktor:ktor-events:3.4.0
io.ktor:ktor-http-cio-jvm:3.4.0
io.ktor:ktor-http-cio:3.4.0
io.ktor:ktor-http-jvm:3.4.0
io.ktor:ktor-http:3.4.0
io.ktor:ktor-io-jvm:3.4.0
io.ktor:ktor-io:3.4.0
io.ktor:ktor-network-jvm:3.4.0
io.ktor:ktor-network:3.4.0
io.ktor:ktor-serialization-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx-json-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx-json:3.4.0
io.ktor:ktor-serialization-kotlinx-jvm:3.4.0
io.ktor:ktor-serialization-kotlinx:3.4.0
io.ktor:ktor-serialization:3.4.0
io.ktor:ktor-sse-jvm:3.4.0
io.ktor:ktor-sse:3.4.0
io.ktor:ktor-utils-jvm:3.4.0
io.ktor:ktor-utils:3.4.0
io.ktor:ktor-websocket-serialization-jvm:3.4.0
io.ktor:ktor-websocket-serialization:3.4.0
io.ktor:ktor-websockets-jvm:3.4.0
io.ktor:ktor-websockets:3.4.0
io.opentelemetry:opentelemetry-api:1.40.0
io.opentelemetry:opentelemetry-context:1.40.0
jakarta.inject:jakarta.inject-api:2.0.1
javax.inject:javax.inject:1
no.nordicsemi.android:dfu:2.10.1
no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha12
no.nordicsemi.kotlin.ble:client-core-android:2.0.0-alpha12
no.nordicsemi.kotlin.ble:client-core:2.0.0-alpha12
no.nordicsemi.kotlin.ble:core:2.0.0-alpha12
org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5
org.jctools:jctools-core:3.3.0
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6
org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6
org.jetbrains.androidx.savedstate:savedstate:1.3.6
org.jetbrains.compose.animation:animation-core:1.10.0
org.jetbrains.compose.animation:animation:1.10.0
org.jetbrains.compose.annotation-internal:annotation:1.10.0
org.jetbrains.compose.collection-internal:collection:1.10.0
org.jetbrains.compose.components:components-resources-android:1.10.0
org.jetbrains.compose.components:components-resources:1.10.0
org.jetbrains.compose.foundation:foundation-layout:1.10.0
org.jetbrains.compose.foundation:foundation:1.10.0
org.jetbrains.compose.material3:material3:1.9.0
org.jetbrains.compose.material:material-ripple:1.10.0
org.jetbrains.compose.material:material:1.10.0
org.jetbrains.compose.runtime:runtime-saveable:1.10.0
org.jetbrains.compose.runtime:runtime:1.10.0
org.jetbrains.compose.ui:ui-backhandler-android:1.9.1
org.jetbrains.compose.ui:ui-backhandler:1.9.1
org.jetbrains.compose.ui:ui-geometry:1.10.0
org.jetbrains.compose.ui:ui-graphics:1.10.0
org.jetbrains.compose.ui:ui-text:1.10.0
org.jetbrains.compose.ui:ui-tooling-preview:1.10.0-rc02
org.jetbrains.compose.ui:ui-unit:1.10.0
org.jetbrains.compose.ui:ui-util:1.10.0
org.jetbrains.compose.ui:ui:1.10.0
org.jetbrains.kotlin:kotlin-bom:1.8.22
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21
org.jetbrains.kotlin:kotlin-stdlib:2.3.0
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1-0.6.x-compat
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2
org.jetbrains.kotlinx:kotlinx-io-core:0.8.2
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0
org.jetbrains:annotations:23.0.0
org.jetbrains:markdown-jvm:0.7.3
org.jetbrains:markdown:0.7.3
org.jspecify:jspecify:1.0.0
org.slf4j:slf4j-api:2.0.17

View file

@ -1,92 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues/>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>CyclomaticComplexMethod:BleError.kt$BleError.Companion$fun from(exception: Throwable): BleError</ID>
<ID>CyclomaticComplexMethod:MeshMessageProcessor.kt$MeshMessageProcessor$private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?)</ID>
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
<ID>MagicNumber:Contacts.kt$7</ID>
<ID>MagicNumber:Contacts.kt$8</ID>
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$0xff</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$3</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
<ID>MagicNumber:UIState.kt$4</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"</ID>
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ </ID>
<ID>NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ </ID>
<ID>NoConsecutiveBlankLines:DebugLogFile.kt$ </ID>
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$@Suppress("LongMethod") private fun handleStoreForwardPlusPlus(packet: MeshPacket)</ID>
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
<ID>SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException</ID>
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MeshDataHandler.kt$MeshDataHandler$e: Exception</ID>
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception</ID>
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$t: Throwable</ID>
<ID>TooGenericExceptionCaught:RadioInterfaceService.kt$RadioInterfaceService$t: Throwable</ID>
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect")</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")</ID>
<ID>TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
</CurrentIssues>
</SmellBaseline> </SmellBaseline>

View file

@ -1,50 +1,45 @@
# Add project specific ProGuard rules here. # ============================================================================
# You can control the set of applied configuration files using the # Meshtastic Android ProGuard / R8 rules for release minification
# proguardFiles setting in build.gradle.kts. # ============================================================================
# Open-source project: obfuscation and optimization are disabled. We rely on
# tree-shaking (unused code removal) for APK size reduction.
# #
# For more details, see # Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room,
# http://developer.android.com/guide/developing/tools/proguard.html # Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3,
# AboutLibraries, Markdown, QRCode, CMP resources, core model) live in
# config/proguard/shared-rules.pro and are wired in by the
# AndroidApplicationConventionPlugin. This file holds only Android-specific
# rules and R8-only directives.
# ============================================================================
# If your project uses WebView with JS, uncomment the following # ---- General ----------------------------------------------------------------
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for # Open-source no need to obfuscate
# debugging stack traces. -dontobfuscate
-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to # Disable R8 optimization passes. Tree-shaking (unused code removal) still
# hide the original source file name. # runs only method-body rewrites and call-site transformations are suppressed.
#-renamesourcefileattribute SourceFile #
# Why: CMP 1.11 ships consumer rules with -assumenosideeffects on
# Composer.<clinit>() and ComposerImpl.<clinit>(), plus -assumevalues on
# ComposeRuntimeFlags and ComposeStackTraceMode. These optimization directives
# let R8 rewrite *call sites* (class-init triggers, flag reads) even when the
# target classes are preserved by -keep rules. The result is that the Compose
# recomposer/frame-clock/animation state machines silently freeze on their
# first frame in release builds. -dontoptimize is the only directive that
# disables processing of -assumenosideeffects/-assumevalues. See #5146.
-dontoptimize
# Needed for protobufs # Dump the full merged R8 configuration (app rules + all library consumer rules)
-keep class com.google.protobuf.** { *; } # for auditing. Inspect this file after a release build to see what libraries inject.
-keep class org.meshtastic.proto.** { *; } -printconfiguration build/outputs/mapping/r8-merged-config.txt
# eclipse.paho.client # ---- Networking (transitive references from Ktor on Android) ----------------
-keep class org.eclipse.paho.client.mqttv3.logging.JSR47Logger { *; }
# OkHttp
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.** -dontwarn org.bouncycastle.**
-dontwarn org.openjsse.** -dontwarn org.openjsse.**
# ? # Compose runtime/ui/animation/foundation/material3 keep rules now live in
-dontwarn java.lang.reflect.** # config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard)
-dontwarn com.google.errorprone.annotations.** # get the same defence-in-depth coverage against CMP 1.11 optimizer folding.
# Our app is opensource no need to obsfucate
-dontobfuscate
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
# R8 optimization for Kotlin null checks (AGP 9.0+)
-processkotlinnullchecks remove
# Nordic BLE
-dontwarn no.nordicsemi.kotlin.ble.environment.android.mock.**
-keep class no.nordicsemi.kotlin.ble.environment.android.mock.** { *; }
-keep class no.nordicsemi.kotlin.ble.environment.android.compose.** { *; }

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) 2025-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 com.geeksville.mesh.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.prefs.filter.FilterPrefs
import org.meshtastic.core.service.filter.MessageFilterService
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MessageFilterIntegrationTest {
@get:Rule var hiltRule = HiltAndroidRule(this)
@Inject lateinit var filterPrefs: FilterPrefs
@Inject lateinit var filterService: MessageFilterService
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun filterPrefsIntegration() = runTest {
filterPrefs.filterEnabled = true
filterPrefs.filterWords = setOf("test", "spam")
filterService.rebuildPatterns()
assertTrue(filterService.shouldFilter("this is a test message"))
assertTrue(filterService.shouldFilter("spam content"))
}
}

View file

@ -15,14 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.cluster; package org.meshtastic.app.map.cluster;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Point; import android.graphics.Point;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.meshtastic.feature.map.model.MarkerWithLabel; import org.meshtastic.app.map.model.MarkerWithLabel;
import org.osmdroid.util.BoundingBox; import org.osmdroid.util.BoundingBox;
import org.osmdroid.views.MapView; import org.osmdroid.views.MapView;

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.cluster; package org.meshtastic.app.map.cluster;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -27,7 +27,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.meshtastic.feature.map.model.MarkerWithLabel; import org.meshtastic.app.map.model.MarkerWithLabel;
import org.osmdroid.bonuspack.R; import org.osmdroid.bonuspack.R;
import org.osmdroid.util.BoundingBox; import org.osmdroid.util.BoundingBox;

View file

@ -15,9 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.cluster; package org.meshtastic.app.map.cluster;
import org.meshtastic.feature.map.model.MarkerWithLabel; import org.meshtastic.app.map.model.MarkerWithLabel;
import org.osmdroid.util.BoundingBox; import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,29 +14,28 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.analytics
package org.meshtastic.core.analytics.platform
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity import co.touchlab.kermit.Severity
import org.meshtastic.core.analytics.BuildConfig import org.koin.core.annotation.Single
import org.meshtastic.core.analytics.DataPair import org.meshtastic.app.BuildConfig
import javax.inject.Inject import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
/** /**
* F-Droid specific implementation of [org.meshtastic.analytics.platform.PlatformAnalytics]. This provides no-op * F-Droid specific implementation of [PlatformAnalytics]. This provides no-op implementations for analytics and other
* implementations for analytics and other platform services. * platform services.
*/ */
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics { @Single
class FdroidPlatformAnalytics : PlatformAnalytics {
init { init {
// For F-Droid builds we don't initialize external analytics services. // For F-Droid builds we don't initialize external analytics services.
// In debug builds we attach a DebugTree for convenient local logging, but // In debug builds we attach a DebugTree for convenient local logging, but
// release builds rely on system logging only. // release builds rely on system logging only.
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.setMinSeverity(Severity.Debug) Logger.setMinSeverity(Severity.Debug)
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode }." } Logger.i { "F-Droid platform no-op analytics initialized (Debug mode)." }
} else { } else {
Logger.setMinSeverity(Severity.Info) Logger.setMinSeverity(Severity.Info)
Logger.i { "F-Droid platform no-op analytics initialized." } Logger.i { "F-Droid platform no-op analytics initialized." }
@ -48,16 +47,6 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" } Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
} }
@Composable
override fun AddNavigationTrackingEffect(navController: NavHostController) {
// No-op for F-Droid, but we can log navigation if needed for debugging
if (BuildConfig.DEBUG) {
navController.addOnDestinationChangedListener { _, destination, _ ->
Logger.d { "Navigation changed to: ${destination.route}" }
}
}
}
override val isPlatformServicesAvailable: Boolean override val isPlatformServicesAvailable: Boolean
get() = false get() = false

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,40 +14,18 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.di
package org.meshtastic.core.network.di import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.meshtastic.core.model.NetworkDeviceHardware import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.BuildConfig
import org.meshtastic.core.network.service.ApiService import org.meshtastic.core.network.service.ApiService
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module @Module
class FDroidNetworkModule { class FDroidNetworkModule {
@Provides @Single
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
interceptor =
HttpLoggingInterceptor().apply {
if (BuildConfig.DEBUG) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
@Provides
@Singleton
fun provideApiService(): ApiService = object : ApiService { fun provideApiService(): ApiService = object : ApiService {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> = override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.") throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,11 +14,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.di
package org.meshtastic.core.di import org.koin.core.annotation.Module
import javax.inject.Qualifier @Module(includes = [FDroidNetworkModule::class])
class FlavorModule
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProcessLifecycle

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.intro
package org.meshtastic.feature.intro
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View file

@ -0,0 +1,39 @@
/*
* 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.app.map
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
/** OSMDroid implementation of [MapViewProvider]. */
@Single
class FdroidMapViewProvider : MapViewProvider {
@Composable
override fun MapView(modifier: Modifier, navigateToNodeDetails: (Int) -> Unit, waypointId: Int?) {
val mapViewModel: MapViewModel = koinViewModel()
LaunchedEffect(waypointId) { mapViewModel.setWaypointId(waypointId) }
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,
navigateToNodeDetails = navigateToNodeDetails,
)
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.app.map
import org.meshtastic.core.ui.util.MapViewProvider
fun getMapViewProvider(): MapViewProvider = FdroidMapViewProvider()

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map
package org.meshtastic.feature.map
import android.content.Context import android.content.Context
import android.util.TypedValue import android.util.TypedValue

View file

@ -14,13 +14,11 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map package org.meshtastic.app.map
import android.Manifest import android.Manifest
import android.graphics.Paint
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
@ -28,29 +26,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Lens
import androidx.compose.material.icons.rounded.LocationDisabled
import androidx.compose.material.icons.rounded.PinDrop
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -59,6 +49,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -66,15 +57,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -83,14 +71,20 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.R
import org.meshtastic.app.map.cluster.RadiusMarkerClusterer
import org.meshtastic.app.map.component.CacheLayout
import org.meshtastic.app.map.component.DownloadButton
import org.meshtastic.app.map.component.EditWaypointDialog
import org.meshtastic.app.map.model.CustomTileSource
import org.meshtastic.app.map.model.MarkerWithLabel
import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.toString import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.calculating import org.meshtastic.core.resources.calculating
import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.cancel
@ -100,10 +94,8 @@ import org.meshtastic.core.resources.delete_for_everyone
import org.meshtastic.core.resources.delete_for_me import org.meshtastic.core.resources.delete_for_me
import org.meshtastic.core.resources.expires import org.meshtastic.core.resources.expires
import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.heading import org.meshtastic.core.resources.last_heard_filter_label
import org.meshtastic.core.resources.latitude
import org.meshtastic.core.resources.location_disabled import org.meshtastic.core.resources.location_disabled
import org.meshtastic.core.resources.longitude
import org.meshtastic.core.resources.map_cache_info import org.meshtastic.core.resources.map_cache_info
import org.meshtastic.core.resources.map_cache_manager import org.meshtastic.core.resources.map_cache_manager
import org.meshtastic.core.resources.map_cache_size import org.meshtastic.core.resources.map_cache_size
@ -112,7 +104,6 @@ import org.meshtastic.core.resources.map_clear_tiles
import org.meshtastic.core.resources.map_download_complete import org.meshtastic.core.resources.map_download_complete
import org.meshtastic.core.resources.map_download_errors import org.meshtastic.core.resources.map_download_errors
import org.meshtastic.core.resources.map_download_region import org.meshtastic.core.resources.map_download_region
import org.meshtastic.core.resources.map_filter
import org.meshtastic.core.resources.map_node_popup_details import org.meshtastic.core.resources.map_node_popup_details
import org.meshtastic.core.resources.map_offline_manager import org.meshtastic.core.resources.map_offline_manager
import org.meshtastic.core.resources.map_purge_fail import org.meshtastic.core.resources.map_purge_fail
@ -121,27 +112,25 @@ import org.meshtastic.core.resources.map_style_selection
import org.meshtastic.core.resources.map_subDescription import org.meshtastic.core.resources.map_subDescription
import org.meshtastic.core.resources.map_tile_source import org.meshtastic.core.resources.map_tile_source
import org.meshtastic.core.resources.only_favorites import org.meshtastic.core.resources.only_favorites
import org.meshtastic.core.resources.position
import org.meshtastic.core.resources.show_precision_circle import org.meshtastic.core.resources.show_precision_circle
import org.meshtastic.core.resources.show_waypoints import org.meshtastic.core.resources.show_waypoints
import org.meshtastic.core.resources.toggle_my_position
import org.meshtastic.core.resources.waypoint_delete import org.meshtastic.core.resources.waypoint_delete
import org.meshtastic.core.resources.you import org.meshtastic.core.resources.you
import org.meshtastic.core.ui.component.BasicListItem import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.theme.TracerouteColors import org.meshtastic.core.ui.icon.Check
import org.meshtastic.core.ui.icon.Favorite
import org.meshtastic.core.ui.icon.Layers
import org.meshtastic.core.ui.icon.Lens
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.PinDrop
import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.core.ui.util.showToast import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
import org.meshtastic.feature.map.component.CacheLayout import org.meshtastic.feature.map.LastHeardFilter
import org.meshtastic.feature.map.component.DownloadButton
import org.meshtastic.feature.map.component.EditWaypointDialog
import org.meshtastic.feature.map.component.MapButton import org.meshtastic.feature.map.component.MapButton
import org.meshtastic.feature.map.model.CustomTileSource import org.meshtastic.feature.map.component.MapControlsOverlay
import org.meshtastic.feature.map.model.MarkerWithLabel
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.Position
import org.meshtastic.proto.Waypoint import org.meshtastic.proto.Waypoint
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
@ -160,38 +149,23 @@ import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.MapEventsOverlay import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.infowindow.InfoWindow import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.roundToInt
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
private fun MapView.updateMarkers( private fun MapView.updateMarkers(
nodeMarkers: List<MarkerWithLabel>, nodeMarkers: List<MarkerWithLabel>,
waypointMarkers: List<MarkerWithLabel>, waypointMarkers: List<MarkerWithLabel>,
trackMarkers: List<Marker>,
trackPolylines: List<Polyline>,
nodeClusterer: RadiusMarkerClusterer, nodeClusterer: RadiusMarkerClusterer,
) { ) {
Logger.d { Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
"Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints ${trackMarkers.size} tracks"
}
val trackOverlayIds = (trackMarkers + trackPolylines).toSet()
overlays.removeAll { overlay -> overlays.removeAll { overlay ->
overlay is MarkerWithLabel || overlay is MarkerWithLabel || (overlay is Marker && overlay !in nodeClusterer.items)
(overlay is Marker && overlay !in nodeClusterer.items && overlay !in trackOverlayIds) ||
(overlay is Polyline && overlay !in trackOverlayIds)
} }
overlays.addAll(waypointMarkers) overlays.addAll(waypointMarkers)
overlays.addAll(trackPolylines)
overlays.addAll(trackMarkers)
nodeClusterer.items.clear() nodeClusterer.items.clear()
nodeClusterer.items.addAll(nodeMarkers) nodeClusterer.items.addAll(nodeMarkers)
@ -229,16 +203,12 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
* @param navigateToNodeDetails Callback to navigate to the details screen of a selected node. * @param navigateToNodeDetails Callback to navigate to the details screen of a selected node.
*/ */
@OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist @OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist
@Suppress("CyclomaticComplexMethod", "LongParameterList", "LongMethod") @Suppress("CyclomaticComplexMethod", "LongMethod")
@Composable @Composable
fun MapView( fun MapView(
mapViewModel: MapViewModel = hiltViewModel(), modifier: Modifier = Modifier,
mapViewModel: MapViewModel = koinViewModel(),
navigateToNodeDetails: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit,
focusedNodeNum: Int? = null,
nodeTracks: List<Position>? = null,
tracerouteOverlay: TracerouteOverlay? = null,
tracerouteNodePositions: Map<Int, Position> = emptyMap(),
onTracerouteMappableCountChanged: (shown: Int, total: Int) -> Unit = { _, _ -> },
) { ) {
var mapFilterExpanded by remember { mutableStateOf(false) } var mapFilterExpanded by remember { mutableStateOf(false) }
@ -337,6 +307,16 @@ fun MapView(
} }
} }
// Keep screen on while location tracking is active
LaunchedEffect(myLocationOverlay) {
val activity = context as? android.app.Activity ?: return@LaunchedEffect
if (myLocationOverlay != null) {
activity.window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
activity.window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
val nodes by mapViewModel.nodes.collectAsStateWithLifecycle() val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap()) val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle() val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle()
@ -344,7 +324,7 @@ fun MapView(
LaunchedEffect(selectedWaypointId, waypoints) { LaunchedEffect(selectedWaypointId, waypoints) {
if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) { if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) {
waypoints[selectedWaypointId]?.data?.waypoint?.let { pt -> waypoints[selectedWaypointId]?.waypoint?.let { pt ->
val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7) val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
map.controller.setCenter(geoPoint) map.controller.setCenter(geoPoint)
map.controller.setZoom(WAYPOINT_ZOOM) map.controller.setZoom(WAYPOINT_ZOOM)
@ -352,77 +332,21 @@ fun MapView(
} }
} }
val tracerouteSelection =
remember(tracerouteOverlay, tracerouteNodePositions, nodes) {
mapViewModel.tracerouteNodeSelection(
tracerouteOverlay = tracerouteOverlay,
tracerouteNodePositions = tracerouteNodePositions,
nodes = nodes,
)
}
val overlayNodeNums = tracerouteSelection.overlayNodeNums
val nodeLookup = tracerouteSelection.nodeLookup
val nodesForMarkers = tracerouteSelection.nodesForMarkers
val tracerouteForwardPoints =
remember(tracerouteOverlay, nodeLookup) {
tracerouteOverlay?.forwardRoute?.mapNotNull {
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
} ?: emptyList()
}
val tracerouteReturnPoints =
remember(tracerouteOverlay, nodeLookup) {
tracerouteOverlay?.returnRoute?.mapNotNull {
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
} ?: emptyList()
}
LaunchedEffect(tracerouteOverlay, nodesForMarkers) {
if (tracerouteOverlay != null) {
onTracerouteMappableCountChanged(nodesForMarkers.size, tracerouteOverlay.relatedNodeNums.size)
}
}
val tracerouteHeadingReferencePoints =
remember(tracerouteForwardPoints, tracerouteReturnPoints) {
when {
tracerouteForwardPoints.size >= 2 -> tracerouteForwardPoints
tracerouteReturnPoints.size >= 2 -> tracerouteReturnPoints
else -> emptyList()
}
}
val tracerouteForwardOffsetPoints =
remember(tracerouteForwardPoints, tracerouteHeadingReferencePoints) {
offsetPolyline(
points = tracerouteForwardPoints,
offsetMeters = TRACEROUTE_OFFSET_METERS,
headingReferencePoints = tracerouteHeadingReferencePoints,
sideMultiplier = 1.0,
)
}
val tracerouteReturnOffsetPoints =
remember(tracerouteReturnPoints, tracerouteHeadingReferencePoints) {
offsetPolyline(
points = tracerouteReturnPoints,
offsetMeters = TRACEROUTE_OFFSET_METERS,
headingReferencePoints = tracerouteHeadingReferencePoints,
sideMultiplier = -1.0,
)
}
val traceroutePolylines = remember { mutableStateListOf<Polyline>() }
var hasCenteredTraceroute by remember(tracerouteOverlay) { mutableStateOf(false) }
val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) } val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) }
fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> { fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> {
val nodesWithPosition = nodes.filter { it.validPosition != null } val nodesWithPosition = nodes.filter { it.validPosition != null }
val ourNode = mapViewModel.ourNodeInfo.value val ourNode = mapViewModel.ourNodeInfo.value
val displayUnits = val displayUnits = mapViewModel.config.display?.units ?: DisplayUnits.METRIC
mapViewModel.config.display?.units ?: org.meshtastic.proto.Config.DisplayConfig.DisplayUnits.METRIC
val mapFilterStateValue = mapViewModel.mapFilterStateFlow.value // Access mapFilterState directly val mapFilterStateValue = mapViewModel.mapFilterStateFlow.value // Access mapFilterState directly
return nodesWithPosition.mapNotNull { node -> return nodesWithPosition.mapNotNull { node ->
if (mapFilterStateValue.onlyFavorites && !node.isFavorite && !node.equals(ourNode)) {
return@mapNotNull null
}
if ( if (
mapFilterStateValue.onlyFavorites && mapFilterStateValue.lastHeardFilter.seconds != 0L &&
!node.isFavorite && (nowSeconds - node.lastHeard) > mapFilterStateValue.lastHeardFilter.seconds &&
!overlayNodeNums.contains(node.num) && node.num != ourNode?.num
!node.equals(ourNode)
) { ) {
return@mapNotNull null return@mapNotNull null
} }
@ -441,7 +365,9 @@ fun MapView(
if (node.batteryStr != "") node.batteryStr else "?", if (node.batteryStr != "") node.batteryStr else "?",
) )
ourNode?.distanceStr(node, displayUnits)?.let { dist -> ourNode?.distanceStr(node, displayUnits)?.let { dist ->
subDescription = getString(Res.string.map_subDescription, ourNode.bearing(node).toString(), dist) ourNode.bearing(node)?.let { bearing ->
subDescription = getString(Res.string.map_subDescription, bearing, dist)
}
} }
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
position = nodePosition position = nodePosition
@ -450,7 +376,7 @@ fun MapView(
if (!mapFilterStateValue.showPrecisionCircle) { if (!mapFilterStateValue.showPrecisionCircle) {
setPrecisionBits(0) setPrecisionBits(0)
} else { } else {
setPrecisionBits(p.precision_bits ?: 0) setPrecisionBits(p.precision_bits)
} }
setOnLongClickListener { setOnLongClickListener {
navigateToNodeDetails(node.num) navigateToNodeDetails(node.num)
@ -470,7 +396,7 @@ fun MapView(
Logger.d { "User deleted waypoint ${waypoint.id} for me" } Logger.d { "User deleted waypoint ${waypoint.id} for me" }
mapViewModel.deleteWaypoint(waypoint.id) mapViewModel.deleteWaypoint(waypoint.id)
} }
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
builder.setPositiveButton(getString(Res.string.delete_for_everyone)) { _, _ -> builder.setPositiveButton(getString(Res.string.delete_for_everyone)) { _, _ ->
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" } Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
mapViewModel.sendWaypoint(waypoint.copy(expire = 1)) mapViewModel.sendWaypoint(waypoint.copy(expire = 1))
@ -496,9 +422,9 @@ fun MapView(
fun showMarkerLongPressDialog(id: Int) { fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback() performHapticFeedback()
Logger.d { "marker long pressed id=$id" } Logger.d { "marker long pressed id=$id" }
val waypoint = waypoints[id]?.data?.waypoint ?: return val waypoint = waypoints[id]?.waypoint ?: return
// edit only when unlocked or lockedTo myNodeNum // edit only when unlocked or lockedTo myNodeNum
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) { if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
showEditWaypointDialog = waypoint showEditWaypointDialog = waypoint
} else { } else {
showDeleteMarkerDialog(waypoint) showDeleteMarkerDialog(waypoint)
@ -512,25 +438,25 @@ fun MapView(
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun MapView.onWaypointChanged(waypoints: Collection<Packet>, selectedWaypointId: Int?): List<MarkerWithLabel> { fun MapView.onWaypointChanged(waypoints: Collection<DataPacket>, selectedWaypointId: Int?): List<MarkerWithLabel> {
return waypoints.mapNotNull { waypoint -> return waypoints.mapNotNull { waypoint ->
val pt = waypoint.data.waypoint ?: return@mapNotNull null val pt = waypoint.waypoint ?: return@mapNotNull null
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else "" val lock = if (pt.locked_to != 0) "\uD83D\uDD12" else ""
val time = DateFormatter.formatDateTime(waypoint.received_time) val time = DateFormatter.formatDateTime(waypoint.time)
val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt()) val label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt())
val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!)) val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
val now = nowMillis val now = nowMillis
val expireTimeMillis = (pt.expire ?: 0) * 1000L val expireTimeMillis = pt.expire * 1000L
val expireTimeStr = val expireTimeStr =
when { when {
(pt.expire ?: 0) == 0 || pt.expire == Int.MAX_VALUE -> "Never" pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never"
expireTimeMillis <= now -> "Expired" expireTimeMillis <= now -> "Expired"
else -> DateFormatter.formatRelativeTime(expireTimeMillis) else -> DateFormatter.formatRelativeTime(expireTimeMillis)
} }
MarkerWithLabel(this, label, emoji).apply { MarkerWithLabel(this, label, emoji).apply {
id = "${pt.id}" id = "${pt.id}"
title = "${pt.name} (${getUsername(waypoint.data.from)}$lock)" title = "${pt.name} (${getUsername(waypoint.from)}$lock)"
snippet = "[$time] ${pt.description} " + getString(Res.string.expires) + ": $expireTimeStr" snippet = "[$time] ${pt.description} " + getString(Res.string.expires) + ": $expireTimeStr"
position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7) position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
if (selectedWaypointId == pt.id) { if (selectedWaypointId == pt.id) {
@ -581,53 +507,6 @@ fun MapView(
invalidate() invalidate()
} }
fun MapView.updateTracerouteOverlay(forwardPoints: List<GeoPoint>, returnPoints: List<GeoPoint>) {
overlays.removeAll(traceroutePolylines)
traceroutePolylines.clear()
fun buildPolyline(points: List<GeoPoint>, color: Int, strokeWidth: Float): Polyline = Polyline().apply {
setPoints(points)
outlinePaint.apply {
this.color = color
this.strokeWidth = strokeWidth
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
style = Paint.Style.STROKE
}
}
forwardPoints
.takeIf { it.size >= 2 }
?.let { points ->
traceroutePolylines.add(
buildPolyline(points, TracerouteColors.OutgoingRoute.toArgb(), with(density) { 6.dp.toPx() }),
)
}
returnPoints
.takeIf { it.size >= 2 }
?.let { points ->
traceroutePolylines.add(
buildPolyline(points, TracerouteColors.ReturnRoute.toArgb(), with(density) { 5.dp.toPx() }),
)
}
overlays.addAll(traceroutePolylines)
invalidate()
}
LaunchedEffect(tracerouteOverlay, tracerouteForwardPoints, tracerouteReturnPoints) {
if (tracerouteOverlay == null || hasCenteredTraceroute) return@LaunchedEffect
val allPoints = (tracerouteForwardPoints + tracerouteReturnPoints).distinct()
if (allPoints.isNotEmpty()) {
if (allPoints.size == 1) {
map.controller.setCenter(allPoints.first())
map.controller.setZoom(TRACEROUTE_SINGLE_POINT_ZOOM)
} else {
map.zoomToBoundingBox(BoundingBox.fromGeoPoints(allPoints).zoomIn(-TRACEROUTE_ZOOM_OUT_LEVELS), true)
}
hasCenteredTraceroute = true
}
}
fun MapView.generateBoxOverlay() { fun MapView.generateBoxOverlay() {
overlays.removeAll { it is Polygon } overlays.removeAll { it is Polygon }
val zoomFactor = 1.3 val zoomFactor = 1.3
@ -690,52 +569,8 @@ fun MapView(
} }
} }
fun MapView.onTracksChanged(nodeTracks: List<Position>?, focusedNodeNum: Int?): Pair<List<Marker>, List<Polyline>> {
if (nodeTracks == null || focusedNodeNum == null) return emptyList<Marker>() to emptyList<Polyline>()
val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter
val timeFilteredPositions =
nodeTracks.filter {
lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds
}
val sortedPositions = timeFilteredPositions.sortedBy { it.time }
val focusedNode = nodes.find { it.num == focusedNodeNum } ?: return emptyList<Marker>() to emptyList<Polyline>()
val color = focusedNode.colors.second
val trackPolylines = mutableListOf<Polyline>()
if (sortedPositions.size > 1) {
val segments = sortedPositions.windowed(size = 2, step = 1, partialWindows = false)
segments.forEachIndexed { index, segmentPoints ->
val alpha = (index.toFloat() / (segments.size.toFloat() - 1))
val polyline =
Polyline().apply {
setPoints(
segmentPoints.map { GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) },
)
outlinePaint.color = Color(color).copy(alpha = alpha).toArgb()
outlinePaint.strokeWidth = 8f
}
trackPolylines.add(polyline)
}
}
val trackMarkers =
sortedPositions.mapIndexedNotNull { index, position ->
if (index == sortedPositions.lastIndex) return@mapIndexedNotNull null
Marker(this).apply {
this.position = GeoPoint((position.latitude_i ?: 0) * 1e-7, (position.longitude_i ?: 0) * 1e-7)
icon = AppCompatResources.getDrawable(context, R.drawable.ic_map_location_dot)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
title = getString(Res.string.position)
snippet = formatAgo(position.time)
}
}
return trackMarkers to trackPolylines
}
Scaffold( Scaffold(
modifier = modifier,
floatingActionButton = { floatingActionButton = {
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true } DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true }
}, },
@ -750,14 +585,10 @@ fun MapView(
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
update = { mapView -> update = { mapView ->
mapView.updateTracerouteOverlay(tracerouteForwardOffsetPoints, tracerouteReturnOffsetPoints)
val (trackMarkers, trackPolylines) = mapView.onTracksChanged(nodeTracks, focusedNodeNum)
with(mapView) { with(mapView) {
updateMarkers( updateMarkers(
onNodesChanged(nodesForMarkers), onNodesChanged(nodes),
onWaypointChanged(waypoints.values, selectedWaypointId), onWaypointChanged(waypoints.values, selectedWaypointId),
trackMarkers,
trackPolylines,
nodeClusterer, nodeClusterer,
) )
} }
@ -776,122 +607,34 @@ fun MapView(
modifier = Modifier.align(Alignment.BottomCenter), modifier = Modifier.align(Alignment.BottomCenter),
) )
} else { } else {
@Suppress("MagicNumber") MapControlsOverlay(
Column( modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
modifier = Modifier.padding(top = 16.dp, end = 16.dp).align(Alignment.TopEnd), onToggleFilterMenu = { mapFilterExpanded = true },
verticalArrangement = Arrangement.spacedBy(8.dp), filterDropdownContent = {
) { FdroidMainMapFilterDropdown(
MapButton(
onClick = { showMapStyleDialog = true },
icon = Icons.Outlined.Layers,
contentDescription = Res.string.map_style_selection,
)
Box(modifier = Modifier) {
MapButton(
onClick = { mapFilterExpanded = true },
icon = Icons.Outlined.Tune,
contentDescription = stringResource(Res.string.map_filter),
)
DropdownMenu(
expanded = mapFilterExpanded, expanded = mapFilterExpanded,
onDismissRequest = { mapFilterExpanded = false }, onDismissRequest = { mapFilterExpanded = false },
modifier = Modifier.background(MaterialTheme.colorScheme.surface), mapFilterState = mapFilterState,
) { mapViewModel = mapViewModel,
DropdownMenuItem( )
text = { },
Row( mapTypeContent = {
modifier = Modifier.fillMaxWidth(), MapButton(
verticalAlignment = Alignment.CenterVertically, icon = MeshtasticIcons.Layers,
) { contentDescription = stringResource(Res.string.map_style_selection),
Icon( onClick = { showMapStyleDialog = true },
imageVector = Icons.Rounded.Star, )
contentDescription = null, },
modifier = Modifier.padding(end = 8.dp), isLocationTrackingEnabled = myLocationOverlay != null,
tint = MaterialTheme.colorScheme.onSurface, onToggleLocationTracking = {
)
Text(
text = stringResource(Res.string.only_favorites),
modifier = Modifier.weight(1f),
)
Checkbox(
checked = mapFilterState.onlyFavorites,
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleOnlyFavorites() },
)
DropdownMenuItem(
text = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.PinDrop,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(Res.string.show_waypoints),
modifier = Modifier.weight(1f),
)
Checkbox(
checked = mapFilterState.showWaypoints,
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
)
DropdownMenuItem(
text = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.Lens,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(Res.string.show_precision_circle),
modifier = Modifier.weight(1f),
)
@Suppress("MagicNumber")
Checkbox(
checked = mapFilterState.showPrecisionCircle,
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
)
}
}
MapButton(
icon =
if (myLocationOverlay == null) {
Icons.Outlined.MyLocation
} else {
Icons.Rounded.LocationDisabled
},
contentDescription = stringResource(Res.string.toggle_my_position),
) {
if (locationPermissionsState.allPermissionsGranted) { if (locationPermissionsState.allPermissionsGranted) {
map.toggleMyLocation() map.toggleMyLocation()
} else { } else {
triggerLocationToggleAfterPermission = true triggerLocationToggleAfterPermission = true
locationPermissionsState.launchMultiplePermissionRequest() locationPermissionsState.launchMultiplePermissionRequest()
} }
} },
} )
} }
} }
} }
@ -941,12 +684,11 @@ fun MapView(
Logger.d { "User clicked send waypoint ${waypoint.id}" } Logger.d { "User clicked send waypoint ${waypoint.id}" }
showEditWaypointDialog = null showEditWaypointDialog = null
val newId = val newId = if (waypoint.id == 0) mapViewModel.generatePacketId() else waypoint.id
if (waypoint.id == 0) mapViewModel.generatePacketId() ?: return@EditWaypointDialog else waypoint.id
val newName = if (waypoint.name.isNullOrEmpty()) "Dropped Pin" else waypoint.name val newName = if (waypoint.name.isNullOrEmpty()) "Dropped Pin" else waypoint.name
val newExpire = if ((waypoint.expire ?: 0) == 0) Int.MAX_VALUE else (waypoint.expire ?: Int.MAX_VALUE) val newExpire = if (waypoint.expire == 0) Int.MAX_VALUE else waypoint.expire
val newLockedTo = if ((waypoint.locked_to ?: 0) != 0) mapViewModel.myNodeNum ?: 0 else 0 val newLockedTo = if (waypoint.locked_to != 0) mapViewModel.myNodeNum ?: 0 else 0
val newIcon = if ((waypoint.icon ?: 0) == 0) 128205 else waypoint.icon val newIcon = if (waypoint.icon == 0) 128205 else waypoint.icon
mapViewModel.sendWaypoint( mapViewModel.sendWaypoint(
waypoint.copy( waypoint.copy(
@ -971,6 +713,103 @@ fun MapView(
} }
} }
/** F-Droid main map filter dropdown — favorites, waypoints, precision circle, and last-heard time filter slider. */
@Composable
private fun FdroidMainMapFilterDropdown(
expanded: Boolean,
onDismissRequest: () -> Unit,
mapFilterState: MapFilterState,
mapViewModel: MapViewModel,
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
) {
DropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = MeshtasticIcons.Favorite,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Text(text = stringResource(Res.string.only_favorites), modifier = Modifier.weight(1f))
Checkbox(
checked = mapFilterState.onlyFavorites,
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleOnlyFavorites() },
)
DropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = MeshtasticIcons.PinDrop,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Text(text = stringResource(Res.string.show_waypoints), modifier = Modifier.weight(1f))
Checkbox(
checked = mapFilterState.showWaypoints,
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
)
DropdownMenuItem(
text = {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = MeshtasticIcons.Lens,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Text(text = stringResource(Res.string.show_precision_circle), modifier = Modifier.weight(1f))
Checkbox(
checked = mapFilterState.showPrecisionCircle,
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
modifier = Modifier.padding(start = 8.dp),
)
}
},
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
)
HorizontalDivider()
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val filterOptions = LastHeardFilter.entries
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)
var sliderPosition by remember(selectedIndex) { mutableFloatStateOf(selectedIndex.toFloat()) }
Text(
text =
stringResource(
Res.string.last_heard_filter_label,
stringResource(mapFilterState.lastHeardFilter.label),
),
style = MaterialTheme.typography.labelLarge,
)
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
onValueChangeFinished = {
val newIndex = sliderPosition.roundToInt().coerceIn(0, filterOptions.size - 1)
mapViewModel.setLastHeardFilter(filterOptions[newIndex])
},
valueRange = 0f..(filterOptions.size - 1).toFloat(),
steps = filterOptions.size - 2,
)
}
}
}
@Composable @Composable
private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) { private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) {
val selected = remember { mutableStateOf(selectedMapStyle) } val selected = remember { mutableStateOf(selectedMapStyle) }
@ -979,7 +818,7 @@ private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelec
CustomTileSource.mTileSources.values.forEachIndexed { index, style -> CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
ListItem( ListItem(
text = style, text = style,
trailingIcon = if (index == selected.value) Icons.Rounded.Check else null, trailingIcon = if (index == selected.value) MeshtasticIcons.Check else null,
onClick = { onClick = {
selected.value = index selected.value = index
onSelectMapStyle(index) onSelectMapStyle(index)
@ -1022,15 +861,9 @@ private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
onDismiss = onDismiss, onDismiss = onDismiss,
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } }, negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
) { ) {
Text( val capacityMb = (cacheCapacity / (1024 * 1024)).toLong()
modifier = Modifier.padding(16.dp), val usageMb = (currentCacheUsage / (1024 * 1024)).toLong()
text = Text(modifier = Modifier.padding(16.dp), text = stringResource(Res.string.map_cache_info, capacityMb, usageMb))
stringResource(
Res.string.map_cache_info,
cacheCapacity / (1024.0 * 1024.0),
currentCacheUsage / (1024.0 * 1024.0),
),
)
} }
} }
@ -1126,57 +959,4 @@ private fun MapsDialog(
} }
} }
private const val EARTH_RADIUS_METERS = 6_371_000.0
private const val TRACEROUTE_OFFSET_METERS = 100.0
private const val TRACEROUTE_SINGLE_POINT_ZOOM = 12.0
private const val TRACEROUTE_ZOOM_OUT_LEVELS = 0.5
private const val WAYPOINT_ZOOM = 15.0 private const val WAYPOINT_ZOOM = 15.0
@Suppress("MagicNumber")
private fun Double.toRad(): Double = this * Math.PI / 180.0
private fun bearingRad(from: GeoPoint, to: GeoPoint): Double {
val lat1 = from.latitude.toRad()
val lat2 = to.latitude.toRad()
val dLon = (to.longitude - from.longitude).toRad()
return atan2(sin(dLon) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon))
}
private fun GeoPoint.offsetPoint(headingRad: Double, offsetMeters: Double): GeoPoint {
val distanceByRadius = offsetMeters / EARTH_RADIUS_METERS
val lat1 = latitude.toRad()
val lon1 = longitude.toRad()
val lat2 = asin(sin(lat1) * cos(distanceByRadius) + cos(lat1) * sin(distanceByRadius) * cos(headingRad))
val lon2 =
lon1 + atan2(sin(headingRad) * sin(distanceByRadius) * cos(lat1), cos(distanceByRadius) - sin(lat1) * sin(lat2))
return GeoPoint(Math.toDegrees(lat2), Math.toDegrees(lon2))
}
private fun offsetPolyline(
points: List<GeoPoint>,
offsetMeters: Double,
headingReferencePoints: List<GeoPoint> = points,
sideMultiplier: Double = 1.0,
): List<GeoPoint> {
val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points
if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points
val headings =
headingPoints.mapIndexed { index, _ ->
when (index) {
0 -> bearingRad(headingPoints[0], headingPoints[1])
headingPoints.lastIndex ->
bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex])
else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1])
}
}
return points.mapIndexed { index, point ->
val heading = headings[index.coerceIn(0, headings.lastIndex)]
@Suppress("MagicNumber")
val perpendicularHeading = heading + (Math.PI / 2 * sideMultiplier)
point.offsetPoint(perpendicularHeading, abs(offsetMeters))
}
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map package org.meshtastic.app.map
import android.graphics.Color import android.graphics.Color
import android.graphics.DashPathEffect import android.graphics.DashPathEffect
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.meshtastic.app.R
import org.meshtastic.proto.Position import org.meshtastic.proto.Position
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
@ -123,17 +124,17 @@ fun MapView.addPolyline(density: Density, geoPoints: List<GeoPoint>, onClick: ()
return polyline return polyline
} }
fun MapView.addPositionMarkers(positions: List<Position>, onClick: () -> Unit): List<Marker> { fun MapView.addPositionMarkers(positions: List<Position>, onClick: (Int) -> Unit): List<Marker> {
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation) val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation)
val markers = val markers =
positions.map { positions.map { pos ->
Marker(this).apply { Marker(this).apply {
icon = navIcon icon = navIcon
rotation = ((it.ground_track ?: 0) * 1e-5).toFloat() rotation = ((pos.ground_track ?: 0) * 1e-5).toFloat()
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
position = GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) position = GeoPoint((pos.latitude_i ?: 0) * 1e-7, (pos.longitude_i ?: 0) * 1e-7)
setOnMarkerClickListener { _, _ -> setOnMarkerClickListener { _, _ ->
onClick() onClick(pos.time)
true true
} }
} }

View file

@ -14,47 +14,48 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map package org.meshtastic.app.map
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.model.RadioController
import org.meshtastic.core.data.repository.PacketRepository import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.model.DataPacket import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.BaseMapViewModel
import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalConfig
import javax.inject.Inject
@Suppress("LongParameterList") @Suppress("LongParameterList")
@HiltViewModel @KoinViewModel
class MapViewModel class MapViewModel(
@Inject
constructor(
mapPrefs: MapPrefs, mapPrefs: MapPrefs,
packetRepository: PacketRepository, packetRepository: PacketRepository,
private val nodeRepository: NodeRepository, nodeRepository: NodeRepository,
serviceRepository: ServiceRepository, radioController: RadioController,
radioConfigRepository: RadioConfigRepository, radioConfigRepository: RadioConfigRepository,
buildConfigProvider: BuildConfigProvider, buildConfigProvider: BuildConfigProvider,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) { ) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId) private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow() val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
fun setWaypointId(id: Int?) {
if (_selectedWaypointId.value != id) {
_selectedWaypointId.value = id
}
}
var mapStyleId: Int var mapStyleId: Int
get() = mapPrefs.mapStyle get() = mapPrefs.mapStyle.value
set(value) { set(value) {
mapPrefs.mapStyle = value mapPrefs.setMapStyle(value)
} }
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
@ -63,6 +64,4 @@ constructor(
get() = localConfig.value get() = localConfig.value
val applicationId = buildConfigProvider.applicationId val applicationId = buildConfigProvider.applicationId
override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,12 +14,8 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map
package org.meshtastic.feature.map
import android.annotation.SuppressLint
import android.content.Context
import android.os.PowerManager
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -33,7 +29,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import co.touchlab.kermit.Logger
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@ -42,29 +37,6 @@ import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
@SuppressLint("WakelockTimeout")
private fun PowerManager.WakeLock.safeAcquire() {
if (!isHeld) {
try {
acquire()
} catch (e: SecurityException) {
Logger.e { "WakeLock permission exception: ${e.message}" }
} catch (e: IllegalStateException) {
Logger.e { "WakeLock acquire() exception: ${e.message}" }
}
}
}
private fun PowerManager.WakeLock.safeRelease() {
if (isHeld) {
try {
release()
} catch (e: IllegalStateException) {
Logger.e { "WakeLock release() exception: ${e.message}" }
}
}
}
private const val MIN_ZOOM_LEVEL = 1.5 private const val MIN_ZOOM_LEVEL = 1.5
private const val MAX_ZOOM_LEVEL = 20.0 private const val MAX_ZOOM_LEVEL = 20.0
private const val DEFAULT_ZOOM_LEVEL = 15.0 private const val DEFAULT_ZOOM_LEVEL = 15.0
@ -137,22 +109,13 @@ internal fun rememberMapViewWithLifecycle(
} }
val lifecycle = LocalLifecycleOwner.current.lifecycle val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) { DisposableEffect(lifecycle) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
@Suppress("DEPRECATION")
val wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "Meshtastic:MapViewLock")
wakeLock.safeAcquire()
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
wakeLock.safeRelease()
mapView.onPause() mapView.onPause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
wakeLock.safeAcquire()
mapView.onResume() mapView.onResume()
} }
@ -167,10 +130,7 @@ internal fun rememberMapViewWithLifecycle(
lifecycle.addObserver(observer) lifecycle.addObserver(observer)
onDispose { onDispose { lifecycle.removeObserver(observer) }
lifecycle.removeObserver(observer)
wakeLock.safeRelease()
}
} }
return mapView return mapView
} }

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map package org.meshtastic.app.map
import android.database.Cursor import android.database.Cursor
import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowMillis

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.component package org.meshtastic.app.map.component
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View file

@ -14,15 +14,13 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.component package org.meshtastic.app.map.component
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -32,6 +30,8 @@ import androidx.compose.ui.draw.scale
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.map_download_region import org.meshtastic.core.resources.map_download_region
import org.meshtastic.core.ui.icon.Download
import org.meshtastic.core.ui.icon.MeshtasticIcons
@Composable @Composable
fun DownloadButton(enabled: Boolean, onClick: () -> Unit) { fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
@ -50,7 +50,7 @@ fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
) { ) {
FloatingActionButton(onClick = onClick, contentColor = MaterialTheme.colorScheme.primary) { FloatingActionButton(onClick = onClick, contentColor = MaterialTheme.colorScheme.primary) {
Icon( Icon(
imageVector = Icons.Rounded.Download, imageVector = MeshtasticIcons.Download,
contentDescription = stringResource(Res.string.map_download_region), contentDescription = stringResource(Res.string.map_download_region),
modifier = Modifier.scale(1.25f), modifier = Modifier.scale(1.25f),
) )

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.component package org.meshtastic.app.map.component
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.widget.DatePicker import android.widget.DatePicker
@ -34,9 +34,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CalendarMonth
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -60,16 +57,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.Month import kotlinx.datetime.Month
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowInstant
import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.common.util.systemTimeZone import org.meshtastic.core.common.util.systemTimeZone
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.cancel
import org.meshtastic.core.resources.date import org.meshtastic.core.resources.date
@ -84,9 +78,13 @@ import org.meshtastic.core.resources.waypoint_edit
import org.meshtastic.core.resources.waypoint_new import org.meshtastic.core.resources.waypoint_new
import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.emoji.EmojiPickerDialog import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.icon.CalendarMonth
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.Waypoint import org.meshtastic.proto.Waypoint
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Instant
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@ -102,7 +100,7 @@ fun EditWaypointDialog(
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
@Suppress("MagicNumber") @Suppress("MagicNumber")
val emoji = if ((waypointInput.icon ?: 0) == 0) 128205 else waypointInput.icon!! val emoji = if (waypointInput.icon == 0) 128205 else waypointInput.icon
var showEmojiPickerView by remember { mutableStateOf(false) } var showEmojiPickerView by remember { mutableStateOf(false) }
// Get current context for dialogs // Get current context for dialogs
@ -117,11 +115,11 @@ fun EditWaypointDialog(
val currentInstant = val currentInstant =
remember(waypointInput.expire) { remember(waypointInput.expire) {
val expire = waypointInput.expire ?: 0 val expire = waypointInput.expire
if (expire != 0 && expire != Int.MAX_VALUE) { if (expire != 0 && expire != Int.MAX_VALUE) {
Instant.fromEpochSeconds(expire.toLong()) kotlin.time.Instant.fromEpochSeconds(expire.toLong())
} else { } else {
nowInstant + 8.hours kotlin.time.Clock.System.now() + 8.hours
} }
} }
@ -129,8 +127,8 @@ fun EditWaypointDialog(
var selectedDate by var selectedDate by
remember(currentInstant) { remember(currentInstant) {
mutableStateOf( mutableStateOf(
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
dateFormat.format(currentInstant.toDate()) dateFormat.format(java.util.Date(currentInstant.toEpochMilliseconds()))
} else { } else {
"" ""
}, },
@ -139,8 +137,8 @@ fun EditWaypointDialog(
var selectedTime by var selectedTime by
remember(currentInstant) { remember(currentInstant) {
mutableStateOf( mutableStateOf(
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
timeFormat.format(currentInstant.toDate()) timeFormat.format(java.util.Date(currentInstant.toEpochMilliseconds()))
} else { } else {
"" ""
}, },
@ -164,7 +162,7 @@ fun EditWaypointDialog(
) )
EditTextPreference( EditTextPreference(
title = stringResource(Res.string.name), title = stringResource(Res.string.name),
value = waypointInput.name ?: "", value = waypointInput.name,
maxSize = 29, maxSize = 29,
enabled = true, enabled = true,
isError = false, isError = false,
@ -187,7 +185,7 @@ fun EditWaypointDialog(
) )
EditTextPreference( EditTextPreference(
title = stringResource(Res.string.description), title = stringResource(Res.string.description),
value = waypointInput.description ?: "", value = waypointInput.description,
maxSize = 99, maxSize = 99,
enabled = true, enabled = true,
isError = false, isError = false,
@ -200,11 +198,14 @@ fun EditWaypointDialog(
modifier = Modifier.fillMaxWidth().size(48.dp), modifier = Modifier.fillMaxWidth().size(48.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Image(imageVector = Icons.Rounded.Lock, contentDescription = stringResource(Res.string.locked)) Image(
imageVector = MeshtasticIcons.Lock,
contentDescription = stringResource(Res.string.locked),
)
Text(stringResource(Res.string.locked)) Text(stringResource(Res.string.locked))
Switch( Switch(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End), modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
checked = (waypointInput.locked_to ?: 0) != 0, checked = waypointInput.locked_to != 0,
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) }, onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
) )
} }
@ -227,7 +228,7 @@ fun EditWaypointDialog(
waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt()) waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
}, },
ldt.year, ldt.year,
ldt.monthNumber - 1, ldt.month.ordinal,
ldt.day, ldt.day,
) )
@ -257,13 +258,13 @@ fun EditWaypointDialog(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Image( Image(
imageVector = Icons.Rounded.CalendarMonth, imageVector = MeshtasticIcons.CalendarMonth,
contentDescription = stringResource(Res.string.expires), contentDescription = stringResource(Res.string.expires),
) )
Text(stringResource(Res.string.expires)) Text(stringResource(Res.string.expires))
Switch( Switch(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End), modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
checked = waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0, checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0,
onCheckedChange = { isChecked -> onCheckedChange = { isChecked ->
if (isChecked) { if (isChecked) {
waypointInput = waypointInput.copy(expire = currentInstant.epochSeconds.toInt()) waypointInput = waypointInput.copy(expire = currentInstant.epochSeconds.toInt())
@ -274,7 +275,7 @@ fun EditWaypointDialog(
) )
} }
if (waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0) { if (waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map.model
package org.meshtastic.feature.map.model
import org.osmdroid.tileprovider.tilesource.ITileSource import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,16 +14,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map.model
package org.meshtastic.feature.map.model
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.view.MotionEvent import android.view.MotionEvent
import org.meshtastic.feature.map.dpToPx import org.meshtastic.app.map.dpToPx
import org.meshtastic.feature.map.spToPx import org.meshtastic.app.map.spToPx
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon import org.osmdroid.views.overlay.Polygon

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.model package org.meshtastic.app.map.model
import android.content.res.Resources import android.content.res.Resources
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
@ -86,22 +86,6 @@ open class NOAAWmsTileSource(
if (time != null) this.time = time if (time != null) this.time = time
} }
// fun createFrom(endpoint: WMSEndpoint, layer: WMSLayer): WMSTileSource? {
// var srs: String? = "EPSG:900913"
// if (layer.srs.isNotEmpty()) {
// srs = layer.srs[0]
// }
// return if (layer.styles.isEmpty()) {
// WMSTileSource(
// layer.name, arrayOf(endpoint.baseurl), layer.name,
// endpoint.wmsVersion, srs, null, layer.pixelSize
// )
// } else WMSTileSource(
// layer.name, arrayOf(endpoint.baseurl), layer.name,
// endpoint.wmsVersion, srs, layer.styles[0], layer.pixelSize
// )
// }
private fun tile2lon(x: Int, z: Int): Double = x / 2.0.pow(z.toDouble()) * 360.0 - 180 private fun tile2lon(x: Int, z: Int): Double = x / 2.0.pow(z.toDouble()) * 360.0 - 180
private fun tile2lat(y: Int, z: Int): Double { private fun tile2lat(y: Int, z: Int): Double {

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map.model
package org.meshtastic.feature.map.model
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
import org.osmdroid.tileprovider.tilesource.TileSourcePolicy import org.osmdroid.tileprovider.tilesource.TileSourcePolicy

View file

@ -14,9 +14,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.map.node package org.meshtastic.app.map.node
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -24,13 +24,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.feature.map.MapView import org.meshtastic.feature.map.node.NodeMapViewModel
@Composable @Composable
fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) { fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) {
val node by nodeMapViewModel.node.collectAsStateWithLifecycle() val node by nodeMapViewModel.node.collectAsStateWithLifecycle()
val positions by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle() val positions by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle()
val destNum = node?.num
Scaffold( Scaffold(
topBar = { topBar = {
@ -45,8 +44,11 @@ fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit)
) )
}, },
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) { NodeTrackOsmMap(
MapView(focusedNodeNum = destNum, nodeTracks = positions, navigateToNodeDetails = {}) positions = positions,
} applicationId = nodeMapViewModel.applicationId,
mapStyleId = nodeMapViewModel.mapStyleId,
modifier = Modifier.fillMaxSize().padding(paddingValues),
)
} }
} }

View file

@ -0,0 +1,50 @@
/*
* 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.app.map.node
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.feature.map.node.NodeMapViewModel
import org.meshtastic.proto.Position
/**
* Flavor-unified entry point for the embeddable node-track map. Resolves [destNum] to obtain
* [NodeMapViewModel.applicationId] and [NodeMapViewModel.mapStyleId], then delegates to the OSMDroid implementation
* ([NodeTrackOsmMap]).
*
* Supports optional synchronized selection via [selectedPositionTime] and [onPositionSelected].
*/
@Composable
fun NodeTrackMap(
destNum: Int,
positions: List<Position>,
modifier: Modifier = Modifier,
selectedPositionTime: Int? = null,
onPositionSelected: ((Int) -> Unit)? = null,
) {
val vm = koinViewModel<NodeMapViewModel>()
vm.setDestNum(destNum)
NodeTrackOsmMap(
positions = positions,
applicationId = vm.applicationId,
mapStyleId = vm.mapStyleId,
modifier = modifier,
selectedPositionTime = selectedPositionTime,
onPositionSelected = onPositionSelected,
)
}

View file

@ -0,0 +1,162 @@
/*
* 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.app.map.node
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.MapViewModel
import org.meshtastic.app.map.addCopyright
import org.meshtastic.app.map.addPolyline
import org.meshtastic.app.map.addPositionMarkers
import org.meshtastic.app.map.addScaleBarOverlay
import org.meshtastic.app.map.model.CustomTileSource
import org.meshtastic.app.map.rememberMapViewWithLifecycle
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.util.GeoConstants.DEG_D
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.last_heard_filter_label
import org.meshtastic.feature.map.LastHeardFilter
import org.meshtastic.feature.map.component.MapControlsOverlay
import org.meshtastic.proto.Position
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import kotlin.math.roundToInt
/**
* A focused OSMDroid map composable that renders **only** a node's position track a dashed polyline with directional
* markers for each historical position.
*
* Applies the [lastHeardTrackFilter][org.meshtastic.feature.map.BaseMapViewModel.MapFilterState.lastHeardTrackFilter]
* from [MapViewModel] to filter positions by time, matching the behavior of the Google Maps implementation. Includes a
* minimal [MapControlsOverlay][org.meshtastic.feature.map.component.MapControlsOverlay] with a track time filter slider
* so users can adjust the time range directly from the map.
*
* Supports optional synchronized selection via [selectedPositionTime] and [onPositionSelected].
*
* Unlike the main [org.meshtastic.app.map.MapView], this composable does **not** include node clusters, waypoints, or
* location tracking. It is designed to be embedded inside the position-log adaptive layout.
*/
@Composable
fun NodeTrackOsmMap(
positions: List<Position>,
applicationId: String,
mapStyleId: Int,
modifier: Modifier = Modifier,
selectedPositionTime: Int? = null,
onPositionSelected: ((Int) -> Unit)? = null,
mapViewModel: MapViewModel = koinViewModel(),
) {
val density = LocalDensity.current
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter
val filteredPositions =
remember(positions, lastHeardTrackFilter) {
positions.filter {
lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds
}
}
val geoPoints =
remember(filteredPositions) {
filteredPositions.map { GeoPoint((it.latitude_i ?: 0) * DEG_D, (it.longitude_i ?: 0) * DEG_D) }
}
val cameraView = remember(geoPoints) { BoundingBox.fromGeoPoints(geoPoints) }
val mapView =
rememberMapViewWithLifecycle(
applicationId = applicationId,
box = cameraView,
tileSource = CustomTileSource.getTileSource(mapStyleId),
)
var filterMenuExpanded by remember { mutableStateOf(false) }
Box(modifier = modifier) {
AndroidView(
modifier = Modifier.matchParentSize(),
factory = { mapView },
update = { map ->
map.overlays.clear()
map.addCopyright()
map.addScaleBarOverlay(density)
map.addPolyline(density, geoPoints) {}
map.addPositionMarkers(filteredPositions) { time -> onPositionSelected?.invoke(time) }
// Center on selected position
if (selectedPositionTime != null) {
val selected = filteredPositions.find { it.time == selectedPositionTime }
if (selected != null) {
val point = GeoPoint((selected.latitude_i ?: 0) * DEG_D, (selected.longitude_i ?: 0) * DEG_D)
map.controller.animateTo(point)
}
}
},
)
// Track filter controls overlay
MapControlsOverlay(
modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
onToggleFilterMenu = { filterMenuExpanded = true },
filterDropdownContent = {
DropdownMenu(expanded = filterMenuExpanded, onDismissRequest = { filterMenuExpanded = false }) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val filterOptions = LastHeardFilter.entries
val selectedIndex = filterOptions.indexOf(lastHeardTrackFilter)
var sliderPosition by remember(selectedIndex) { mutableFloatStateOf(selectedIndex.toFloat()) }
Text(
text =
stringResource(
Res.string.last_heard_filter_label,
stringResource(lastHeardTrackFilter.label),
),
style = MaterialTheme.typography.labelLarge,
)
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
onValueChangeFinished = {
val newIndex = sliderPosition.roundToInt().coerceIn(0, filterOptions.size - 1)
mapViewModel.setLastHeardTrackFilter(filterOptions[newIndex])
},
valueRange = 0f..(filterOptions.size - 1).toFloat(),
steps = filterOptions.size - 2,
)
}
}
},
)
}
}

View file

@ -0,0 +1,41 @@
/*
* 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.app.map.traceroute
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.meshtastic.core.model.TracerouteOverlay
import org.meshtastic.proto.Position
/**
* Flavor-unified entry point for the embeddable traceroute map. Delegates to the OSMDroid implementation
* ([TracerouteOsmMap]).
*/
@Composable
fun TracerouteMap(
tracerouteOverlay: TracerouteOverlay?,
tracerouteNodePositions: Map<Int, Position>,
onMappableCountChanged: (shown: Int, total: Int) -> Unit,
modifier: Modifier = Modifier,
) {
TracerouteOsmMap(
tracerouteOverlay = tracerouteOverlay,
tracerouteNodePositions = tracerouteNodePositions,
onMappableCountChanged = onMappableCountChanged,
modifier = modifier,
)
}

View file

@ -0,0 +1,288 @@
/*
* 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/>.
*/
@file:Suppress("MagicNumber")
package org.meshtastic.app.map.traceroute
import android.graphics.Paint
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.R
import org.meshtastic.app.map.MapViewModel
import org.meshtastic.app.map.addCopyright
import org.meshtastic.app.map.addScaleBarOverlay
import org.meshtastic.app.map.model.CustomTileSource
import org.meshtastic.app.map.model.MarkerWithLabel
import org.meshtastic.app.map.rememberMapViewWithLifecycle
import org.meshtastic.app.map.zoomIn
import org.meshtastic.core.model.TracerouteOverlay
import org.meshtastic.core.model.util.GeoConstants.EARTH_RADIUS_METERS
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.feature.map.tracerouteNodeSelection
import org.meshtastic.proto.Position
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
private const val TRACEROUTE_OFFSET_METERS = 100.0
private const val TRACEROUTE_SINGLE_POINT_ZOOM = 12.0
private const val TRACEROUTE_ZOOM_OUT_LEVELS = 0.5
/**
* A focused OSMDroid map composable that renders **only** traceroute visualization node markers for each hop and
* forward/return offset polylines with auto-centering camera.
*
* Unlike the main `MapView`, this composable does **not** include node clusters, waypoints, location tracking, or any
* map controls. It is designed to be embedded inside `TracerouteMapScreen`'s scaffold.
*/
@Composable
fun TracerouteOsmMap(
tracerouteOverlay: TracerouteOverlay?,
tracerouteNodePositions: Map<Int, Position>,
onMappableCountChanged: (shown: Int, total: Int) -> Unit,
modifier: Modifier = Modifier,
mapViewModel: MapViewModel = koinViewModel(),
) {
val context = LocalContext.current
val density = LocalDensity.current
val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) }
// Resolve which nodes to display for the traceroute
val tracerouteSelection =
remember(tracerouteOverlay, tracerouteNodePositions, nodes) {
mapViewModel.tracerouteNodeSelection(
tracerouteOverlay = tracerouteOverlay,
tracerouteNodePositions = tracerouteNodePositions,
nodes = nodes,
)
}
val displayNodes = tracerouteSelection.nodesForMarkers
val nodeLookup = tracerouteSelection.nodeLookup
// Report mappable count
LaunchedEffect(tracerouteOverlay, displayNodes) {
if (tracerouteOverlay != null) {
onMappableCountChanged(displayNodes.size, tracerouteOverlay.relatedNodeNums.size)
}
}
// Compute polyline GeoPoints from node positions
val forwardPoints =
remember(tracerouteOverlay, nodeLookup) {
tracerouteOverlay?.forwardRoute?.mapNotNull {
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
} ?: emptyList()
}
val returnPoints =
remember(tracerouteOverlay, nodeLookup) {
tracerouteOverlay?.returnRoute?.mapNotNull {
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
} ?: emptyList()
}
// Compute offset polylines for visual separation
val headingReferencePoints =
remember(forwardPoints, returnPoints) {
when {
forwardPoints.size >= 2 -> forwardPoints
returnPoints.size >= 2 -> returnPoints
else -> emptyList()
}
}
val forwardOffsetPoints =
remember(forwardPoints, headingReferencePoints) {
offsetPolyline(
points = forwardPoints,
offsetMeters = TRACEROUTE_OFFSET_METERS,
headingReferencePoints = headingReferencePoints,
sideMultiplier = 1.0,
)
}
val returnOffsetPoints =
remember(returnPoints, headingReferencePoints) {
offsetPolyline(
points = returnPoints,
offsetMeters = TRACEROUTE_OFFSET_METERS,
headingReferencePoints = headingReferencePoints,
sideMultiplier = -1.0,
)
}
// Camera auto-center
var hasCentered by remember(tracerouteOverlay) { mutableStateOf(false) }
// Build initial camera from all traceroute points
val allPoints = remember(forwardPoints, returnPoints) { (forwardPoints + returnPoints).distinct() }
val initialCameraView =
remember(allPoints) { if (allPoints.isEmpty()) null else BoundingBox.fromGeoPoints(allPoints) }
val mapView =
rememberMapViewWithLifecycle(
applicationId = mapViewModel.applicationId,
box = initialCameraView ?: BoundingBox(),
tileSource = CustomTileSource.getTileSource(mapViewModel.mapStyleId),
)
// Center camera on traceroute bounds
LaunchedEffect(tracerouteOverlay, forwardPoints, returnPoints) {
if (tracerouteOverlay == null || hasCentered) return@LaunchedEffect
if (allPoints.isNotEmpty()) {
if (allPoints.size == 1) {
mapView.controller.setCenter(allPoints.first())
mapView.controller.setZoom(TRACEROUTE_SINGLE_POINT_ZOOM)
} else {
mapView.zoomToBoundingBox(
BoundingBox.fromGeoPoints(allPoints).zoomIn(-TRACEROUTE_ZOOM_OUT_LEVELS),
true,
)
}
hasCentered = true
}
}
AndroidView(
modifier = modifier,
factory = { mapView.apply { setDestroyMode(false) } },
update = { map ->
map.overlays.clear()
map.addCopyright()
map.addScaleBarOverlay(density)
// Render traceroute polylines
buildTraceroutePolylines(forwardOffsetPoints, returnOffsetPoints, density).forEach { map.overlays.add(it) }
// Render simple node markers
displayNodes.forEach { node ->
val position = GeoPoint(node.latitude, node.longitude)
val marker =
MarkerWithLabel(mapView = map, label = "${node.user.short_name} ${formatAgo(node.position.time)}")
.apply {
id = node.user.id
title = node.user.long_name
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
this.position = position
icon = markerIcon
setNodeColors(node.colors)
}
map.overlays.add(marker)
}
map.invalidate()
},
)
}
private fun buildTraceroutePolylines(
forwardPoints: List<GeoPoint>,
returnPoints: List<GeoPoint>,
density: androidx.compose.ui.unit.Density,
): List<Polyline> {
val polylines = mutableListOf<Polyline>()
fun buildPolyline(points: List<GeoPoint>, color: Int, strokeWidth: Float): Polyline = Polyline().apply {
setPoints(points)
outlinePaint.apply {
this.color = color
this.strokeWidth = strokeWidth
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
style = Paint.Style.STROKE
}
}
forwardPoints
.takeIf { it.size >= 2 }
?.let { points ->
polylines.add(buildPolyline(points, TracerouteColors.OutgoingRoute.toArgb(), with(density) { 6.dp.toPx() }))
}
returnPoints
.takeIf { it.size >= 2 }
?.let { points ->
polylines.add(buildPolyline(points, TracerouteColors.ReturnRoute.toArgb(), with(density) { 5.dp.toPx() }))
}
return polylines
}
// --- Haversine offset math for OSMDroid (no SphericalUtil available) ---
private fun Double.toRad(): Double = this * PI / 180.0
private fun bearingRad(from: GeoPoint, to: GeoPoint): Double {
val lat1 = from.latitude.toRad()
val lat2 = to.latitude.toRad()
val dLon = (to.longitude - from.longitude).toRad()
return atan2(sin(dLon) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon))
}
private fun GeoPoint.offsetPoint(headingRad: Double, offsetMeters: Double): GeoPoint {
val distanceByRadius = offsetMeters / EARTH_RADIUS_METERS
val lat1 = latitude.toRad()
val lon1 = longitude.toRad()
val lat2 = asin(sin(lat1) * cos(distanceByRadius) + cos(lat1) * sin(distanceByRadius) * cos(headingRad))
val lon2 =
lon1 + atan2(sin(headingRad) * sin(distanceByRadius) * cos(lat1), cos(distanceByRadius) - sin(lat1) * sin(lat2))
return GeoPoint(lat2 * 180.0 / PI, lon2 * 180.0 / PI)
}
private fun offsetPolyline(
points: List<GeoPoint>,
offsetMeters: Double,
headingReferencePoints: List<GeoPoint> = points,
sideMultiplier: Double = 1.0,
): List<GeoPoint> {
val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points
if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points
val headings =
headingPoints.mapIndexed { index, _ ->
when (index) {
0 -> bearingRad(headingPoints[0], headingPoints[1])
headingPoints.lastIndex ->
bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex])
else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1])
}
}
return points.mapIndexed { index, point ->
val heading = headings[index.coerceIn(0, headings.lastIndex)]
val perpendicularHeading = heading + (PI / 2 * sideMultiplier)
point.offsetPoint(perpendicularHeading, abs(offsetMeters))
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025-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.app.node.component
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import org.meshtastic.core.model.Node
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
@Composable
fun InlineMap(node: Node, modifier: Modifier = Modifier) {
val context = androidx.compose.ui.platform.LocalContext.current
val map = remember {
MapView(context).apply {
layoutParams =
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
// Default osmdroid tile source.
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(false)
controller.setZoom(15.0)
}
}
LaunchedEffect(node.num) {
val point = GeoPoint(node.latitude, node.longitude)
map.overlays.clear()
val marker =
Marker(map).apply {
position = point
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
}
map.overlays.add(marker)
map.controller.animateTo(point)
}
AndroidView(factory = { map }, modifier = modifier)
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,15 +14,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.node.metrics
package org.meshtastic.feature.node.metrics
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
internal object TracerouteMapOverlayInsets { fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
val overlayAlignment: Alignment = Alignment.BottomEnd overlayAlignment = Alignment.BottomEnd,
val overlayPadding: PaddingValues = PaddingValues(end = 16.dp, bottom = 16.dp) overlayPadding = PaddingValues(end = 16.dp, bottom = 16.dp),
val contentHorizontalAlignment: Alignment.Horizontal = Alignment.End contentHorizontalAlignment = Alignment.End,
} )

View file

@ -14,22 +14,18 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.core.analytics.platform package org.meshtastic.app.analytics
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.compose.runtime.Composable
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.LogWriter import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity import co.touchlab.kermit.Severity
import com.datadog.android.Datadog import com.datadog.android.Datadog
import com.datadog.android.DatadogSite import com.datadog.android.DatadogSite
import com.datadog.android.compose.ExperimentalTrackingApi
import com.datadog.android.compose.NavigationViewTrackingEffect
import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.configuration.Configuration
import com.datadog.android.log.Logger import com.datadog.android.log.Logger
import com.datadog.android.log.Logs import com.datadog.android.log.Logs
@ -37,8 +33,11 @@ import com.datadog.android.log.LogsConfiguration
import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.GlobalRumMonitor
import com.datadog.android.rum.Rum import com.datadog.android.rum.Rum
import com.datadog.android.rum.RumActionType
import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumConfiguration
import com.datadog.android.rum.tracking.AcceptAllNavDestinations import com.datadog.android.sessionreplay.SessionReplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.trace.Trace import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration import com.datadog.android.trace.TraceConfiguration
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
@ -51,15 +50,15 @@ import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize import com.google.firebase.initialize
import dagger.hilt.android.qualifiers.ApplicationContext
import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.GlobalOpenTelemetry
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.BuildConfig import org.koin.core.annotation.Single
import org.meshtastic.core.analytics.DataPair import org.meshtastic.app.BuildConfig
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs import org.meshtastic.core.repository.AnalyticsPrefs
import javax.inject.Inject import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import co.touchlab.kermit.Logger as KermitLogger import co.touchlab.kermit.Logger as KermitLogger
/** /**
@ -69,14 +68,11 @@ import co.touchlab.kermit.Logger as KermitLogger
* This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and * This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and
* respect privacy-focused environments. * respect privacy-focused environments.
*/ */
class GooglePlatformAnalytics @Single
@Inject class GooglePlatformAnalytics(private val context: Context, private val analyticsPrefs: AnalyticsPrefs) :
constructor( PlatformAnalytics {
@ApplicationContext private val context: Context,
private val analyticsPrefs: AnalyticsPrefs,
) : PlatformAnalytics {
private val sampleRate = 100f.takeIf { BuildConfig.DEBUG } ?: 10f // For Datadog remote sample rate private val sampleRate = 100f // Match Apple: 100% sampling for cross-platform DataDog comparison
private var datadogLogger: Logger? = null private var datadogLogger: Logger? = null
private var isFirebaseInitialized = false private var isFirebaseInitialized = false
@ -109,11 +105,10 @@ constructor(
KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info) KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
// Initial consent state // Initial consent state
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed) updateAnalyticsConsent(analyticsPrefs.analyticsAllowed.value)
// Subscribe to analytics preference changes // Subscribe to analytics preference changes
analyticsPrefs analyticsPrefs.analyticsAllowed
.getAnalyticsAllowedChangesFlow()
.onEach { allowed -> updateAnalyticsConsent(allowed) } .onEach { allowed -> updateAnalyticsConsent(allowed) }
.launchIn(ProcessLifecycleOwner.get().lifecycleScope) .launchIn(ProcessLifecycleOwner.get().lifecycleScope)
} }
@ -122,7 +117,7 @@ constructor(
* Ensures that Datadog and Firebase SDKs are initialized if allowed. This is called lazily when consent is granted. * Ensures that Datadog and Firebase SDKs are initialized if allowed. This is called lazily when consent is granted.
*/ */
private fun ensureInitialized() { private fun ensureInitialized() {
if (!analyticsPrefs.analyticsAllowed || isInTestLab) return if (!analyticsPrefs.analyticsAllowed.value || isInTestLab) return
if (!Datadog.isInitialized()) { if (!Datadog.isInitialized()) {
initDatadog(context as Application) initDatadog(context as Application)
@ -146,7 +141,7 @@ constructor(
val configuration = val configuration =
Configuration.Builder( Configuration.Builder(
clientToken = BuildConfig.datadogClientToken, clientToken = BuildConfig.datadogClientToken,
env = if (BuildConfig.DEBUG) "debug" else "release", env = if (BuildConfig.DEBUG) "Local" else "Production",
variant = BuildConfig.FLAVOR, variant = BuildConfig.FLAVOR,
) )
.useSite(DatadogSite.US5) .useSite(DatadogSite.US5)
@ -160,7 +155,7 @@ constructor(
val rumConfiguration = val rumConfiguration =
RumConfiguration.Builder(BuildConfig.datadogApplicationId) RumConfiguration.Builder(BuildConfig.datadogApplicationId)
.trackAnonymousUser(true) .trackAnonymousUser(true)
.trackBackgroundEvents(false) // Disable background noise .trackBackgroundEvents(true) // Match Apple: track background events for cross-platform parity
.trackFrustrations(false) // Disable click-tracking based frustration detection .trackFrustrations(false) // Disable click-tracking based frustration detection
.trackLongTasks() .trackLongTasks()
.trackNonFatalAnrs(true) .trackNonFatalAnrs(true)
@ -171,12 +166,20 @@ constructor(
val logsConfig = LogsConfiguration.Builder().build() val logsConfig = LogsConfiguration.Builder().build()
Logs.enable(logsConfig) Logs.enable(logsConfig)
val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(false).build() val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(true).build()
Trace.enable(traceConfig) Trace.enable(traceConfig)
GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME)) // Session Replay for debug builds only, matching Apple's TestFlight-only gating.
// Masks all text inputs to protect message content.
if (BuildConfig.DEBUG) {
val sessionReplayConfig =
SessionReplayConfiguration.Builder(sampleRate)
.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_ALL_INPUTS)
.build()
SessionReplay.enable(sessionReplayConfig)
}
// Session Replay disabled to reduce PII collection GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME))
} }
private fun initCrashlytics(application: Application) { private fun initCrashlytics(application: Application) {
@ -244,16 +247,22 @@ constructor(
GlobalRumMonitor.get().addAttribute("device_hardware", model) GlobalRumMonitor.get().addAttribute("device_hardware", model)
} }
@OptIn(ExperimentalTrackingApi::class) override fun trackConnect(
@Composable firmwareVersion: String?,
override fun AddNavigationTrackingEffect(navController: NavHostController) { transportType: String?,
if (Datadog.isInitialized()) { hardwareModel: String?,
NavigationViewTrackingEffect( nodes: Int,
navController = navController, connectionRestored: Boolean,
trackArguments = true, ) {
destinationPredicate = AcceptAllNavDestinations(), if (!Datadog.isInitialized() || !GlobalRumMonitor.isRegistered()) return
) val attributes = buildMap {
firmwareVersion?.let { put("firmwareVersion", it) }
transportType?.let { put("transportType", it) }
hardwareModel?.let { put("hardwareModel", it) }
put("nodes", nodes)
if (connectionRestored) put("connectionRestored", true)
} }
GlobalRumMonitor.get().addAction(RumActionType.CUSTOM, "connect", attributes)
} }
private val isGooglePlayAvailable: Boolean private val isGooglePlayAvailable: Boolean
@ -309,7 +318,7 @@ constructor(
} }
private fun String.extractSemanticVersion(): String { private fun String.extractSemanticVersion(): String {
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?".toRegex() val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?$".toRegex()
val matchResult = regex.find(this) val matchResult = regex.find(this)
return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".") ?: this return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".") ?: this
} }
@ -318,16 +327,16 @@ constructor(
if (!isFirebaseInitialized) return if (!isFirebaseInitialized) return
val bundle = Bundle() val bundle = Bundle()
properties.forEach { properties.forEach {
when (it.value) { val value = it.value
is Double -> bundle.putDouble(it.name, it.value) when (value) {
is Int -> is Double -> bundle.putDouble(it.name, value)
bundle.putLong(it.name, it.value.toLong()) // Firebase expects Long for integer values in bundles is Int -> bundle.putLong(it.name, value.toLong()) // Firebase expects Long for integer values in bundles
is Long -> bundle.putLong(it.name, it.value) is Long -> bundle.putLong(it.name, value)
is Float -> bundle.putDouble(it.name, it.value.toDouble()) is Float -> bundle.putDouble(it.name, value.toDouble())
is String -> bundle.putString(it.name, it.value as String?) // Explicitly handle String is String -> bundle.putString(it.name, value) // Explicitly handle String
else -> bundle.putString(it.name, it.value.toString()) // Fallback for other types else -> bundle.putString(it.name, value.toString()) // Fallback for other types
} }
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : ${it.value})" } KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : $value)" }
} }
Firebase.analytics.logEvent(event, bundle) Firebase.analytics.logEvent(event, bundle)
} }

View file

@ -0,0 +1,23 @@
/*
* 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.app.di
import org.koin.core.annotation.Module
import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class])
class FlavorModule

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,18 +14,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.di
package com.geeksville.mesh.repository.radio import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import javax.inject.Inject @Module
class GoogleNetworkModule {
/** @Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl
* No-op interface backend implementation.
*/
class NopInterfaceSpec @Inject constructor(
private val factory: NopInterfaceFactory
) : InterfaceSpec<NopInterface> {
override fun createInterface(rest: String): NopInterface {
return factory.create(rest)
}
} }

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.intro package org.meshtastic.app.intro
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column

View file

@ -0,0 +1,21 @@
/*
* 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.app.map
import org.meshtastic.core.ui.util.MapViewProvider
fun getMapViewProvider(): MapViewProvider = GoogleMapViewProvider()

View file

@ -0,0 +1,39 @@
/*
* 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.app.map
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
/** Google Maps implementation of [MapViewProvider]. */
@Single
class GoogleMapViewProvider : MapViewProvider {
@Composable
override fun MapView(modifier: Modifier, navigateToNodeDetails: (Int) -> Unit, waypointId: Int?) {
val mapViewModel: MapViewModel = koinViewModel()
LaunchedEffect(waypointId) { mapViewModel.setWaypointId(waypointId) }
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,
navigateToNodeDetails = navigateToNodeDetails,
)
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025 Meshtastic LLC * Copyright (c) 2025-2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.app.map
package org.meshtastic.feature.map
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity

Some files were not shown because too many files have changed in this diff Show more