Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
69883e9
refactor: remove AIDL API and modernize radio architecture
jamesarich May 23, 2026
50cf226
Change Ktor log level to INFO and add 'HttpClient' tag to KermitHttpL…
jamesarich May 23, 2026
d62c459
Add test coverage for refactored modules
jamesarich May 23, 2026
058a522
build: Remove vestigial kotlin-android plugin references for AGP 9
jamesarich May 26, 2026
f4fab69
build: Remove publishing infrastructure and unify JDK to 21
jamesarich May 26, 2026
7e3bd85
cleanup: Remove legacy cruft and eliminate runBlocking from UI path
jamesarich May 26, 2026
de66551
fix: Resolve lint errors and warnings
jamesarich May 26, 2026
4adc6a6
build: Remove redundant OptimizeNonSkippingGroups flag
jamesarich May 26, 2026
913c1a9
fix: Address P1 code-review findings from AIDL-removal refactor
jamesarich May 28, 2026
aec9325
fix: Address P2 code-review findings — concurrency & API contracts
jamesarich May 28, 2026
e6b08ff
fix: Address P3 + P4 code-review findings — pre-existing bugs + perf …
jamesarich May 28, 2026
8ae937b
refactor: Move RadioController interfaces to core:repository and spli…
jamesarich May 28, 2026
19fe079
fix: Harden orchestrator start() race condition and strengthen test a…
jamesarich May 28, 2026
50cb557
cleanup: Remove dead MeshRouter dependency from orchestrator and fix …
jamesarich May 28, 2026
ad553f1
refactor: Remove MeshRouter service-locator facade
jamesarich May 28, 2026
b742d12
refactor: Convert DeviceVersion to @JvmInline value class
jamesarich May 28, 2026
e6a8336
test: Add NeighborInfoHandlerImpl unit tests
jamesarich May 28, 2026
8aa0017
fix: Restore forced manual verification on contact import
jamesarich May 29, 2026
4970e45
refactor: Consolidate contact-key parsing into ContactKey
jamesarich May 29, 2026
b97ff4d
refactor: Idempotent node ops (setFavorite/setIgnored) matching SDK
jamesarich May 29, 2026
2c00129
refactor: Replace begin/commitEditSettings with editSettings DSL
jamesarich May 29, 2026
6940b6a
refactor: Split DirectRadioControllerImpl into focused sub-controllers
jamesarich May 29, 2026
c303515
refactor: Extract shared RequestTimer from request handlers
jamesarich May 29, 2026
404adf1
docs: Add session handover entry for AIDL removal + RadioController work
jamesarich May 29, 2026
9f5b7d6
docs: Refresh module READMEs after AIDL removal / controller refactor
jamesarich May 29, 2026
975b6f4
docs: Document RadioController composition in architecture guide
jamesarich May 29, 2026
dd6ca40
refactor: Decompose ServiceRepository into focused provider interfaces
jamesarich May 29, 2026
1182ea0
docs: Document ServiceRepository ISP decomposition in architecture guide
jamesarich May 29, 2026
b7e36a9
refactor: Narrow read-only connection-state consumers to ConnectionSt…
jamesarich May 29, 2026
9dc1e50
refactor: Adopt ServiceStateWriter / ConnectionStateProvider in handlers
jamesarich May 29, 2026
94bcec8
refactor: Naming cleanup for clarity and Kotlin conventions
jamesarich May 29, 2026
51c5136
Merge remote-tracking branch 'origin/main' into jamesarich/remove-aid…
jamesarich May 30, 2026
a5f60bc
docs: log 2026-05-30 main-merge handover in session context
jamesarich May 30, 2026
b0ee2de
fix: prevent DB connection leaks with NonCancellable in PacketRepository
jamesarich May 31, 2026
e3a26df
style: fix formatting in PacketRepositoryImpl
jamesarich May 31, 2026
File filter

Filter by extension

Filter by extension


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

