Skip to content

feat: service decoupling#4685

Merged
jamesarich merged 20 commits intomainfrom
feat/decoupling
Mar 3, 2026
Merged

feat: service decoupling#4685
jamesarich merged 20 commits intomainfrom
feat/decoupling

Conversation

@jamesarich
Copy link
Collaborator

Overview

This PR implements a major architectural refactor to decouple the application layers, moving the project toward Modern Android Development (MAD) best practices and Kotlin Multiplatform (KMP) readiness. Business logic has been extracted from the :app module into specialized core modules, and platform-specific radio logic is now abstracted behind clean interfaces.

Key Changes

  • Layer Separation: Established a clear hierarchy between :core:model (KMP models), :core:repository (interfaces), :core:domain (business logic/use cases), and :core:data/:core:service (implementations).
  • Interface Abstraction: Introduced RadioController and ServiceRepository to decouple the UI from AIDL and background service lifecycle.
  • Use Case Pattern: Migrated complex workflows (e.g., sending messages, admin actions) into dedicated Use Cases for improved readability and testability.
  • Model Promotion: Promoted core domain models (Node, Message, Position) to the :core:model module.
  • Reactivity: Standardized mesh event handling using StateFlow and SharedFlow across all layers.

Regressions Fixed During Review

  • Node Persistence: Restored database synchronization in NodeManagerImpl (previously only updating in-memory).
  • Data Integrity: Fixed missing publicKey mapping in NodeWithRelations to ensure PKI features remain functional.
  • Packet ID Stability: Fixed a bug in SendMessageUseCase where packet IDs could be zero or negative.
  • Test Suite: Fixed and verified MeshDataHandlerTest.kt and SendMessageWorkerTest.kt to match the new constructor signatures.

- Move `Node`, `Message`, `TAK`, and `NodeSortOption` models from `core:database` to `core:model`.
- Create `:core:repository` module to host platform-agnostic repository interfaces and use cases.
- Refactor `NodeRepository` into a shared interface with an Android-specific `NodeRepositoryImpl`.
- Extract `PacketRepository` and `MessageQueue` interfaces for cross-platform support.
- Relocate `SendMessageUseCase` to the common repository layer.
- Update model dependencies across messaging, node, and map features.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…hitecture

- Introduce interfaces for `NodeManager`, `CommandSender`, `PacketRepository`, `RadioConfigRepository`, `DeviceHardwareRepository`, and `ServiceBroadcasts`.
- Move interfaces to the `core:repository` module to decouple logic from concrete implementations.
- Relocate concrete implementations to `core:data` and update Hilt modules to bind new implementations.
- Standardize the usage of core models (`Node`, `Message`, `Reaction`, `ContactSettings`) across feature modules and services.
- Refactor `MeshService` and its handlers to use the new interface types for better testability.
- Update unit tests to reflect class renaming and interface migrations.
- Move `ContactSettings` and `Reaction` to `core:model` for shared access.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Introduce `HistoryManager` interface in `core:repository` and its implementation in `core:data`.
- Move `NodeManager`, `CommandSender`, and `HistoryManager` unit tests from the `app` module to `core:data`.
- Update `MeshConnectionManager` and `MeshDataHandler` to use the new `HistoryManager` interface instead of the concrete implementation.
- Refactor unit tests and fakes to use domain models (`Node`, `DataPacket`) instead of database entities.
- Expose `getOrCreateNode` in `NodeManagerImpl` and `resolveNodeNum` in `CommandSenderImpl` for improved testability.
- Update DI module to bind the new `HistoryManager` implementation.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…re modules

- Introduce `RadioController` in `core:model` to abstract radio operations and settings.
- Extract `ServiceRepository`, `MqttManager`, `TracerouteHandler`, and `NeighborInfoHandler` interfaces into `core:repository`.
- Move MQTT repository to `core:network` and handler implementations to `core:data`.
- Update ViewModels to interact with `RadioController` instead of `IMeshService` directly.
- Relocate `ServiceAction` and `TracerouteResponse` to `core:model` for better module accessibility.
- Bind new implementations in `RepositoryModule` and `ServiceModule`.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Move `Node`, `Message`, `TAK`, and `NodeSortOption` models from `core:database` to `core:model`.
- Create `:core:repository` module to host platform-agnostic repository interfaces and use cases.
- Refactor `NodeRepository` into a shared interface with an Android-specific `NodeRepositoryImpl`.
- Extract `PacketRepository` and `MessageQueue` interfaces for cross-platform support.
- Relocate `SendMessageUseCase` to the common repository layer.
- Update model dependencies across messaging, node, and map features.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…hitecture