## 2026-05-30 — Merged main into remove-aidl-api branch; resolved conflicts; PR #5586 mergeable
- `git merge origin/main` (branch was 40 behind / 31 ahead). 12 files flagged; git auto-resolved 11. Only real conflict: this append-only `session_context.md` (both sides added top entries — kept both in date order).
- AIDL removal preserved through the merge (main still ships `IMeshService.aidl`/`FakeIMeshService` etc.; our deletions held). Manifest `MeshService` now `exported=false` with no AIDL bind filter; main's Android Auto removal (#5662) carried through.
- proto submodule advanced to v2.7.24 (dd6c3f8) per main #5654; synced working tree. main dep bumps merged: takpacket-sdk 0.5.1, firebase-bom 34.14.0, atomicfu 0.33.0.
- Validated: `assembleDebug`, `spotlessCheck`, `detekt`, `:core:data:jvmTest`, `:core:takserver:allTests` all green (the two conflicted feature modules). Pushed; PR #5586 → MERGEABLE, CI re-running.
- Aside (separate repo): meshtastic-sdk PR #3 was superseded by main's PR #1; extracted only the toolchain bump into SDK PR #4 (Kotlin 2.3.21 / SKIE 0.10.12 / Wire 6.4.0 / Ktor 3.5.0 / coroutines stable).

## 2026-05-29 — Removed AIDL/broadcast service layer; modernized RadioController; deferred R4/R5
- AIDL bound-service + broadcast (`core:api`, `IMeshService`, `ServiceBroadcasts`, `ServiceAction`, `MeshActionHandler`, `MeshRouter`) removed; replaced with direct suspend-based `RadioController`.
- `RadioController` split into `AdminController`/`MessagingController`/`NodeController`/`RequestController`, composed in `DirectRadioControllerImpl` via Kotlin `by` delegation to four focused impls (`AdminControllerImpl` etc., core/service commonMain).
- Typed addressing: `NodeAddress` (sealed) + `ContactKey` (value class) in core/model; 6 hand-rolled contact-key parsers consolidated onto `ContactKey.channelOrNull`/`addressString`.
- Idempotent node ops: `setFavorite`/`setIgnored(Boolean)` + `toggleMuted` (fixed a latent toggle bug in `SendMessageUseCase`); `editSettings { }` DSL (`AdminEditScope`) replaced begin/commitEditSettings.
- Shared `RequestTimer` extracted from Traceroute/NeighborInfo handlers. `formatAgo` runBlocking removed. Contact import re-marks `manually_verified=true` (review-fix regression).
- Admin sends are intentionally fire-and-forget (device = source of truth).
- R4 (`AdminResult<T>`) and R5 (`NodeId` value class) investigated and DEFERRED to the SDK migration — do not build standalone. Rationale in `.agent_plans/post-aidl-modernization.md`.

## 2026-05-28 — Stabilized DatabaseManager withDb retry host test
- Hardened `DatabaseManagerWithDbRetryTest` to remove CI race conditions by running the manager on a `StandardTestDispatcher(testScheduler)` instead of real `Dispatchers.IO`.
- Added a `withTimeout(10_000)` guard around the test body to fail fast on coordination stalls instead of hanging/flapping.
Expand Down
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ git submodule update --init

KMP modules have different task names than pure-Android modules. Using the wrong name silently skips tests or fails resolution.

| Intent | KMP modules (`core:*`, `feature:*`) | Android-only (`app`, `core:api`, `core:barcode`) |
| Intent | KMP modules (`core:*`, `feature:*`) | Android-only (`app`, `core:barcode`) |
|--------|--------------------------------------|--------------------------------------------------|
| Run tests | `:module:allTests` | `:module:testFdroidDebugUnitTest` |
| Detekt | `:module:detekt` (lifecycle task) | `:module:detekt` |
Expand Down
51 changes: 0 additions & 51 deletions .github/workflows/publish-core.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/reusable-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:

- name: Lint, Analysis & KMP Smoke Compile
if: inputs.run_lint == true
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug kmpSmokeCompile -Pci=true --continue

- name: KMP Smoke Compile (lint skipped)
if: inputs.run_lint == false
Expand Down
3 changes: 1 addition & 2 deletions .skills/project-overview/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin

- **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`.
- **Android-only Modules:** `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`.

## Codebase Map

Expand All @@ -28,7 +28,6 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
| `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`. |
Expand Down
2 changes: 1 addition & 1 deletion .skills/testing-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Run in a single invocation for routine changes to ensure code formatting, analys
> 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 (`:androidApp`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `:core:barcode`, 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.*

Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ Each module has its own README with details on its responsibilities, API surface

| Module | Description |
|---|---|
| [core/api](core/api/README.md) | AIDL service API for third-party integrations |
| [core/domain](core/domain/README.md) | Business-logic use cases (radio config, sessions, exports) |
| [core/repository](core/repository/README.md) | Data & infrastructure contracts (RadioTransport, NodeRepository, ServiceRepository) |
| [core/takserver](core/takserver/README.md) | Meshtastic ↔ TAK (ATAK/iTAK) bridge — CoT server & conversion |
Expand Down Expand Up @@ -123,13 +122,9 @@ Each module has its own README with details on its responsibilities, API surface

You can help translate the app into your native language using [Crowdin](https://crowdin.meshtastic.org/android).

## API & Integration
## Integration

Developers can integrate with the Meshtastic Android app using our published API library via **JitPack**. This allows third-party applications (like the ATAK plugin) to communicate with the mesh service via AIDL.

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.
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
> [!WARNING]
Expand Down
4 changes: 2 additions & 2 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ configure<ApplicationExtension> {
),
)
}
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") }
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a") }

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -124,7 +124,7 @@ configure<ApplicationExtension> {
abi {
isEnable = !disableSplits
reset()
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ protected void hideInfoWindows(){
int zoomLevel = mapView.getZoomLevel();
if (zoomLevel != mLastZoomLevel && !mapView.isAnimating()){
hideInfoWindows();
mClusters = clusterer(mapView);
renderer(mClusters, canvas, mapView);
mClusters = clusterer(mapView);
renderer(mClusters, canvas, mapView);
mLastZoomLevel = zoomLevel;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;

import androidx.core.content.res.ResourcesCompat;

import org.meshtastic.app.map.model.MarkerWithLabel;

import org.osmdroid.bonuspack.R;
Expand Down Expand Up @@ -72,7 +74,7 @@ public RadiusMarkerClusterer(Context ctx) {
mTextPaint.setFakeBoldText(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setAntiAlias(true);
Drawable clusterIconD = ctx.getResources().getDrawable(R.drawable.marker_cluster);
Drawable clusterIconD = ResourcesCompat.getDrawable(ctx.getResources(), R.drawable.marker_cluster, ctx.getTheme());
Bitmap clusterIcon = ((BitmapDrawable) clusterIconD).getBitmap();
setIcon(clusterIcon);
mAnimated = true;
Expand Down
76 changes: 42 additions & 34 deletions androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -87,6 +88,7 @@ import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.calculating
import org.meshtastic.core.resources.cancel
Expand All @@ -113,9 +115,11 @@ import org.meshtastic.core.resources.map_purge_success
import org.meshtastic.core.resources.map_style_selection
import org.meshtastic.core.resources.map_subDescription
import org.meshtastic.core.resources.map_tile_source
import org.meshtastic.core.resources.now
import org.meshtastic.core.resources.only_favorites
import org.meshtastic.core.resources.show_precision_circle
import org.meshtastic.core.resources.show_waypoints
import org.meshtastic.core.resources.unknown
import org.meshtastic.core.resources.waypoint_delete
import org.meshtastic.core.resources.you
import org.meshtastic.core.ui.component.BasicListItem
Expand Down Expand Up @@ -239,6 +243,9 @@ fun MapView(
val haptic = LocalHapticFeedback.current
fun performHapticFeedback() = haptic.performHapticFeedback(HapticFeedbackType.LongPress)

val unknownText = stringResource(Res.string.unknown)
val nowText = stringResource(Res.string.now)

// Accompanist permissions state for location
val locationPermissionsState =
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
Expand Down Expand Up @@ -355,36 +362,37 @@ fun MapView(

val (p, u) = node.position to node.user
val nodePosition = GeoPoint(node.latitude, node.longitude)
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time)}").apply {
id = u.id
title = u.long_name
snippet =
getString(
Res.string.map_node_popup_details,
node.gpsString(),
formatAgo(node.lastHeard),
formatAgo(p.time),
if (node.batteryStr != "") node.batteryStr else "?",
)
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
ourNode.bearing(node)?.let { bearing ->
subDescription = getString(Res.string.map_subDescription, bearing, dist)
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time, unknownText, nowText)}")
.apply {
id = u.id
title = u.long_name
snippet =
getString(
Res.string.map_node_popup_details,
node.gpsString(),
formatAgo(node.lastHeard, unknownText, nowText),
formatAgo(p.time, unknownText, nowText),
if (node.batteryStr != "") node.batteryStr else "?",
)
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
ourNode.bearing(node)?.let { bearing ->
subDescription = getString(Res.string.map_subDescription, bearing, dist)
}
}
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
position = nodePosition
icon = markerIcon
setNodeColors(node.colors)
if (!mapFilterStateValue.showPrecisionCircle) {
setPrecisionBits(0)
} else {
setPrecisionBits(p.precision_bits)
}
setOnLongClickListener {
navigateToNodeDetails(node.num)
true
}
}
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
position = nodePosition
icon = markerIcon
setNodeColors(node.colors)
if (!mapFilterStateValue.showPrecisionCircle) {
setPrecisionBits(0)
} else {
setPrecisionBits(p.precision_bits)
}
setOnLongClickListener {
navigateToNodeDetails(node.num)
true
}
}
}
}

Expand Down Expand Up @@ -433,7 +441,7 @@ fun MapView(
}
}

fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL || (myId != null && id == myId)) {
fun getUsername(id: String?) = if (id == NodeAddress.ID_LOCAL || (myId != null && id == myId)) {
getString(Res.string.you)
} else {
mapViewModel.getUser(id).long_name
Expand All @@ -446,7 +454,7 @@ fun MapView(
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
val lock = if (pt.locked_to != 0) "\uD83D\uDD12" else ""
val time = DateFormatter.formatDateTime(waypoint.time)
val label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt())
val label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt(), unknownText, nowText)
val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
val now = nowMillis
val expireTimeMillis = pt.expire * 1000L
Expand Down Expand Up @@ -818,15 +826,15 @@ private fun FdroidMainMapFilterDropdown(

@Composable
private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) {
val selected = remember { mutableStateOf(selectedMapStyle) }
val selected = remember { mutableIntStateOf(selectedMapStyle) }

MapsDialog(onDismiss = onDismiss) {
CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
ListItem(
text = style,
trailingIcon = if (index == selected.value) MeshtasticIcons.Check else null,
trailingIcon = if (index == selected.intValue) MeshtasticIcons.Check else null,
onClick = {
selected.value = index
selected.intValue = index
onSelectMapStyle(index)
onDismiss()
},
Expand Down Expand Up @@ -879,7 +887,7 @@ private fun PurgeTileSourceDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
val cache = SqlTileWriterExt()

val sourceList by derivedStateOf { cache.sources.map { it.source as String } }
val sourceList by remember { derivedStateOf { cache.sources.map { it.source as String } } }

val selected = remember { mutableStateListOf<Int>() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.BaseMapViewModel
import org.meshtastic.proto.LocalConfig
Expand Down
Loading