- Introduce interfaces for `NodeManager`, `CommandSender`, `PacketRepository`, `RadioConfigRepository`, `DeviceHardwareRepository`, and `ServiceBroadcasts`.
- Move interfaces to the `core:repository` module to decouple logic from concrete implementations.
- Relocate concrete implementations to `core:data` and update Hilt modules to bind new implementations.
- Standardize the usage of core models (`Node`, `Message`, `Reaction`, `ContactSettings`) across feature modules and services.
- Refactor `MeshService` and its handlers to use the new interface types for better testability.
- Update unit tests to reflect class renaming and interface migrations.
- Move `ContactSettings` and `Reaction` to `core:model` for shared access.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Introduce `HistoryManager` interface in `core:repository` and its implementation in `core:data`.
- Move `NodeManager`, `CommandSender`, and `HistoryManager` unit tests from the `app` module to `core:data`.
- Update `MeshConnectionManager` and `MeshDataHandler` to use the new `HistoryManager` interface instead of the concrete implementation.
- Refactor unit tests and fakes to use domain models (`Node`, `DataPacket`) instead of database entities.
- Expose `getOrCreateNode` in `NodeManagerImpl` and `resolveNodeNum` in `CommandSenderImpl` for improved testability.
- Update DI module to bind the new `HistoryManager` implementation.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…re modules

- Introduce `RadioController` in `core:model` to abstract radio operations and settings.
- Extract `ServiceRepository`, `MqttManager`, `TracerouteHandler`, and `NeighborInfoHandler` interfaces into `core:repository`.
- Move MQTT repository to `core:network` and handler implementations to `core:data`.
- Update ViewModels to interact with `RadioController` instead of `IMeshService` directly.
- Relocate `ServiceAction` and `TracerouteResponse` to `core:model` for better module accessibility.
- Bind new implementations in `RepositoryModule` and `ServiceModule`.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…e modules

This migration moves core business logic out of the app module and into decoupled core repository and data modules:
- Extracts interfaces for `MeshDataHandler`, `MeshActionHandler`, `MeshRouter`, `MeshMessageProcessor`, and others into `core:repository`.
- Moves concrete implementations to `core:data` under the `manager` package.
- Renames `MessageFilterService` to `MessageFilter` and moves it to the core library.
- Updates dependency injection modules to bind the new interfaces to their respective implementations.
- Refactors `MeshService` and `MeshConnectionManager` to rely on the new repository-level interfaces.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Relocate `RadioInterfaceService`, `PacketHandler`, and `MeshConnectionManager` from the app module to `core:data` and `core:repository`.
- Extract platform-specific implementations for location, workers, and widget updates into Android-specific classes.
- Move shared models and exceptions, including `InterfaceId` and `RadioNotConnectedException`, to `core:model`.
- Introduce `Lazy` injection in several managers to resolve circular dependencies.
- Update Dagger Hilt bindings to support the new interface-driven architecture.
- Adjust unit tests to reflect the new package structure and implementation details.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Implement `upsert` in `NodeRepository` and enable it in `NodeManager`
- Update `SendMessageWorker` to retry on failure instead of marking as error
- Ensure `publicKey` is preserved during `NodeEntity` conversions
- Refine packet ID generation to use positive non-zero integers
- Remove redundant `Dispatchers.Main` in `SettingsViewModel`
- Refactor `MeshDataHandlerTest` to use lazy mocks

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
@github-actions github-actions bot added the enhancement New feature or request label Mar 2, 2026
* Simplify task determination by using capitalized flavor names for Gradle tasks.
* Separate code style (Spotless, Detekt) and shared unit tests into dedicated workflow steps.
* Ensure flavor-specific assembly, lint, and coverage reports run for all matrix variations.
* Improve readability by renaming check steps to distinguish between shared and flavor-specific execution.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
@codecov
Copy link

codecov bot commented Mar 2, 2026

Codecov Report

❌ Patch coverage is 3.84615% with 100 lines in your changes missing coverage. Please review.
✅ Project coverage is 8.12%. Comparing base (8c6bd8a) to head (3b39fa4).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...a/com/geeksville/mesh/service/ServiceBroadcasts.kt 2.38% 41 Missing ⚠️
...h/repository/radio/AndroidRadioInterfaceService.kt 0.00% 19 Missing ⚠️
...eeksville/mesh/service/AndroidMeshWorkerManager.kt 0.00% 10 Missing ⚠️
...ville/mesh/service/MeshServiceNotificationsImpl.kt 0.00% 8 Missing ⚠️
...le/mesh/repository/radio/NordicBleInterfaceSpec.kt 0.00% 4 Missing ⚠️
...geeksville/mesh/service/AndroidAppWidgetUpdater.kt 0.00% 4 Missing ⚠️
...com/geeksville/mesh/ui/sharing/ChannelViewModel.kt 0.00% 4 Missing ⚠️
...main/java/com/geeksville/mesh/model/UIViewModel.kt 0.00% 3 Missing ⚠️
...geeksville/mesh/ui/connections/ScannerViewModel.kt 0.00% 3 Missing ⚠️
...sville/mesh/repository/radio/NordicBleInterface.kt 60.00% 2 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff            @@
##             main   #4685      +/-   ##
=========================================
- Coverage   15.18%   8.12%   -7.07%     
=========================================
  Files          83      64      -19     
  Lines        4353    2499    -1854     
  Branches      734     289     -445     
=========================================
- Hits          661     203     -458     
+ Misses       3566    2264    -1302     
+ Partials      126      32      -94     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Move `ChannelOption.UNSET` to the end of the enum.
- Strip base64 padding characters from generated `ChannelSet` URLs.
- Update `SharedContactTest` to use `MalformedMeshtasticUrlException` and add additional validation test cases.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a major architectural decoupling refactor for the Meshtastic Android app, moving business logic out of the :app module into specialized core modules and introducing clean interfaces for platform-specific radio and service logic.

Changes:

  • Introduced a new :core:repository module containing interface definitions (NodeRepository, ServiceRepository, RadioConfigRepository, MessageFilter, MessageQueue, etc.) to decouple business logic from implementation details.
  • Promoted core domain models (Node, Message, Reaction, NodeSortOption, ContactSettings, etc.) from core.database.model to the core.model module for KMP readiness.
  • Replaced direct AIDL meshService calls with RadioController abstractions throughout ViewModels, use cases, and firmware update handlers.

Reviewed changes

Copilot reviewed 250 out of 250 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
settings.gradle.kts Registers the new :core:repository module
core/repository/src/commonMain/... New interface definitions for the repository layer
core/model/src/... Promoted domain models and new model types
core/data/src/... Implementations renamed with Impl suffix to separate from interfaces
core/domain/src/... Use cases updated to use core.repository interfaces
core/service/src/... Service implementations refactored; MessageFilterImpl replaces MessageFilterService
feature/*/src/... Feature-layer ViewModels and components updated to use new interfaces and model locations
app/src/... App-level service classes refactored to use RadioController and new repository interfaces
Comments suppressed due to low confidence (2)

feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt:1

  • A bare CoroutineScope(Dispatchers.IO) is created without any lifecycle management. This scope will never be cancelled if the handler is destroyed or if an exception occurs, which can lead to coroutine leaks. The existing Esp32OtaUpdateHandler class should have its own managed CoroutineScope injected or stored as a member, similar to how other handlers in this codebase (e.g. MeshLocationManager) maintain a private var scope that is replaced via a start(scope: CoroutineScope) method.
    core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt:1
  • Similar to MeshServiceNotifications, the return type Any is too broad and erases type safety. The same recommendation applies: use a platform-neutral type, Unit, or move the method out of the shared interface.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
…() to getCurrentCacheLimit()

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
- Add `isBonded(address)` helper to `BluetoothRepository` to verify bonding status with permission and state checks.
- Refactor `NordicBleInterfaceSpec` to use the new helper method.
- Update `bond()` to call `updateBluetoothState()`.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Update `PacketDao` and `PacketRepositoryImpl` to identify packets using specific fields (`id`, `from`, and `to`) rather than checking for full object equality. This ensures more reliable updates when modifying packet status or IDs.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
@jamesarich jamesarich added this pull request to the merge queue Mar 3, 2026
@jamesarich jamesarich changed the title feat/decoupling feat: service decoupling Mar 3, 2026
Merged via the queue into main with commit 2c49db8 Mar 3, 2026
8 of 9 checks passed
@jamesarich jamesarich deleted the feat/decoupling branch March 3, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants