refactor: migrate from Hilt to Koin and expand KMP common modules (#4746)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2026-03-09 20:19:46 -05:00
committed by GitHub
parent a5390a80e7
commit 875cf1cff2
440 changed files with 3738 additions and 3508 deletions

View File

@@ -9,7 +9,7 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
| Directory | Description |
| :--- | :--- |
| `app/` | Main application module. Contains `MainActivity`, Hilt DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
| `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. |
@@ -39,8 +39,8 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
- **Concurrency:** Use Kotlin Coroutines and Flow.
- **Thread-Safety:** Use `atomicfu` and `kotlinx.collections.immutable` for shared state in `commonMain`. Avoid `synchronized` or JVM-specific atomics.
- **Dependency Injection:**
- Use **Hilt**.
- **Restriction:** Move Hilt modules to the `app` module if the library module is KMP with multiple flavors, as KSP/Hilt generation often fails in these complex scenarios.
- Use **Koin**.
- **Restriction:** Move Koin modules to the `app` module if the library module is KMP with multiple flavors, as KSP/Koin generation often fails in these complex scenarios.
### C. Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
@@ -58,4 +58,4 @@ Use `expect`/`actual` sparingly for platform-specific types (e.g., `Location`, `
## 5. Troubleshooting
- **Build Failures:** Always check `gradle/libs.versions.toml` for dependency conflicts.
- **Hilt Generation:** If `@Inject` fails in a KMP module, ensure the corresponding module is bound in the `app` layer's DI package.
- **Koin Generation:** If a component fails to inject in a KMP module, ensure the corresponding module is bound in the `app` layer's DI package.

75
GEMINI.md Normal file
View File

@@ -0,0 +1,75 @@
# Meshtastic-Android: AI Agent Instructions (GEMINI.md)
**CRITICAL AGENT DIRECTIVE:** This file contains validated, comprehensive instructions for interacting with the Meshtastic-Android repository. You MUST adhere strictly to these rules, build commands, and architectural constraints. Only deviate or explore alternatives if the documented commands fail with unexpected errors.
## 1. Project Overview & Architecture
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks.
- **Language:** Kotlin (primary), AIDL.
- **Build System:** Gradle (Kotlin DSL). JDK 17 is REQUIRED.
- **Target SDK:** API 36. Min SDK: API 26 (Android 8.0).
- **Flavors:**
- `fdroid`: Open source only, no tracking/analytics.
- `google`: Includes Google Play Services (Maps) and DataDog analytics.
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
- **KMP Modules:** `core:model`, `core:proto`, `core:common`, `core:resources`, `core:database`, `core:datastore`, `core:repository`, `core:domain`, `core:prefs`, `core:network`, `core:di`, and `core:data`.
- **UI:** Jetpack Compose (Material 3).
- **DI:** Koin (centralized in `app` module for KMP modules).
- **Navigation:** Type-Safe Jetpack Navigation.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
## 2. Environment Setup (Mandatory First Steps)
Before attempting any builds or tests, ensure the environment is configured:
1. **JDK 17 MUST be used** to prevent Gradle sync/build failures.
2. **Secrets:** You must copy `secrets.defaults.properties` to `local.properties` to satisfy build requirements, even for dummy builds:
```properties
# local.properties example
MAPS_API_KEY=dummy_key
datadogApplicationId=dummy_id
datadogClientToken=dummy_token
```
## 3. Strict Execution Commands
Always run commands in the following order to ensure reliability. Do not attempt to bypass `clean` if you are facing build issues.
**Formatting & Linting (Run BEFORE committing):**
```bash
./gradlew spotlessApply # Always run to auto-fix formatting
./gradlew detekt # Run static analysis
```
**Building:**
```bash
./gradlew clean # Always start here if facing issues
./gradlew assembleDebug # Full build (fdroid and google)
```
**Testing:**
```bash
./gradlew testAndroid # Run Android unit tests (Robolectric)
./gradlew testCommonMain # Run KMP common tests (if applicable)
./gradlew connectedAndroidTest # Run instrumented tests
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
## 4. Coding Standards & Mandates
- **UI Components:** Always utilize `:core:ui` for shared Jetpack Compose components (e.g., `MeshtasticResourceDialog`, `TransportIcon`). Do not reinvent standard dialogs or preference screens.
- **Strings/Localization:** **NEVER** use hardcoded strings or the legacy `app/src/main/res/values/strings.xml`.
- **Rule:** You MUST use the Compose Multiplatform Resource library.
- **Location:** `core/resources/src/commonMain/composeResources/values/strings.xml`.
- **Usage:** `stringResource(Res.string.your_key)`
- **Bluetooth/BLE:** Do not use legacy Android Bluetooth callbacks. All BLE communication MUST route through `:core:ble`, utilizing Nordic Semiconductor's Android Common Libraries and Kotlin Coroutines/Flows.
- **Dependencies:** Never assume a library is available. Check `gradle/libs.versions.toml` first. If adding a new dependency, it MUST be added to the version catalog, not directly to a `build.gradle.kts` file.
- **Namespacing:** Prefer the `org.meshtastic` namespace for all new code. The legacy `com.geeksville.mesh` ApplicationId is maintained for compatibility.
## 5. Module Map
When locating code to modify, use this map:
- **`app/`**: Main application wiring and Koin modules. Package: `org.meshtastic.app`.
- **`:core:data`**: Core business logic and managers. Package: `org.meshtastic.core.data`.
- **`:core:repository`**: Domain interfaces and common models. Package: `org.meshtastic.core.repository`.
- **`:core:ble`**: Coroutine-based Bluetooth logic.
- **`:core:api`**: AIDL service interface (`IMeshService.aidl`) for third-party integrations (like ATAK).
- **`:core:ui`**: Shared Compose UI elements and theming.
- **`:feature:*`**: Isolated feature screens (e.g., `:feature:messaging` for chat, `:feature:map` for mapping).

View File

@@ -63,7 +63,7 @@ The app follows modern Android development practices, built on top of a shared K
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, enabling future support for Desktop and Web.
- **UI:** Jetpack Compose (Material 3) using Compose Multiplatform resources.
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
- **Dependency Injection:** Hilt (mapped to KMP `javax.inject` interfaces).
- **Dependency Injection:** Koin with Koin Annotations (Compiler Plugin).
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).

View File

@@ -11,8 +11,8 @@ The single Activity of the application. It hosts the `NavHost` and manages the r
### 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.
### 3. Hilt Application
`MeshUtilApplication` is the Hilt entry point, providing the global dependency injection container.
### 3. Koin Application
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
## Architecture
The module primarily serves as a "glue" layer, connecting:

View File

@@ -29,7 +29,7 @@ plugins {
alias(libs.plugins.meshtastic.android.application)
alias(libs.plugins.meshtastic.android.application.flavors)
alias(libs.plugins.meshtastic.android.application.compose)
alias(libs.plugins.meshtastic.hilt)
id("meshtastic.koin")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.devtools.ksp)
alias(libs.plugins.secrets)
@@ -216,6 +216,7 @@ dependencies {
implementation(projects.core.database)
implementation(projects.core.datastore)
implementation(projects.core.di)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.navigation)
implementation(projects.core.network)
@@ -261,9 +262,11 @@ dependencies {
implementation(libs.org.eclipse.paho.client.mqttv3)
implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
ksp(libs.androidx.hilt.compiler)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.androidx.workmanager)
implementation(libs.koin.annotations)
implementation(libs.accompanist.permissions)
implementation(libs.kermit)
implementation(libs.kotlinx.datetime)
@@ -300,13 +303,13 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.nordic.client.android.mock)
androidTestImplementation(libs.nordic.core.mock)
testImplementation(libs.androidx.work.testing)
testImplementation(libs.koin.test)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)

View File

@@ -4,6 +4,13 @@
<CurrentIssues>
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
<ID>LongParameterList:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$( savedStateHandle: SavedStateHandle, private val app: Application, dispatchers: CoroutineDispatchers, meshLogRepository: MeshLogRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, tracerouteSnapshotRepository: TracerouteSnapshotRepository, nodeRequestActions: NodeRequestActions, alertManager: AlertManager, getNodeDetailsUseCase: GetNodeDetailsUseCase, )</ID>
<ID>LongParameterList:AndroidNodeListViewModel.kt$AndroidNodeListViewModel$( savedStateHandle: SavedStateHandle, nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, radioController: RadioController, nodeManagementActions: NodeManagementActions, getFilteredNodesUseCase: GetFilteredNodesUseCase, nodeFilterPreferences: NodeFilterPreferences, )</ID>
<ID>LongParameterList:AndroidRadioConfigViewModel.kt$AndroidRadioConfigViewModel$( savedStateHandle: SavedStateHandle, private val app: Application, radioConfigRepository: RadioConfigRepository, packetRepository: PacketRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, private val locationRepository: LocationRepository, mapConsentPrefs: MapConsentPrefs, analyticsPrefs: AnalyticsPrefs, homoglyphEncodingPrefs: HomoglyphPrefs, toggleAnalyticsUseCase: ToggleAnalyticsUseCase, toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase, importProfileUseCase: ImportProfileUseCase, exportProfileUseCase: ExportProfileUseCase, exportSecurityConfigUseCase: ExportSecurityConfigUseCase, installProfileUseCase: InstallProfileUseCase, radioConfigUseCase: RadioConfigUseCase, adminActionsUseCase: AdminActionsUseCase, processRadioResponseUseCase: ProcessRadioResponseUseCase, )</ID>
<ID>LongParameterList:AndroidSettingsViewModel.kt$AndroidSettingsViewModel$( private val app: Application, radioConfigRepository: RadioConfigRepository, radioController: RadioController, nodeRepository: NodeRepository, uiPrefs: UiPrefs, buildConfigProvider: BuildConfigProvider, databaseManager: DatabaseManager, meshLogPrefs: MeshLogPrefs, setThemeUseCase: SetThemeUseCase, setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase, setProvideLocationUseCase: SetProvideLocationUseCase, setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase, setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase, meshLocationUseCase: MeshLocationUseCase, exportDataUseCase: ExportDataUseCase, isOtaCapableUseCase: IsOtaCapableUseCase, )</ID>
<ID>MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1000L</ID>
<ID>MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-5</ID>
<ID>MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-7</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
@@ -15,10 +22,9 @@
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
<ID>MaxLineLength:DataSourceModule.kt$DataSourceModule$fun</ID>
<ID>ParameterListWrapping:DataSourceModule.kt$DataSourceModule$(impl: BootloaderOtaQuirksJsonDataSourceImpl)</ID>
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
<ID>TooGenericExceptionCaught:AndroidRadioConfigViewModel.kt$AndroidRadioConfigViewModel$ex: Exception</ID>
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception</ID>
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
<ID>TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface</ID>

View File

@@ -17,32 +17,21 @@
package org.meshtastic.app.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.koin.test.KoinTest
import org.koin.test.inject
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MessageFilterIntegrationTest {
class MessageFilterIntegrationTest : KoinTest {
@get:Rule var hiltRule = HiltAndroidRule(this)
private val filterPrefs: FilterPrefs by inject()
@Inject lateinit var filterPrefs: FilterPrefs
@Inject lateinit var filterService: MessageFilter
@Before
fun setup() {
hiltRule.inject()
}
private val filterService: MessageFilter by inject()
@Test
fun filterPrefsIntegration() = runTest {

View File

@@ -18,16 +18,17 @@ package org.meshtastic.app.analytics
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Inject
/**
* F-Droid specific implementation of [PlatformAnalytics]. This provides no-op implementations for analytics and other
* platform services.
*/
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
@Single
class FdroidPlatformAnalytics : PlatformAnalytics {
init {
// For F-Droid builds we don't initialize external analytics services.
// In debug builds we attach a DebugTree for convenient local logging, but

View File

@@ -16,24 +16,19 @@
*/
package org.meshtastic.app.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class FDroidNetworkModule {
@Provides
@Singleton
@Single
fun provideOkHttpClient(buildConfigProvider: BuildConfigProvider): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
interceptor =
@@ -45,8 +40,7 @@ class FDroidNetworkModule {
)
.build()
@Provides
@Singleton
@Single
fun provideApiService(): ApiService = object : ApiService {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")

View File

@@ -0,0 +1,22 @@
/*
* 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
@Module(includes = [FDroidNetworkModule::class])
class FlavorModule

View File

@@ -18,9 +18,11 @@ package org.meshtastic.app.map
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
@Single
class FdroidMapViewProvider : MapViewProvider {
@Composable
override fun MapView(
@@ -33,7 +35,7 @@ class FdroidMapViewProvider : MapViewProvider {
tracerouteNodePositions: Map<Int, Any>,
onTracerouteMappableCountChanged: (Int, Int) -> Unit,
) {
val mapViewModel: MapViewModel = hiltViewModel()
val mapViewModel: MapViewModel = koinViewModel()
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,

View File

@@ -74,7 +74,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -83,6 +82,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
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
@@ -235,7 +235,7 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
@Composable
fun MapView(
modifier: Modifier = Modifier,
mapViewModel: MapViewModel = hiltViewModel(),
mapViewModel: MapViewModel = koinViewModel(),
navigateToNodeDetails: (Int) -> Unit,
focusedNodeNum: Int? = null,
nodeTracks: List<Position>? = null,

View File

@@ -18,10 +18,10 @@ package org.meshtastic.app.map
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
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.DataPacket
import org.meshtastic.core.model.RadioController
@@ -33,13 +33,10 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.BaseMapViewModel
import org.meshtastic.proto.LocalConfig
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
class MapViewModel
@Inject
constructor(
@KoinViewModel
class MapViewModel(
mapPrefs: MapPrefs,
packetRepository: PacketRepository,
override val nodeRepository: NodeRepository,

View File

@@ -86,22 +86,6 @@ open class NOAAWmsTileSource(
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 tile2lat(y: Int, z: Int): Double {

View File

@@ -14,13 +14,13 @@
* 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.node.component
package org.meshtastic.app.node.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.meshtastic.core.model.Node
@Composable
internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
fun InlineMap(node: Node, modifier: Modifier = Modifier) {
// No-op for F-Droid builds
}

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
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.metrics
package org.meshtastic.app.node.metrics
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
internal object TracerouteMapOverlayInsets {
val overlayAlignment: Alignment = Alignment.BottomEnd
val overlayPadding: PaddingValues = PaddingValues(end = 16.dp, bottom = 16.dp)
val contentHorizontalAlignment: Alignment.Horizontal = Alignment.End
}
fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
overlayAlignment = Alignment.BottomEnd,
overlayPadding = PaddingValues(end = 16.dp, bottom = 16.dp),
contentHorizontalAlignment = Alignment.End,
)

View File

@@ -46,16 +46,15 @@ import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize
import dagger.hilt.android.qualifiers.ApplicationContext
import io.opentelemetry.api.GlobalOpenTelemetry
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Inject
import co.touchlab.kermit.Logger as KermitLogger
/**
@@ -65,12 +64,9 @@ import co.touchlab.kermit.Logger as KermitLogger
* This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and
* respect privacy-focused environments.
*/
class GooglePlatformAnalytics
@Inject
constructor(
@ApplicationContext private val context: Context,
private val analyticsPrefs: AnalyticsPrefs,
) : PlatformAnalytics {
@Single
class GooglePlatformAnalytics(private val context: Context, private val analyticsPrefs: AnalyticsPrefs) :
PlatformAnalytics {
private val sampleRate = 100f.takeIf { BuildConfig.DEBUG } ?: 10f // For Datadog remote sample rate

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

@@ -19,35 +19,24 @@ package org.meshtastic.app.di
import android.content.Context
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import java.io.File
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
interface GoogleNetworkModule {
class GoogleNetworkModule {
@Binds @Singleton
fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService
@Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl
companion object {
@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context,
buildConfigProvider: BuildConfigProvider,
): OkHttpClient = OkHttpClient.Builder()
@Single
fun provideOkHttpClient(context: Context, buildConfigProvider: BuildConfigProvider): OkHttpClient =
OkHttpClient.Builder()
.cache(
cache =
Cache(
@@ -63,10 +52,7 @@ interface GoogleNetworkModule {
}
},
)
.addInterceptor(
interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build(),
)
.addInterceptor(interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build())
.eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory())
.build()
}
}

View File

@@ -1,34 +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 org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.app.analytics.GooglePlatformAnalytics
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Singleton
/** Hilt module to provide the [GooglePlatformAnalytics] for the google flavor. */
@Module
@InstallIn(SingletonComponent::class)
abstract class GooglePlatformAnalyticsModule {
@Binds @Singleton
abstract fun bindPlatformHelper(googlePlatformHelper: GooglePlatformAnalytics): PlatformAnalytics
}

View File

@@ -18,9 +18,11 @@ package org.meshtastic.app.map
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.Single
import org.meshtastic.core.ui.util.MapViewProvider
@Single
class GoogleMapViewProvider : MapViewProvider {
@Composable
override fun MapView(
@@ -33,7 +35,7 @@ class GoogleMapViewProvider : MapViewProvider {
tracerouteNodePositions: Map<Int, Any>,
onTracerouteMappableCountChanged: (Int, Int) -> Unit,
) {
val mapViewModel: MapViewModel = hiltViewModel()
val mapViewModel: MapViewModel = koinViewModel()
org.meshtastic.app.map.MapView(
modifier = modifier,
mapViewModel = mapViewModel,

View File

@@ -59,7 +59,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -95,6 +94,7 @@ import com.google.maps.android.data.kml.KmlLayer
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.json.JSONObject
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.component.ClusterItemsListDialog
import org.meshtastic.app.map.component.CustomMapLayersSheet
import org.meshtastic.app.map.component.CustomTileProviderManagerSheet
@@ -149,7 +149,7 @@ private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
@Composable
fun MapView(
modifier: Modifier = Modifier,
mapViewModel: MapViewModel = hiltViewModel(),
mapViewModel: MapViewModel = koinViewModel(),
navigateToNodeDetails: (Int) -> Unit,
focusedNodeNum: Int? = null,
nodeTracks: List<Position>? = null,

View File

@@ -29,7 +29,6 @@ import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.MapType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -43,6 +42,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.app.map.model.CustomTileProviderConfig
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
import org.meshtastic.app.map.repository.CustomTileProviderRepository
@@ -62,7 +62,6 @@ import java.io.IOException
import java.io.InputStream
import java.net.MalformedURLException
import java.net.URL
import javax.inject.Inject
import kotlin.uuid.Uuid
private const val TILE_SIZE = 256
@@ -77,10 +76,8 @@ data class MapCameraPosition(
)
@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
class MapViewModel
@Inject
constructor(
@KoinViewModel
class MapViewModel(
private val application: Application,
mapPrefs: MapPrefs,
private val googleMapsPrefs: GoogleMapsPrefs,

View File

@@ -0,0 +1,44 @@
/*
* 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.prefs.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
@Module
@ComponentScan("org.meshtastic.app.map")
class GoogleMapsKoinModule {
@Single
@Named("GoogleMapsDataStore")
fun provideGoogleMapsDataStore(context: Context): DataStore<Preferences> = PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile = { context.preferencesDataStoreFile("google_maps_ds") },
)
}

View File

@@ -1,68 +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 org.meshtastic.app.map.prefs.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
import org.meshtastic.app.map.prefs.map.GoogleMapsPrefsImpl
import org.meshtastic.app.map.repository.CustomTileProviderRepository
import org.meshtastic.app.map.repository.CustomTileProviderRepositoryImpl
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GoogleMapsDataStore
@InstallIn(SingletonComponent::class)
@Module
interface GoogleMapsModule {
@Binds fun bindGoogleMapsPrefs(googleMapsPrefsImpl: GoogleMapsPrefsImpl): GoogleMapsPrefs
@Binds
@Singleton
fun bindCustomTileProviderRepository(impl: CustomTileProviderRepositoryImpl): CustomTileProviderRepository
companion object {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Provides
@Singleton
@GoogleMapsDataStore
fun provideGoogleMapsDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("google_maps_ds") },
)
}
}

View File

@@ -31,10 +31,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.app.map.prefs.di.GoogleMapsDataStore
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import javax.inject.Inject
import javax.inject.Singleton
/** Interface for prefs specific to Google Maps. For general map prefs, see MapPrefs. */
interface GoogleMapsPrefs {
@@ -75,11 +74,9 @@ interface GoogleMapsPrefs {
fun setNetworkMapLayers(value: Set<String>)
}
@Singleton
class GoogleMapsPrefsImpl
@Inject
constructor(
@GoogleMapsDataStore private val dataStore: DataStore<Preferences>,
@Single
class GoogleMapsPrefsImpl(
@Named("GoogleMapsDataStore") private val dataStore: DataStore<Preferences>,
dispatchers: CoroutineDispatchers,
) : GoogleMapsPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)

View File

@@ -23,11 +23,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Single
import org.meshtastic.app.map.model.CustomTileProviderConfig
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.MapTileProviderPrefs
import javax.inject.Inject
import javax.inject.Singleton
interface CustomTileProviderRepository {
fun getCustomTileProviders(): Flow<List<CustomTileProviderConfig>>
@@ -41,10 +40,8 @@ interface CustomTileProviderRepository {
suspend fun getCustomTileProviderById(configId: String): CustomTileProviderConfig?
}
@Singleton
class CustomTileProviderRepositoryImpl
@Inject
constructor(
@Single
class CustomTileProviderRepositoryImpl(
private val json: Json,
private val dispatchers: CoroutineDispatchers,
private val mapTileProviderPrefs: MapTileProviderPrefs,

View File

@@ -14,7 +14,7 @@
* 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.node.component
package org.meshtastic.app.node.component
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
@@ -39,7 +39,7 @@ private const val DEFAULT_ZOOM = 15f
@OptIn(MapsComposeExperimentalApi::class)
@Composable
internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
fun InlineMap(node: Node, modifier: Modifier = Modifier) {
val dark = isSystemInDarkTheme()
val mapColorScheme =
when (dark) {

View File

@@ -0,0 +1,28 @@
/*
* 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.metrics
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
overlayAlignment = Alignment.BottomCenter,
overlayPadding = PaddingValues(bottom = 16.dp),
contentHorizontalAlignment = Alignment.CenterHorizontally,
)

View File

@@ -1,78 +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 org.meshtastic.app
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.app.repository.radio.AndroidRadioInterfaceService
import org.meshtastic.app.service.AndroidAppWidgetUpdater
import org.meshtastic.app.service.AndroidMeshLocationManager
import org.meshtastic.app.service.AndroidMeshWorkerManager
import org.meshtastic.app.service.MeshServiceNotificationsImpl
import org.meshtastic.app.service.ServiceBroadcasts
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.repository.MeshServiceNotifications
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
interface ApplicationModule {
@Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications
@Binds
fun bindMeshLocationManager(impl: AndroidMeshLocationManager): org.meshtastic.core.repository.MeshLocationManager
@Binds fun bindMeshWorkerManager(impl: AndroidMeshWorkerManager): org.meshtastic.core.repository.MeshWorkerManager
@Binds fun bindAppWidgetUpdater(impl: AndroidAppWidgetUpdater): org.meshtastic.core.repository.AppWidgetUpdater
@Binds
fun bindRadioInterfaceService(
impl: AndroidRadioInterfaceService,
): org.meshtastic.core.repository.RadioInterfaceService
@Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts
companion object {
@Provides @ProcessLifecycle
fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
@Provides
@ProcessLifecycle
fun provideProcessLifecycle(@ProcessLifecycle processLifecycleOwner: LifecycleOwner): Lifecycle =
processLifecycleOwner.lifecycle
@Singleton
@Provides
fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
override val isDebug: Boolean = BuildConfig.DEBUG
override val applicationId: String = BuildConfig.APPLICATION_ID
override val versionCode: Int = BuildConfig.VERSION_CODE
override val versionName: String = BuildConfig.VERSION_NAME
override val absoluteMinFwVersion: String = BuildConfig.ABS_MIN_FW_VERSION
override val minFwVersion: String = BuildConfig.MIN_FW_VERSION
}
}
}

View File

@@ -32,7 +32,6 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.CompositionLocalProvider
@@ -40,18 +39,22 @@ import androidx.compose.runtime.getValue
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.meshtastic.app.intro.AnalyticsIntro
import org.meshtastic.app.intro.AndroidIntroViewModel
import org.meshtastic.app.map.getMapViewProvider
import org.meshtastic.app.model.UIViewModel
import org.meshtastic.app.node.component.InlineMap
import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
import org.meshtastic.app.ui.MainScreen
import org.meshtastic.core.barcode.rememberBarcodeScanner
import org.meshtastic.core.model.util.dispatchMeshtasticUri
@@ -63,27 +66,30 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
import org.meshtastic.core.ui.util.LocalInlineMapProvider
import org.meshtastic.core.ui.util.LocalMapViewProvider
import org.meshtastic.core.ui.util.LocalNfcScannerProvider
import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.intro.AppIntroductionScreen
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val model: UIViewModel by viewModels()
private val model: UIViewModel by viewModel()
/**
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
* itself as a LifecycleObserver in its init block.
*/
@Inject internal lateinit var meshServiceClient: MeshServiceClient
internal val meshServiceClient: MeshServiceClient by inject { parametersOf(this) }
@Inject internal lateinit var androidEnvironment: AndroidEnvironment
internal val androidEnvironment: AndroidEnvironment by inject()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
// Eagerly evaluate lazy Koin dependency so it registers its LifecycleObserver
meshServiceClient.hashCode()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -124,6 +130,8 @@ class MainActivity : ComponentActivity() {
LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) },
LocalAnalyticsIntroProvider provides { AnalyticsIntro() },
LocalMapViewProvider provides getMapViewProvider(),
LocalInlineMapProvider provides { node, modifier -> InlineMap(node, modifier) },
LocalTracerouteMapOverlayInsetsProvider provides getTracerouteMapOverlayInsets(),
) {
AppTheme(dynamicColor = dynamic, darkTheme = dark) {
val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle()
@@ -135,7 +143,7 @@ class MainActivity : ComponentActivity() {
if (appIntroCompleted) {
MainScreen(uIViewModel = model)
} else {
val introViewModel = hiltViewModel<AndroidIntroViewModel>()
val introViewModel = koinViewModel<AndroidIntroViewModel>()
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel)
}
}

View File

@@ -14,10 +14,11 @@
* 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.di
package org.meshtastic.app
import javax.inject.Qualifier
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProcessLifecycle
@Module
@ComponentScan("org.meshtastic.app")
class MainKoinModule

View File

@@ -23,9 +23,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory
import org.meshtastic.app.service.MeshService
import org.meshtastic.app.service.startService
import org.meshtastic.core.common.util.SequentialJob
@@ -33,14 +32,11 @@ import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.BindFailedException
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceClient
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@ActivityScoped
class MeshServiceClient
@Inject
constructor(
@ActivityContext private val context: Context,
@Factory
class MeshServiceClient(
private val context: Context,
private val serviceRepository: AndroidServiceRepository,
private val serviceSetupJob: SequentialJob,
) : ServiceClient<IMeshService>(IMeshService.Stub::asInterface),

View File

@@ -21,17 +21,11 @@ import android.appwidget.AppWidgetProviderInfo
import android.os.Build
import androidx.collection.intSetOf
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import co.touchlab.kermit.Logger
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
@@ -40,13 +34,17 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
import org.meshtastic.app.di.AppKoinModule
import org.meshtastic.app.di.module
import org.meshtastic.app.widget.LocalStatsWidgetReceiver
import org.meshtastic.app.worker.MeshLogCleanupWorker
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshPrefs
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@@ -54,15 +52,11 @@ import kotlin.time.toJavaDuration
/**
* The main application class for Meshtastic.
*
* This class is annotated with [HiltAndroidApp] to enable Hilt for dependency injection. It initializes core
* application components, including analytics and platform-specific helpers, and manages analytics consent based on
* user preferences.
* This class initializes core application components using Koin for dependency injection.
*/
@HiltAndroidApp
open class MeshUtilApplication :
Application(),
Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
private val applicationScope = CoroutineScope(Dispatchers.Default)
@@ -70,6 +64,12 @@ open class MeshUtilApplication :
super.onCreate()
ContextServices.app = this
startKoin {
androidContext(this@MeshUtilApplication)
workManagerFactory()
modules(AppKoinModule().module())
}
// Schedule periodic MeshLog cleanup
scheduleMeshLogCleanup()
@@ -93,15 +93,11 @@ open class MeshUtilApplication :
pushPreview()
val entryPoint =
EntryPointAccessors.fromApplication(
this@MeshUtilApplication,
org.meshtastic.app.widget.LocalStatsWidget.LocalStatsWidgetEntryPoint::class.java,
)
val widgetStateProvider: org.meshtastic.app.widget.LocalStatsWidgetStateProvider = get()
try {
// Wait for real data for up to 30 seconds before pushing an updated preview
withTimeout(30.seconds) {
entryPoint.widgetStateProvider().state.first { it.showContent && it.nodeShortName != null }
widgetStateProvider.state.first { it.showContent && it.nodeShortName != null }
}
Logger.i { "Real node data acquired. Pushing updated widget preview." }
@@ -113,17 +109,20 @@ open class MeshUtilApplication :
}
// Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB
val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
applicationScope.launch { entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress.value) }
applicationScope.launch {
val dbManager: DatabaseManager = get()
val meshPrefs: MeshPrefs = get()
dbManager.init(meshPrefs.deviceAddress.value)
}
}
override fun onTerminate() {
// Shutdown managers (useful for Robolectric tests)
val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
entryPoint.databaseManager().close()
entryPoint.androidEnvironment().close()
get<DatabaseManager>().close()
get<AndroidEnvironment>().close()
applicationScope.cancel()
super.onTerminate()
org.koin.core.context.stopKoin()
}
private fun scheduleMeshLogCleanup() {
@@ -139,19 +138,7 @@ open class MeshUtilApplication :
}
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppEntryPoint {
fun databaseManager(): DatabaseManager
fun meshPrefs(): MeshPrefs
fun meshLogPrefs(): MeshLogPrefs
fun androidEnvironment(): AndroidEnvironment
get() = Configuration.Builder().setWorkerFactory(get()).build()
}
fun logAssert(executeReliableWrite: Boolean) {

View File

@@ -0,0 +1,111 @@
/*
* 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 android.app.Application
import android.content.Context
import android.hardware.usb.UsbManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.WorkManager
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.app.repository.usb.ProbeTableProvider
import org.meshtastic.core.ble.di.CoreBleAndroidModule
import org.meshtastic.core.ble.di.CoreBleModule
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.di.CoreCommonModule
import org.meshtastic.core.data.di.CoreDataAndroidModule
import org.meshtastic.core.data.di.CoreDataModule
import org.meshtastic.core.database.di.CoreDatabaseAndroidModule
import org.meshtastic.core.database.di.CoreDatabaseModule
import org.meshtastic.core.datastore.di.CoreDatastoreAndroidModule
import org.meshtastic.core.datastore.di.CoreDatastoreModule
import org.meshtastic.core.di.di.CoreDiModule
import org.meshtastic.core.network.di.CoreNetworkModule
import org.meshtastic.core.prefs.di.CorePrefsAndroidModule
import org.meshtastic.core.prefs.di.CorePrefsModule
import org.meshtastic.core.service.di.CoreServiceAndroidModule
import org.meshtastic.core.service.di.CoreServiceModule
import org.meshtastic.core.ui.di.CoreUiModule
import org.meshtastic.feature.firmware.di.FeatureFirmwareModule
import org.meshtastic.feature.intro.di.FeatureIntroModule
import org.meshtastic.feature.map.di.FeatureMapModule
import org.meshtastic.feature.messaging.di.FeatureMessagingModule
import org.meshtastic.feature.node.di.FeatureNodeModule
import org.meshtastic.feature.settings.di.FeatureSettingsModule
@Module(
includes =
[
org.meshtastic.app.MainKoinModule::class,
CoreDiModule::class,
CoreCommonModule::class,
CoreBleModule::class,
CoreBleAndroidModule::class,
CoreDataModule::class,
CoreDataAndroidModule::class,
org.meshtastic.core.domain.di.CoreDomainModule::class,
CoreDatabaseModule::class,
CoreDatabaseAndroidModule::class,
org.meshtastic.core.repository.di.CoreRepositoryModule::class,
CoreDatastoreModule::class,
CoreDatastoreAndroidModule::class,
CorePrefsModule::class,
CorePrefsAndroidModule::class,
CoreServiceModule::class,
CoreServiceAndroidModule::class,
CoreNetworkModule::class,
CoreUiModule::class,
FeatureNodeModule::class,
FeatureMessagingModule::class,
FeatureMapModule::class,
FeatureSettingsModule::class,
FeatureFirmwareModule::class,
FeatureIntroModule::class,
NetworkModule::class,
FlavorModule::class,
],
)
class AppKoinModule {
@Single
@Named("ProcessLifecycle")
fun provideProcessLifecycle(): Lifecycle = ProcessLifecycleOwner.get().lifecycle
@Single
fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider {
override val isDebug: Boolean = org.meshtastic.app.BuildConfig.DEBUG
override val applicationId: String = org.meshtastic.app.BuildConfig.APPLICATION_ID
override val versionCode: Int = org.meshtastic.app.BuildConfig.VERSION_CODE
override val versionName: String = org.meshtastic.app.BuildConfig.VERSION_NAME
override val absoluteMinFwVersion: String = org.meshtastic.app.BuildConfig.ABS_MIN_FW_VERSION
override val minFwVersion: String = org.meshtastic.app.BuildConfig.MIN_FW_VERSION
}
@Single fun provideWorkManager(context: Application): WorkManager = WorkManager.getInstance(context)
@Single
fun provideUsbManager(application: Application): UsbManager? =
application.getSystemService(Context.USB_SERVICE) as UsbManager?
@Single fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
@Single fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
}

View File

@@ -1,75 +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 org.meshtastic.app.di
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.native
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
import org.meshtastic.core.ble.AndroidBleConnectionFactory
import org.meshtastic.core.ble.AndroidBleScanner
import org.meshtastic.core.ble.AndroidBluetoothRepository
import org.meshtastic.core.ble.BleConnection
import org.meshtastic.core.ble.BleConnectionFactory
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.di.CoroutineDispatchers
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class BleModule {
@Binds @Singleton
abstract fun bindBleScanner(impl: AndroidBleScanner): BleScanner
@Binds @Singleton
abstract fun bindBluetoothRepository(impl: AndroidBluetoothRepository): BluetoothRepository
@Binds @Singleton
abstract fun bindBleConnectionFactory(impl: AndroidBleConnectionFactory): BleConnectionFactory
companion object {
@Provides
@Singleton
fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment =
NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true)
@Provides
@Singleton
fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope =
CoroutineScope(SupervisorJob() + dispatchers.default)
@Provides
@Singleton
fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager =
CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope)
@Provides
fun provideBleConnection(factory: BleConnectionFactory, coroutineScope: CoroutineScope): BleConnection =
factory.create(coroutineScope, "BLE")
}
}

View File

@@ -1,47 +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 org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSourceImpl
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSourceImpl
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSourceImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface DataSourceModule {
@Binds
@Singleton
fun bindDeviceHardwareJsonDataSource(impl: DeviceHardwareJsonDataSourceImpl): DeviceHardwareJsonDataSource
@Binds
@Singleton
fun bindFirmwareReleaseJsonDataSource(impl: FirmwareReleaseJsonDataSourceImpl): FirmwareReleaseJsonDataSource
@Binds
@Singleton
fun bindBootloaderOtaQuirksJsonDataSource(
impl: BootloaderOtaQuirksJsonDataSourceImpl,
): BootloaderOtaQuirksJsonDataSource
}

View File

@@ -16,7 +16,10 @@
*/
package org.meshtastic.app.di
import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.nsd.NsdManager
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
@@ -25,72 +28,66 @@ import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.util.DebugLogger
import coil3.util.Logger
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.common.BuildConfigProvider
import javax.inject.Singleton
private const val DISK_CACHE_PERCENT = 0.02
private const val MEMORY_CACHE_PERCENT = 0.25
@InstallIn(SingletonComponent::class)
@Module
interface NetworkModule {
class NetworkModule {
@Binds
@Singleton
@Single
fun provideConnectivityManager(application: Application): ConnectivityManager =
application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@Single
fun provideNsdManager(application: Application): NsdManager =
application.getSystemService(Context.NSD_SERVICE) as NsdManager
@Single
fun bindMqttRepository(
impl: org.meshtastic.core.network.repository.MQTTRepositoryImpl,
): org.meshtastic.core.network.repository.MQTTRepository
): org.meshtastic.core.network.repository.MQTTRepository = impl
companion object {
@Provides
@Singleton
fun provideImageLoader(
okHttpClient: OkHttpClient,
@ApplicationContext application: Context,
buildConfigProvider: BuildConfigProvider,
): ImageLoader {
val sharedOkHttp = okHttpClient.newBuilder().build()
return ImageLoader.Builder(context = application)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
add(SvgDecoder.Factory(scaleToDensity = true))
}
.memoryCache {
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
}
.diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
.logger(
logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null,
)
.crossfade(enable = true)
.build()
}
@Single
fun provideImageLoader(
okHttpClient: OkHttpClient,
application: Context,
buildConfigProvider: BuildConfigProvider,
): ImageLoader {
val sharedOkHttp = okHttpClient.newBuilder().build()
return ImageLoader.Builder(context = application)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
add(SvgDecoder.Factory(scaleToDensity = true))
}
.memoryCache {
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
}
.diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
.logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null)
.crossfade(enable = true)
.build()
}
@Provides
@Singleton
fun provideJson(): Json = Json {
isLenient = true
ignoreUnknownKeys = true
}
@Single
fun provideJson(): Json = Json {
isLenient = true
ignoreUnknownKeys = true
}
@Provides
@Singleton
fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
engine { preconfigured = okHttpClient }
@Single
fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
engine { preconfigured = okHttpClient }
install(plugin = ContentNegotiation) { json(json) }
}
install(plugin = ContentNegotiation) { json(json) }
}
}

View File

@@ -1,37 +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 org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.data.datasource.SwitchingNodeInfoReadDataSource
import org.meshtastic.core.data.datasource.SwitchingNodeInfoWriteDataSource
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface NodeDataSourceModule {
@Binds @Singleton
fun bindNodeInfoReadDataSource(impl: SwitchingNodeInfoReadDataSource): NodeInfoReadDataSource
@Binds @Singleton
fun bindNodeInfoWriteDataSource(impl: SwitchingNodeInfoWriteDataSource): NodeInfoWriteDataSource
}

View File

@@ -1,269 +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 org.meshtastic.app.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.prefs.analytics.AnalyticsPrefsImpl
import org.meshtastic.core.prefs.di.AnalyticsDataStore
import org.meshtastic.core.prefs.di.AppDataStore
import org.meshtastic.core.prefs.di.CustomEmojiDataStore
import org.meshtastic.core.prefs.di.FilterDataStore
import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
import org.meshtastic.core.prefs.di.MapConsentDataStore
import org.meshtastic.core.prefs.di.MapDataStore
import org.meshtastic.core.prefs.di.MapTileProviderDataStore
import org.meshtastic.core.prefs.di.MeshDataStore
import org.meshtastic.core.prefs.di.MeshLogDataStore
import org.meshtastic.core.prefs.di.RadioDataStore
import org.meshtastic.core.prefs.di.UiDataStore
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefsImpl
import org.meshtastic.core.prefs.filter.FilterPrefsImpl
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl
import org.meshtastic.core.prefs.map.MapConsentPrefsImpl
import org.meshtastic.core.prefs.map.MapPrefsImpl
import org.meshtastic.core.prefs.map.MapTileProviderPrefsImpl
import org.meshtastic.core.prefs.mesh.MeshPrefsImpl
import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl
import org.meshtastic.core.prefs.radio.RadioPrefsImpl
import org.meshtastic.core.prefs.ui.UiPrefsImpl
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.CustomEmojiPrefs
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MapConsentPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.MapTileProviderPrefs
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshPrefs
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.UiPrefs
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class AnalyticsDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class HomoglyphEncodingDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class AppDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class CustomEmojiDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapConsentDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapTileProviderDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MeshDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class RadioDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class UiDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MeshLogDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class FilterDataStore
@Suppress("TooManyFunctions")
@InstallIn(SingletonComponent::class)
@Module
interface PrefsModule {
@Binds fun bindAnalyticsPrefs(analyticsPrefsImpl: AnalyticsPrefsImpl): AnalyticsPrefs
@Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs
@Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs
@Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs
@Binds fun bindMapPrefs(mapPrefsImpl: MapPrefsImpl): MapPrefs
@Binds fun bindMapTileProviderPrefs(mapTileProviderPrefsImpl: MapTileProviderPrefsImpl): MapTileProviderPrefs
@Binds fun bindMeshPrefs(meshPrefsImpl: MeshPrefsImpl): MeshPrefs
@Binds fun bindMeshLogPrefs(meshLogPrefsImpl: MeshLogPrefsImpl): MeshLogPrefs
@Binds fun bindRadioPrefs(radioPrefsImpl: RadioPrefsImpl): RadioPrefs
@Binds fun bindUiPrefs(uiPrefsImpl: UiPrefsImpl): UiPrefs
@Binds fun bindFilterPrefs(filterPrefsImpl: FilterPrefsImpl): FilterPrefs
companion object {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Provides
@Singleton
@AnalyticsDataStore
fun provideAnalyticsDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("analytics_ds") },
)
@Provides
@Singleton
@HomoglyphEncodingDataStore
fun provideHomoglyphEncodingDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") },
)
@Provides
@Singleton
@AppDataStore
fun provideAppDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("app_ds") },
)
@Provides
@Singleton
@CustomEmojiDataStore
fun provideCustomEmojiDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") },
)
@Provides
@Singleton
@MapDataStore
fun provideMapDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_ds") },
)
@Provides
@Singleton
@MapConsentDataStore
fun provideMapConsentDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_consent_ds") },
)
@Provides
@Singleton
@MapTileProviderDataStore
fun provideMapTileProviderDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") },
)
@Provides
@Singleton
@MeshDataStore
fun provideMeshDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("mesh_ds") },
)
@Provides
@Singleton
@RadioDataStore
fun provideRadioDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("radio_ds") },
)
@Provides
@Singleton
@UiDataStore
fun provideUiDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("ui_ds") },
)
@Provides
@Singleton
@MeshLogDataStore
fun provideMeshLogDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("meshlog_ds") },
)
@Provides
@Singleton
@FilterDataStore
fun provideFilterDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("filter_ds") },
)
}
}

View File

@@ -1,163 +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 org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.manager.CommandSenderImpl
import org.meshtastic.core.data.manager.FromRadioPacketHandlerImpl
import org.meshtastic.core.data.manager.HistoryManagerImpl
import org.meshtastic.core.data.manager.MeshActionHandlerImpl
import org.meshtastic.core.data.manager.MeshConfigFlowManagerImpl
import org.meshtastic.core.data.manager.MeshConfigHandlerImpl
import org.meshtastic.core.data.manager.MeshConnectionManagerImpl
import org.meshtastic.core.data.manager.MeshDataHandlerImpl
import org.meshtastic.core.data.manager.MeshMessageProcessorImpl
import org.meshtastic.core.data.manager.MeshRouterImpl
import org.meshtastic.core.data.manager.MessageFilterImpl
import org.meshtastic.core.data.manager.MqttManagerImpl
import org.meshtastic.core.data.manager.NeighborInfoHandlerImpl
import org.meshtastic.core.data.manager.NodeManagerImpl
import org.meshtastic.core.data.manager.PacketHandlerImpl
import org.meshtastic.core.data.manager.TracerouteHandlerImpl
import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl
import org.meshtastic.core.data.repository.LocationRepositoryImpl
import org.meshtastic.core.data.repository.MeshLogRepositoryImpl
import org.meshtastic.core.data.repository.NodeRepositoryImpl
import org.meshtastic.core.data.repository.PacketRepositoryImpl
import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl
import org.meshtastic.core.model.util.MeshDataMapper
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.FromRadioPacketHandler
import org.meshtastic.core.repository.HistoryManager
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshActionHandler
import org.meshtastic.core.repository.MeshConfigFlowManager
import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.MeshDataHandler
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.MeshMessageProcessor
import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MessageFilter
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.NeighborInfoHandler
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.TracerouteHandler
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds @Singleton
abstract fun bindNodeRepository(nodeRepositoryImpl: NodeRepositoryImpl): NodeRepository
@Binds
@Singleton
abstract fun bindRadioConfigRepository(radioConfigRepositoryImpl: RadioConfigRepositoryImpl): RadioConfigRepository
@Binds
@Singleton
abstract fun bindLocationRepository(locationRepositoryImpl: LocationRepositoryImpl): LocationRepository
@Binds
@Singleton
abstract fun bindDeviceHardwareRepository(
deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl,
): DeviceHardwareRepository
@Binds @Singleton
abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository
@Binds
@Singleton
abstract fun bindMeshLogRepository(meshLogRepositoryImpl: MeshLogRepositoryImpl): MeshLogRepository
@Binds @Singleton
abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager
@Binds @Singleton
abstract fun bindCommandSender(commandSenderImpl: CommandSenderImpl): CommandSender
@Binds @Singleton
abstract fun bindHistoryManager(historyManagerImpl: HistoryManagerImpl): HistoryManager
@Binds
@Singleton
abstract fun bindTracerouteHandler(tracerouteHandlerImpl: TracerouteHandlerImpl): TracerouteHandler
@Binds
@Singleton
abstract fun bindNeighborInfoHandler(neighborInfoHandlerImpl: NeighborInfoHandlerImpl): NeighborInfoHandler
@Binds @Singleton
abstract fun bindMqttManager(mqttManagerImpl: MqttManagerImpl): MqttManager
@Binds @Singleton
abstract fun bindPacketHandler(packetHandlerImpl: PacketHandlerImpl): PacketHandler
@Binds
@Singleton
abstract fun bindMeshConnectionManager(meshConnectionManagerImpl: MeshConnectionManagerImpl): MeshConnectionManager
@Binds @Singleton
abstract fun bindMeshDataHandler(meshDataHandlerImpl: MeshDataHandlerImpl): MeshDataHandler
@Binds
@Singleton
abstract fun bindMeshActionHandler(meshActionHandlerImpl: MeshActionHandlerImpl): MeshActionHandler
@Binds
@Singleton
abstract fun bindMeshMessageProcessor(meshMessageProcessorImpl: MeshMessageProcessorImpl): MeshMessageProcessor
@Binds @Singleton
abstract fun bindMeshRouter(meshRouterImpl: MeshRouterImpl): MeshRouter
@Binds
@Singleton
abstract fun bindFromRadioPacketHandler(
fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl,
): FromRadioPacketHandler
@Binds
@Singleton
abstract fun bindMeshConfigHandler(meshConfigHandlerImpl: MeshConfigHandlerImpl): MeshConfigHandler
@Binds
@Singleton
abstract fun bindMeshConfigFlowManager(meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl): MeshConfigFlowManager
@Binds @Singleton
abstract fun bindMessageFilter(messageFilterImpl: MessageFilterImpl): MessageFilter
companion object {
@Provides
@Singleton
fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager)
}
}

View File

@@ -1,38 +0,0 @@
/*
* 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 dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.service.AndroidRadioControllerImpl
import org.meshtastic.core.service.AndroidServiceRepository
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds @Singleton
abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController
@Binds @Singleton
abstract fun bindServiceRepository(impl: AndroidServiceRepository): ServiceRepository
}

View File

@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.Single
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.model.getMeshtasticShortName
import org.meshtastic.app.repository.network.NetworkRepository
@@ -37,7 +38,6 @@ import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.meshtastic
import java.util.Locale
import javax.inject.Inject
data class DiscoveredDevices(
val bleDevices: List<DeviceListEntry>,
@@ -47,9 +47,8 @@ data class DiscoveredDevices(
)
@Suppress("LongParameterList")
class GetDiscoveredDevicesUseCase
@Inject
constructor(
@Single
class GetDiscoveredDevicesUseCase(
private val bluetoothRepository: BluetoothRepository,
private val networkRepository: NetworkRepository,
private val recentAddressesDataSource: RecentAddressesDataSource,
@@ -57,7 +56,7 @@ constructor(
private val databaseManager: DatabaseManager,
private val usbRepository: UsbRepository,
private val radioInterfaceService: RadioInterfaceService,
private val usbManagerLazy: dagger.Lazy<UsbManager>,
private val usbManagerLazy: Lazy<UsbManager>,
) {
private val suffixLength = 4
@@ -94,7 +93,7 @@ constructor(
val usbDevicesFlow =
usbRepository.serialDevices.map { usb ->
usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.get(), d) }
usb.map { (_, d) -> DeviceListEntry.Usb(radioInterfaceService, usbManagerLazy.value, d) }
}
return combine(

View File

@@ -0,0 +1,53 @@
/*
* 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.firmware
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.datastore.BootloaderWarningDataSource
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.feature.firmware.FirmwareFileHandler
import org.meshtastic.feature.firmware.FirmwareUpdateManager
import org.meshtastic.feature.firmware.FirmwareUpdateViewModel
import org.meshtastic.feature.firmware.FirmwareUsbManager
@Suppress("LongParameterList")
@KoinViewModel
class AndroidFirmwareUpdateViewModel(
firmwareReleaseRepository: FirmwareReleaseRepository,
deviceHardwareRepository: DeviceHardwareRepository,
nodeRepository: NodeRepository,
radioController: RadioController,
radioPrefs: RadioPrefs,
bootloaderWarningDataSource: BootloaderWarningDataSource,
firmwareUpdateManager: FirmwareUpdateManager,
usbManager: FirmwareUsbManager,
fileHandler: FirmwareFileHandler,
) : FirmwareUpdateViewModel(
firmwareReleaseRepository,
deviceHardwareRepository,
nodeRepository,
radioController,
radioPrefs,
bootloaderWarningDataSource,
firmwareUpdateManager,
usbManager,
fileHandler,
)

View File

@@ -16,9 +16,8 @@
*/
package org.meshtastic.app.intro
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.feature.intro.IntroViewModel
import javax.inject.Inject
/** Android-specific Hilt wrapper for IntroViewModel. */
@HiltViewModel class AndroidIntroViewModel @Inject constructor() : IntroViewModel()
/** Android-specific Koin wrapper for IntroViewModel. */
@KoinViewModel class AndroidIntroViewModel : IntroViewModel()

View File

@@ -16,18 +16,15 @@
*/
package org.meshtastic.app.map
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koin.core.annotation.KoinViewModel
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.feature.map.SharedMapViewModel
import javax.inject.Inject
@HiltViewModel
class AndroidSharedMapViewModel
@Inject
constructor(
@KoinViewModel
class AndroidSharedMapViewModel(
mapPrefs: MapPrefs,
nodeRepository: NodeRepository,
packetRepository: PacketRepository,

View File

@@ -19,7 +19,6 @@ package org.meshtastic.app.map.node
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.navigation.toRoute
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.toList
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.navigation.NodesRoutes
@@ -37,12 +37,9 @@ import org.meshtastic.core.ui.util.toPosition
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Position
import javax.inject.Inject
@HiltViewModel
class NodeMapViewModel
@Inject
constructor(
@KoinViewModel
class NodeMapViewModel(
savedStateHandle: SavedStateHandle,
nodeRepository: NodeRepository,
meshLogRepository: MeshLogRepository,

View File

@@ -16,18 +16,15 @@
*/
package org.meshtastic.app.messaging
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
import javax.inject.Inject
@HiltViewModel
class AndroidContactsViewModel
@Inject
constructor(
@KoinViewModel
class AndroidContactsViewModel(
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
radioConfigRepository: RadioConfigRepository,

View File

@@ -17,7 +17,7 @@
package org.meshtastic.app.messaging
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.repository.CustomEmojiPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
@@ -29,13 +29,10 @@ import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import org.meshtastic.feature.messaging.MessageViewModel
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
class AndroidMessageViewModel
@Inject
constructor(
@KoinViewModel
class AndroidMessageViewModel(
savedStateHandle: SavedStateHandle,
nodeRepository: NodeRepository,
radioConfigRepository: RadioConfigRepository,

View File

@@ -16,11 +16,10 @@
*/
package org.meshtastic.app.messaging
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.feature.messaging.QuickChatViewModel
import javax.inject.Inject
@HiltViewModel
class AndroidQuickChatViewModel @Inject constructor(quickChatActionRepository: QuickChatActionRepository) :
@KoinViewModel
class AndroidQuickChatViewModel(quickChatActionRepository: QuickChatActionRepository) :
QuickChatViewModel(quickChatActionRepository)

View File

@@ -17,22 +17,18 @@
package org.meshtastic.app.messaging.domain.worker
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.koin.android.annotation.KoinWorker
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.PacketRepository
@HiltWorker
class SendMessageWorker
@AssistedInject
constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
@KoinWorker
class SendMessageWorker(
context: Context,
params: WorkerParameters,
private val packetRepository: PacketRepository,
private val radioController: RadioController,
) : CoroutineWorker(context, params) {

View File

@@ -20,13 +20,12 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.MessageQueue
import javax.inject.Inject
import javax.inject.Singleton
/** Android implementation of [MessageQueue] that uses [WorkManager] for reliable background transmission. */
@Singleton
class WorkManagerMessageQueue @Inject constructor(private val workManager: WorkManager) : MessageQueue {
@Single
class WorkManagerMessageQueue(private val workManager: WorkManager) : MessageQueue {
override suspend fun enqueue(packetId: Int) {
val workRequest =

View File

@@ -20,7 +20,6 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -35,6 +34,7 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.datastore.UiPreferencesDataSource
@@ -62,13 +62,10 @@ import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.SharedContact
import javax.inject.Inject
@HiltViewModel
@KoinViewModel
@Suppress("LongParameterList", "TooManyFunctions")
class UIViewModel
@Inject
constructor(
class UIViewModel(
private val nodeDB: NodeRepository,
private val serviceRepository: AndroidServiceRepository,
private val radioController: RadioController,

View File

@@ -17,12 +17,13 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
import org.meshtastic.app.ui.sharing.ChannelScreen
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -38,7 +39,7 @@ fun NavGraphBuilder.channelsGraph(navController: NavHostController) {
) { backStackEntry ->
val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(ChannelsRoutes.ChannelsGraph) }
ChannelScreen(
radioConfigViewModel = hiltViewModel(parentEntry),
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
onNavigate = { route -> navController.navigate(route) },
onNavigateUp = { navController.navigateUp() },
)

View File

@@ -17,12 +17,13 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
import org.meshtastic.app.ui.connections.ConnectionsScreen
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -42,7 +43,7 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
ConnectionsScreen(
radioConfigViewModel = hiltViewModel(parentEntry),
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
onClickNodeChip = {
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
launchSingleTop = true

View File

@@ -17,7 +17,6 @@
package org.meshtastic.app.navigation
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
@@ -26,6 +25,7 @@ import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.Flow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.messaging.AndroidContactsViewModel
import org.meshtastic.app.messaging.AndroidMessageViewModel
import org.meshtastic.app.messaging.AndroidQuickChatViewModel
@@ -43,11 +43,11 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
composable<ContactsRoutes.Contacts>(
deepLinks = listOf(navDeepLink<ContactsRoutes.Contacts>(basePath = "$DEEP_LINK_BASE_URI/contacts")),
) {
val uiViewModel: UIViewModel = hiltViewModel()
val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
val contactsViewModel = hiltViewModel<AndroidContactsViewModel>()
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
AdaptiveContactsScreen(
navController = navController,
@@ -71,11 +71,11 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
),
) { backStackEntry ->
val args = backStackEntry.toRoute<ContactsRoutes.Messages>()
val uiViewModel: UIViewModel = hiltViewModel()
val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
val contactsViewModel = hiltViewModel<AndroidContactsViewModel>()
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
AdaptiveContactsScreen(
navController = navController,
@@ -101,7 +101,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
),
) { backStackEntry ->
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
val viewModel = hiltViewModel<AndroidContactsViewModel>()
val viewModel = koinViewModel<AndroidContactsViewModel>()
ShareScreen(
viewModel = viewModel,
onConfirm = {
@@ -115,7 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
composable<ContactsRoutes.QuickChat>(
deepLinks = listOf(navDeepLink<ContactsRoutes.QuickChat>(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
) {
val viewModel = hiltViewModel<AndroidQuickChatViewModel>()
val viewModel = koinViewModel<AndroidQuickChatViewModel>()
QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
}
}

View File

@@ -19,12 +19,17 @@ package org.meshtastic.app.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import androidx.navigation.compose.navigation
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.firmware.AndroidFirmwareUpdateViewModel
import org.meshtastic.core.navigation.FirmwareRoutes
import org.meshtastic.feature.firmware.FirmwareUpdateScreen
fun NavGraphBuilder.firmwareGraph(navController: NavController) {
navigation<FirmwareRoutes.FirmwareGraph>(startDestination = FirmwareRoutes.FirmwareUpdate) {
composable<FirmwareRoutes.FirmwareUpdate> { FirmwareUpdateScreen(navController) }
composable<FirmwareRoutes.FirmwareUpdate> {
val viewModel = koinViewModel<AndroidFirmwareUpdateViewModel>()
FirmwareUpdateScreen(onNavigateUp = { navController.navigateUp() }, viewModel = viewModel)
}
}
}

View File

@@ -16,11 +16,11 @@
*/
package org.meshtastic.app.navigation
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.AndroidSharedMapViewModel
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.MapRoutes
@@ -29,7 +29,7 @@ import org.meshtastic.feature.map.MapScreen
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
composable<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
val viewModel = hiltViewModel<AndroidSharedMapViewModel>()
val viewModel = koinViewModel<AndroidSharedMapViewModel>()
MapScreen(
viewModel = viewModel,
onClickNodeChip = {

View File

@@ -29,7 +29,6 @@ import androidx.compose.material.icons.rounded.Router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraphBuilder
@@ -40,8 +39,10 @@ import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.Flow
import org.jetbrains.compose.resources.StringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.map.node.NodeMapScreen
import org.meshtastic.app.map.node.NodeMapViewModel
import org.meshtastic.app.node.AndroidMetricsViewModel
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@@ -120,7 +121,7 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
val vm = hiltViewModel<NodeMapViewModel>(parentGraphBackStackEntry)
val vm = koinViewModel<NodeMapViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
NodeMapScreen(vm, onNavigateUp = navController::navigateUp)
}
@@ -135,7 +136,8 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
val metricsViewModel = hiltViewModel<MetricsViewModel>(parentGraphBackStackEntry)
val metricsViewModel =
koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute<NodeDetailRoutes.TracerouteLog>()
metricsViewModel.setNodeId(args.destNum)
@@ -166,7 +168,8 @@ fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTo
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
val metricsViewModel = hiltViewModel<MetricsViewModel>(parentGraphBackStackEntry)
val metricsViewModel =
koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute<NodeDetailRoutes.TracerouteMap>()
metricsViewModel.setNodeId(args.destNum)
@@ -277,7 +280,7 @@ private inline fun <reified R : Route> NavGraphBuilder.addNodeDetailScreenCompos
) { backStackEntry ->
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
val metricsViewModel = hiltViewModel<MetricsViewModel>(parentGraphBackStackEntry)
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
val args = backStackEntry.toRoute<R>()
val destNum = getDestNum(args)

View File

@@ -22,13 +22,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.settings.AndroidCleanNodeDatabaseViewModel
import org.meshtastic.app.settings.AndroidDebugViewModel
import org.meshtastic.app.settings.AndroidFilterSettingsViewModel
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
import org.meshtastic.app.settings.AndroidSettingsViewModel
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.Graph
import org.meshtastic.core.navigation.NodesRoutes
@@ -39,13 +44,11 @@ import org.meshtastic.feature.settings.AdministrationScreen
import org.meshtastic.feature.settings.DeviceConfigurationScreen
import org.meshtastic.feature.settings.ModuleConfigurationScreen
import org.meshtastic.feature.settings.SettingsScreen
import org.meshtastic.feature.settings.SettingsViewModel
import org.meshtastic.feature.settings.debugging.DebugScreen
import org.meshtastic.feature.settings.filter.FilterSettingsScreen
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
import org.meshtastic.feature.settings.radio.CleanNodeDatabaseScreen
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.channel.ChannelConfigScreen
import org.meshtastic.feature.settings.radio.component.AmbientLightingConfigScreen
import org.meshtastic.feature.settings.radio.component.AudioConfigScreen
@@ -83,8 +86,8 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
SettingsScreen(
settingsViewModel = hiltViewModel(parentEntry),
viewModel = hiltViewModel(parentEntry),
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(viewModelStoreOwner = parentEntry),
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
onClickNodeChip = {
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
launchSingleTop = true
@@ -100,7 +103,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
DeviceConfigurationScreen(
viewModel = hiltViewModel(parentEntry),
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
onBack = navController::popBackStack,
onNavigate = { route -> navController.navigate(route) },
)
@@ -109,10 +112,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
composable<SettingsRoutes.ModuleConfiguration> { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
val settingsViewModel: SettingsViewModel = hiltViewModel(parentEntry)
val settingsViewModel: AndroidSettingsViewModel = koinViewModel(viewModelStoreOwner = parentEntry)
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
ModuleConfigurationScreen(
viewModel = hiltViewModel(parentEntry),
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
excludedModulesUnlocked = excludedModulesUnlocked,
onBack = navController::popBackStack,
onNavigate = { route -> navController.navigate(route) },
@@ -122,7 +125,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
composable<SettingsRoutes.Administration> { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
AdministrationScreen(viewModel = hiltViewModel(parentEntry), onBack = navController::popBackStack)
AdministrationScreen(
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
onBack = navController::popBackStack,
)
}
composable<SettingsRoutes.CleanNodeDb>(
@@ -133,7 +139,8 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
),
),
) {
CleanNodeDatabaseScreen()
val viewModel: AndroidCleanNodeDatabaseViewModel = koinViewModel()
CleanNodeDatabaseScreen(viewModel = viewModel)
}
ConfigRoute.entries.forEach { entry ->
@@ -221,18 +228,22 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
deepLinks =
listOf(navDeepLink<SettingsRoutes.DebugPanel>(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")),
) {
DebugScreen(onNavigateUp = navController::navigateUp)
val viewModel: AndroidDebugViewModel = koinViewModel()
DebugScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
}
composable<SettingsRoutes.About> { AboutScreen(onNavigateUp = navController::navigateUp) }
composable<SettingsRoutes.FilterSettings> { FilterSettingsScreen(onBack = navController::navigateUp) }
composable<SettingsRoutes.FilterSettings> {
val viewModel: AndroidFilterSettingsViewModel = koinViewModel()
FilterSettingsScreen(viewModel = viewModel, onBack = navController::navigateUp)
}
}
}
context(_: NavGraphBuilder)
inline fun <reified R : Route, reified G : Graph> NavHostController.configComposable(
noinline content: @Composable (RadioConfigViewModel) -> Unit,
noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
) {
configComposable(route = R::class, parentGraphRoute = G::class, content = content)
}
@@ -241,10 +252,10 @@ context(navGraphBuilder: NavGraphBuilder)
fun <R : Route, G : Graph> NavHostController.configComposable(
route: KClass<R>,
parentGraphRoute: KClass<G>,
content: @Composable (RadioConfigViewModel) -> Unit,
content: @Composable (AndroidRadioConfigViewModel) -> Unit,
) {
navGraphBuilder.composable(route = route) { backStackEntry ->
val parentEntry = remember(backStackEntry) { getBackStackEntry(parentGraphRoute) }
content(hiltViewModel(parentEntry))
content(koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry))
}
}

View File

@@ -14,23 +14,19 @@
* 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
package org.meshtastic.app.node
import android.content.Context
import android.location.LocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.feature.node.compass.CompassHeadingProvider
import org.meshtastic.feature.node.compass.CompassViewModel
import org.meshtastic.feature.node.compass.MagneticFieldProvider
import org.meshtastic.feature.node.compass.PhoneLocationProvider
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideLocationManager(@ApplicationContext context: Context): LocationManager =
context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@KoinViewModel
class AndroidCompassViewModel(
headingProvider: CompassHeadingProvider,
locationProvider: PhoneLocationProvider,
magneticFieldProvider: MagneticFieldProvider,
dispatchers: CoroutineDispatchers,
) : CompassViewModel(headingProvider, locationProvider, magneticFieldProvider, dispatchers)

View File

@@ -0,0 +1,115 @@
/*
* 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
import android.app.Application
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
import org.meshtastic.feature.node.metrics.MetricsViewModel
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.Locale
@KoinViewModel
class AndroidMetricsViewModel(
savedStateHandle: SavedStateHandle,
private val app: Application,
dispatchers: CoroutineDispatchers,
meshLogRepository: MeshLogRepository,
serviceRepository: ServiceRepository,
nodeRepository: NodeRepository,
tracerouteSnapshotRepository: TracerouteSnapshotRepository,
nodeRequestActions: NodeRequestActions,
alertManager: AlertManager,
getNodeDetailsUseCase: GetNodeDetailsUseCase,
) : MetricsViewModel(
savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum ?: 0,
dispatchers,
meshLogRepository,
serviceRepository,
nodeRepository,
tracerouteSnapshotRepository,
nodeRequestActions,
alertManager,
getNodeDetailsUseCase,
) {
override fun savePositionCSV(uri: Any) {
if (uri is Uri) {
savePositionCSVAndroid(uri)
}
}
private fun savePositionCSVAndroid(uri: Uri) = viewModelScope.launch(dispatchers.main) {
val positions = state.value.positionLogs
writeToUri(uri) { writer ->
writer.appendLine(
"\"date\",\"time\",\"latitude\",\"longitude\",\"altitude\",\"satsInView\",\"speed\",\"heading\"",
)
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
positions.forEach { position ->
val rxDateTime = dateFormat.format((position.time.toLong() * 1000L).toInstant().toDate())
val latitude = (position.latitude_i ?: 0) * 1e-7
val longitude = (position.longitude_i ?: 0) * 1e-7
val altitude = position.altitude
val satsInView = position.sats_in_view
val speed = position.ground_speed
val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
writer.appendLine(
"$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
)
}
}
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) =
withContext(dispatchers.io) {
try {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
BufferedWriter(fileWriter).use { writer -> block.invoke(writer) }
}
}
} catch (ex: FileNotFoundException) {
Logger.e(ex) { "Can't write file error" }
}
}
override fun decodeBase64(base64: String): ByteArray =
android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
}

View File

@@ -0,0 +1,40 @@
/*
* 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
import androidx.lifecycle.SavedStateHandle
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.feature.node.detail.NodeDetailViewModel
import org.meshtastic.feature.node.detail.NodeManagementActions
import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
@KoinViewModel
class AndroidNodeDetailViewModel(
savedStateHandle: SavedStateHandle,
nodeManagementActions: NodeManagementActions,
nodeRequestActions: NodeRequestActions,
serviceRepository: ServiceRepository,
getNodeDetailsUseCase: GetNodeDetailsUseCase,
) : NodeDetailViewModel(
savedStateHandle,
nodeManagementActions,
nodeRequestActions,
serviceRepository,
getNodeDetailsUseCase,
)

View File

@@ -0,0 +1,49 @@
/*
* 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
import androidx.lifecycle.SavedStateHandle
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.feature.node.detail.NodeManagementActions
import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
import org.meshtastic.feature.node.list.NodeFilterPreferences
import org.meshtastic.feature.node.list.NodeListViewModel
@KoinViewModel
class AndroidNodeListViewModel(
savedStateHandle: SavedStateHandle,
nodeRepository: NodeRepository,
radioConfigRepository: RadioConfigRepository,
serviceRepository: ServiceRepository,
radioController: RadioController,
nodeManagementActions: NodeManagementActions,
getFilteredNodesUseCase: GetFilteredNodesUseCase,
nodeFilterPreferences: NodeFilterPreferences,
) : NodeListViewModel(
savedStateHandle,
nodeRepository,
radioConfigRepository,
serviceRepository,
radioController,
nodeManagementActions,
getFilteredNodesUseCase,
nodeFilterPreferences,
)

View File

@@ -27,24 +27,20 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.shareIn
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NetworkRepository
@Inject
constructor(
private val nsdManagerLazy: dagger.Lazy<NsdManager>,
private val connectivityManager: dagger.Lazy<ConnectivityManager>,
@Single
class NetworkRepository(
private val nsdManager: NsdManager,
private val connectivityManager: ConnectivityManager,
private val dispatchers: CoroutineDispatchers,
@ProcessLifecycle private val processLifecycle: Lifecycle,
@Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
) {
val networkAvailable: Flow<Boolean> by lazy {
connectivityManager
.get()
.networkAvailable()
.flowOn(dispatchers.io)
.conflate()
@@ -57,8 +53,7 @@ constructor(
}
val resolvedList: Flow<List<NsdServiceInfo>> by lazy {
nsdManagerLazy
.get()
nsdManager
.serviceList(SERVICE_TYPE)
.flowOn(dispatchers.io)
.conflate()

View File

@@ -1,40 +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 org.meshtastic.app.repository.network
import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.nsd.NsdManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class NetworkRepositoryModule {
companion object {
@Provides
fun provideConnectivityManager(application: Application): ConnectivityManager =
application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@Provides
fun provideNsdManager(application: Application): NsdManager =
application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
}

View File

@@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.core.ble.BluetoothRepository
@@ -44,7 +46,6 @@ import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toRemoteExceptions
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.model.MeshActivity
@@ -54,8 +55,6 @@ import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
import javax.inject.Inject
import javax.inject.Singleton
/**
* Handles the bluetooth link with a mesh radio device. Does not cache any device state, just does bluetooth comms
@@ -67,17 +66,15 @@ import javax.inject.Singleton
* can be stubbed out with a simulated version as needed.
*/
@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
class AndroidRadioInterfaceService
@Inject
constructor(
@Single
class AndroidRadioInterfaceService(
private val context: Application,
private val dispatchers: CoroutineDispatchers,
private val bluetoothRepository: BluetoothRepository,
private val networkRepository: NetworkRepository,
@ProcessLifecycle private val processLifecycle: Lifecycle,
@Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
private val radioPrefs: RadioPrefs,
private val interfaceFactory: InterfaceFactory,
private val interfaceFactory: Lazy<InterfaceFactory>,
private val analytics: PlatformAnalytics,
) : RadioInterfaceService {
@@ -179,7 +176,7 @@ constructor(
/** Constructs a full radio address for the specific interface type. */
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
interfaceFactory.toInterfaceAddress(interfaceId, rest)
interfaceFactory.value.toInterfaceAddress(interfaceId, rest)
override fun isMockInterface(): Boolean =
BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
@@ -200,7 +197,7 @@ constructor(
fun getBondedDeviceAddress(): String? {
// If the user has unpaired our device, treat things as if we don't have one
val address = getDeviceAddress()
return if (interfaceFactory.addressValid(address)) {
return if (interfaceFactory.value.addressValid(address)) {
address
} else {
null
@@ -259,24 +256,32 @@ constructor(
if (radioIf !is NopInterface) {
// Already running
return
}
val isTestLab = Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
val address =
getBondedDeviceAddress()
?: if (isTestLab) {
mockInterfaceAddress
} else {
null
}
if (address == null) {
Logger.w { "No bonded mesh radio, can't start interface" }
} else {
val address = getBondedDeviceAddress()
if (address == null) {
Logger.w { "No bonded mesh radio, can't start interface" }
} else {
Logger.i { "Starting radio ${address.anonymize}" }
isStarted = true
Logger.i { "Starting radio ${address.anonymize}" }
isStarted = true
if (logSends) {
sentPacketsLog = BinaryLogFile(context, "sent_log.pb")
}
if (logReceives) {
receivedPacketsLog = BinaryLogFile(context, "receive_log.pb")
}
radioIf = interfaceFactory.createInterface(address)
startHeartbeat()
if (logSends) {
sentPacketsLog = BinaryLogFile(context, "sent_log.pb")
}
if (logReceives) {
receivedPacketsLog = BinaryLogFile(context, "receive_log.pb")
}
radioIf = interfaceFactory.value.createInterface(address, this)
startHeartbeat()
}
}
@@ -297,7 +302,7 @@ constructor(
val r = radioIf
Logger.i { "stopping interface $r" }
isStarted = false
radioIf = interfaceFactory.nopInterface
radioIf = interfaceFactory.value.nopInterface
r.close()
// cancel any old jobs and get ready for the new ones

View File

@@ -16,9 +16,9 @@
*/
package org.meshtastic.app.repository.radio
import org.koin.core.annotation.Single
import org.meshtastic.core.model.InterfaceId
import javax.inject.Inject
import javax.inject.Provider
import org.meshtastic.core.repository.RadioInterfaceService
/**
* Entry point for create radio backend instances given a specific address.
@@ -26,19 +26,31 @@ import javax.inject.Provider
* This class is responsible for building and dissecting radio addresses based upon their interface type and the "rest"
* of the address (which varies per implementation).
*/
class InterfaceFactory
@Inject
constructor(
@Single
class InterfaceFactory(
private val nopInterfaceFactory: NopInterfaceFactory,
private val specMap: Map<InterfaceId, @JvmSuppressWildcards Provider<InterfaceSpec<*>>>,
private val bluetoothSpec: Lazy<NordicBleInterfaceSpec>,
private val mockSpec: Lazy<MockInterfaceSpec>,
private val serialSpec: Lazy<SerialInterfaceSpec>,
private val tcpSpec: Lazy<TCPInterfaceSpec>,
) {
internal val nopInterface by lazy { nopInterfaceFactory.create("") }
private val specMap: Map<InterfaceId, InterfaceSpec<*>>
get() =
mapOf(
InterfaceId.BLUETOOTH to bluetoothSpec.value,
InterfaceId.MOCK to mockSpec.value,
InterfaceId.NOP to NopInterfaceSpec(nopInterfaceFactory),
InterfaceId.SERIAL to serialSpec.value,
InterfaceId.TCP to tcpSpec.value,
)
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest"
fun createInterface(address: String): IRadioInterface {
fun createInterface(address: String, service: RadioInterfaceService): IRadioInterface {
val (spec, rest) = splitAddress(address)
return spec?.createInterface(rest) ?: nopInterface
return spec?.createInterface(rest, service) ?: nopInterface
}
fun addressValid(address: String?): Boolean = address?.let {
@@ -47,7 +59,7 @@ constructor(
} ?: false
private fun splitAddress(address: String): Pair<InterfaceSpec<*>?, String> {
val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() }
val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it] }
val rest = address.substring(1)
return Pair(c, rest)
}

View File

@@ -16,9 +16,11 @@
*/
package org.meshtastic.app.repository.radio
import org.meshtastic.core.repository.RadioInterfaceService
/** This interface defines the contract that all radio backend implementations must adhere to. */
interface InterfaceSpec<T : IRadioInterface> {
fun createInterface(rest: String): T
fun createInterface(rest: String, service: RadioInterfaceService): T
/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid(rest: String): Boolean = true

View File

@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
@@ -58,12 +56,7 @@ private val defaultChannel = ProtoChannel(settings = Channel.default.settings, r
/** A simulated interface that is used for testing in the simulator */
@Suppress("detekt:TooManyFunctions", "detekt:MagicNumber")
class MockInterface
@AssistedInject
constructor(
private val service: RadioInterfaceService,
@Assisted val address: String,
) : IRadioInterface {
class MockInterface(private val service: RadioInterfaceService, val address: String) : IRadioInterface {
companion object {
private const val MY_NODE = 0x42424242

View File

@@ -16,10 +16,11 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `MockInterface` instances. */
@AssistedFactory
interface MockInterfaceFactory {
fun create(rest: String): MockInterface
@Single
class MockInterfaceFactory {
fun create(rest: String, service: RadioInterfaceService): MockInterface = MockInterface(service, rest)
}

View File

@@ -16,11 +16,14 @@
*/
package org.meshtastic.app.repository.radio
import javax.inject.Inject
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.RadioInterfaceService
/** Mock interface backend implementation. */
class MockInterfaceSpec @Inject constructor(private val factory: MockInterfaceFactory) : InterfaceSpec<MockInterface> {
override fun createInterface(rest: String): MockInterface = factory.create(rest)
@Single
class MockInterfaceSpec(private val factory: MockInterfaceFactory) : InterfaceSpec<MockInterface> {
override fun createInterface(rest: String, service: RadioInterfaceService): MockInterface =
factory.create(rest, service)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean = true

View File

@@ -16,10 +16,7 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
class NopInterface @AssistedInject constructor(@Assisted val address: String) : IRadioInterface {
class NopInterface(val address: String) : IRadioInterface {
override fun handleSendToRadio(p: ByteArray) {
// No-op
}

View File

@@ -16,10 +16,10 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
import org.koin.core.annotation.Single
/** Factory for creating `NopInterface` instances. */
@AssistedFactory
interface NopInterfaceFactory {
fun create(rest: String): NopInterface
@Single
class NopInterfaceFactory {
fun create(rest: String): NopInterface = NopInterface(rest)
}

View File

@@ -16,9 +16,11 @@
*/
package org.meshtastic.app.repository.radio
import javax.inject.Inject
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.RadioInterfaceService
/** No-op interface backend implementation. */
class NopInterfaceSpec @Inject constructor(private val factory: NopInterfaceFactory) : InterfaceSpec<NopInterface> {
override fun createInterface(rest: String): NopInterface = factory.create(rest)
@Single
class NopInterfaceSpec(private val factory: NopInterfaceFactory) : InterfaceSpec<NopInterface> {
override fun createInterface(rest: String, service: RadioInterfaceService): NopInterface = factory.create(rest)
}

View File

@@ -18,8 +18,6 @@ package org.meshtastic.app.repository.radio
import android.annotation.SuppressLint
import co.touchlab.kermit.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -72,15 +70,13 @@ private val SCAN_TIMEOUT = 5.seconds
* @param address The BLE address of the device to connect to.
*/
@SuppressLint("MissingPermission")
class NordicBleInterface
@AssistedInject
constructor(
class NordicBleInterface(
private val serviceScope: CoroutineScope,
private val scanner: BleScanner,
private val bluetoothRepository: BluetoothRepository,
private val connectionFactory: BleConnectionFactory,
private val service: RadioInterfaceService,
@Assisted val address: String,
val address: String,
) : IRadioInterface {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->

View File

@@ -16,10 +16,25 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
import org.koin.core.annotation.Single
import org.meshtastic.core.ble.BleConnectionFactory
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `NordicBleInterface` instances. */
@AssistedFactory
interface NordicBleInterfaceFactory {
fun create(rest: String): NordicBleInterface
@Single
class NordicBleInterfaceFactory(
private val scanner: BleScanner,
private val bluetoothRepository: BluetoothRepository,
private val connectionFactory: BleConnectionFactory,
) {
fun create(rest: String, service: RadioInterfaceService): NordicBleInterface = NordicBleInterface(
serviceScope = service.serviceScope,
scanner = scanner,
bluetoothRepository = bluetoothRepository,
connectionFactory = connectionFactory,
service = service,
address = rest,
)
}

View File

@@ -17,18 +17,19 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import org.koin.core.annotation.Single
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.model.util.anonymize
import javax.inject.Inject
import org.meshtastic.core.repository.RadioInterfaceService
/** Bluetooth backend implementation. */
class NordicBleInterfaceSpec
@Inject
constructor(
@Single
class NordicBleInterfaceSpec(
private val factory: NordicBleInterfaceFactory,
private val bluetoothRepository: BluetoothRepository,
) : InterfaceSpec<NordicBleInterface> {
override fun createInterface(rest: String): NordicBleInterface = factory.create(rest)
override fun createInterface(rest: String, service: RadioInterfaceService): NordicBleInterface =
factory.create(rest, service)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean = if (!bluetoothRepository.isBonded(rest)) {

View File

@@ -1,48 +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 org.meshtastic.app.repository.radio
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoMap
import dagger.multibindings.Multibinds
import org.meshtastic.core.model.InterfaceId
@Suppress("unused") // Used by hilt
@Module
@InstallIn(SingletonComponent::class)
abstract class RadioRepositoryModule {
@Multibinds abstract fun interfaceMap(): Map<InterfaceId, @JvmSuppressWildcards InterfaceSpec<*>>
@[Binds IntoMap InterfaceMapKey(InterfaceId.BLUETOOTH)]
abstract fun bindBluetoothInterfaceSpec(spec: NordicBleInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.MOCK)]
abstract fun bindMockInterfaceSpec(spec: MockInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.NOP)]
abstract fun bindNopInterfaceSpec(spec: NopInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.SERIAL)]
abstract fun bindSerialInterfaceSpec(spec: SerialInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.TCP)]
abstract fun bindTCPInterfaceSpec(spec: TCPInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
}

View File

@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.meshtastic.app.repository.usb.SerialConnection
import org.meshtastic.app.repository.usb.SerialConnectionListener
import org.meshtastic.app.repository.usb.UsbRepository
@@ -27,13 +25,10 @@ import org.meshtastic.core.repository.RadioInterfaceService
import java.util.concurrent.atomic.AtomicReference
/** An interface that assumes we are talking to a meshtastic device via USB serial */
class SerialInterface
@AssistedInject
constructor(
class SerialInterface(
service: RadioInterfaceService,
private val serialInterfaceSpec: SerialInterfaceSpec,
private val usbRepository: UsbRepository,
@Assisted private val address: String,
private val address: String,
) : StreamInterface(service) {
private var connRef = AtomicReference<SerialConnection?>()
@@ -47,7 +42,13 @@ constructor(
}
override fun connect() {
val device = serialInterfaceSpec.findSerial(address)
val deviceMap = usbRepository.serialDevices.value
val device =
if (deviceMap.containsKey(address)) {
deviceMap[address]!!
} else {
deviceMap.map { (_, driver) -> driver }.firstOrNull()
}
if (device == null) {
Logger.e { "[$address] Serial device not found at address" }
} else {

View File

@@ -16,10 +16,13 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
import org.koin.core.annotation.Single
import org.meshtastic.app.repository.usb.UsbRepository
import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `SerialInterface` instances. */
@AssistedFactory
interface SerialInterfaceFactory {
fun create(rest: String): SerialInterface
@Single
class SerialInterfaceFactory(private val usbRepository: UsbRepository) {
fun create(rest: String, service: RadioInterfaceService): SerialInterface =
SerialInterface(service, usbRepository, rest)
}

View File

@@ -18,23 +18,24 @@ package org.meshtastic.app.repository.radio
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.UsbSerialDriver
import org.koin.core.annotation.Single
import org.meshtastic.app.repository.usb.UsbRepository
import javax.inject.Inject
import org.meshtastic.core.repository.RadioInterfaceService
/** Serial/USB interface backend implementation. */
class SerialInterfaceSpec
@Inject
constructor(
@Single
class SerialInterfaceSpec(
private val factory: SerialInterfaceFactory,
private val usbManager: dagger.Lazy<UsbManager>,
private val usbManager: UsbManager,
private val usbRepository: UsbRepository,
) : InterfaceSpec<SerialInterface> {
override fun createInterface(rest: String): SerialInterface = factory.create(rest)
override fun createInterface(rest: String, service: RadioInterfaceService): SerialInterface =
factory.create(rest, service)
override fun addressValid(rest: String): Boolean {
usbRepository.serialDevices.value.filterValues { usbManager.get().hasPermission(it.device) }
usbRepository.serialDevices.value.filterValues { usbManager.hasPermission(it.device) }
findSerial(rest)?.let { d ->
return usbManager.get().hasPermission(d.device)
return usbManager.hasPermission(d.device)
}
return false
}

View File

@@ -17,8 +17,6 @@
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.meshtastic.app.repository.network.NetworkRepository
@@ -37,12 +35,10 @@ import java.net.InetAddress
import java.net.Socket
import java.net.SocketTimeoutException
open class TCPInterface
@AssistedInject
constructor(
open class TCPInterface(
service: RadioInterfaceService,
private val dispatchers: CoroutineDispatchers,
@Assisted private val address: String,
private val address: String,
) : StreamInterface(service) {
companion object {

View File

@@ -16,10 +16,12 @@
*/
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
import org.koin.core.annotation.Single
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.RadioInterfaceService
/** Factory for creating `TCPInterface` instances. */
@AssistedFactory
interface TCPInterfaceFactory {
fun create(rest: String): TCPInterface
@Single
class TCPInterfaceFactory(private val dispatchers: CoroutineDispatchers) {
fun create(rest: String, service: RadioInterfaceService): TCPInterface = TCPInterface(service, dispatchers, rest)
}

View File

@@ -16,9 +16,12 @@
*/
package org.meshtastic.app.repository.radio
import javax.inject.Inject
import org.koin.core.annotation.Single
import org.meshtastic.core.repository.RadioInterfaceService
/** TCP interface backend implementation. */
class TCPInterfaceSpec @Inject constructor(private val factory: TCPInterfaceFactory) : InterfaceSpec<TCPInterface> {
override fun createInterface(rest: String): TCPInterface = factory.create(rest)
@Single
class TCPInterfaceSpec(private val factory: TCPInterfaceFactory) : InterfaceSpec<TCPInterface> {
override fun createInterface(rest: String, service: RadioInterfaceService): TCPInterface =
factory.create(rest, service)
}

View File

@@ -19,17 +19,15 @@ package org.meshtastic.app.repository.usb
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
import dagger.Reusable
import javax.inject.Inject
import javax.inject.Provider
import org.koin.core.annotation.Single
/**
* Creates a probe table for the USB driver. This augments the default device-to-driver mappings with additional known
* working configurations. See this package's README for more info.
*/
@Reusable
class ProbeTableProvider @Inject constructor() : Provider<ProbeTable> {
override fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
@Single
class ProbeTableProvider {
fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
// RAK 4631:
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
// LilyGo TBeam v1.1:

View File

@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal class SerialConnectionImpl(
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val usbManagerLazy: Lazy<UsbManager?>,
private val device: UsbSerialDriver,
private val listener: SerialConnectionListener,
) : SerialConnection {
@@ -74,7 +74,7 @@ internal class SerialConnectionImpl(
override fun connect() {
// We shouldn't be able to get this far without a USB subsystem so explode if that isn't true
val usbManager = usbManagerLazy.get()!!
val usbManager = usbManagerLazy.value!!
val usbDeviceConnection = usbManager.openDevice(device.device)
if (usbDeviceConnection == null) {

View File

@@ -23,12 +23,13 @@ import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import co.touchlab.kermit.Logger
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.exceptionReporter
import org.meshtastic.core.common.util.getParcelableExtraCompat
import javax.inject.Inject
/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
class UsbBroadcastReceiver @Inject constructor(private val usbRepository: UsbRepository) : BroadcastReceiver() {
@Single
class UsbBroadcastReceiver(private val usbRepository: UsbRepository) : BroadcastReceiver() {
// Can be used for registering
internal val intentFilter
get() =

View File

@@ -32,31 +32,28 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.registerReceiverCompat
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import javax.inject.Inject
import javax.inject.Singleton
/** Repository responsible for maintaining and updating the state of USB connectivity. */
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class UsbRepository
@Inject
constructor(
@Single
class UsbRepository(
private val application: Application,
private val dispatchers: CoroutineDispatchers,
@ProcessLifecycle private val processLifecycle: Lifecycle,
private val usbBroadcastReceiverLazy: dagger.Lazy<UsbBroadcastReceiver>,
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>,
@Named("ProcessLifecycle") private val processLifecycle: Lifecycle,
private val usbBroadcastReceiverLazy: Lazy<UsbBroadcastReceiver>,
private val usbManagerLazy: Lazy<UsbManager?>,
private val usbSerialProberLazy: Lazy<UsbSerialProber>,
) {
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
val serialDevices =
_serialDevices
.mapLatest { serialDevices ->
val serialProber = usbSerialProberLazy.get()
val serialProber = usbSerialProberLazy.value
buildMap {
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
}
@@ -66,7 +63,7 @@ constructor(
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
refreshStateInternal()
usbBroadcastReceiverLazy.get().let { receiver ->
usbBroadcastReceiverLazy.value.let { receiver ->
application.registerReceiverCompat(receiver, receiver.intentFilter)
}
}
@@ -80,12 +77,12 @@ constructor(
SerialConnectionImpl(usbManagerLazy, device, listener)
fun requestPermission(device: UsbDevice): Flow<Boolean> =
usbManagerLazy.get()?.requestPermission(application, device) ?: emptyFlow()
usbManagerLazy.value?.requestPermission(application, device) ?: emptyFlow()
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
}
private suspend fun refreshStateInternal() =
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap()) }
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) }
}

View File

@@ -1,41 +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 org.meshtastic.app.repository.usb
import android.app.Application
import android.content.Context
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface UsbRepositoryModule {
companion object {
@Provides
fun provideUsbManager(application: Application): UsbManager? =
application.getSystemService(Context.USB_SERVICE) as UsbManager?
@Provides fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
@Provides fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
}
}

View File

@@ -18,14 +18,12 @@ package org.meshtastic.app.service
import android.content.Context
import androidx.glance.appwidget.updateAll
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koin.core.annotation.Single
import org.meshtastic.app.widget.LocalStatsWidget
import org.meshtastic.core.repository.AppWidgetUpdater
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AndroidAppWidgetUpdater @Inject constructor(@ApplicationContext private val context: Context) : AppWidgetUpdater {
@Single
class AndroidAppWidgetUpdater(private val context: Context) : AppWidgetUpdater {
override suspend fun updateAll() {
// Kickstart the widget composition.
// The widget internally uses collectAsState() and its own sampled StateFlow

View File

@@ -26,22 +26,17 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.core.annotation.Single
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.model.Position
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import org.meshtastic.proto.Position as ProtoPosition
@Singleton
class AndroidMeshLocationManager
@Inject
constructor(
private val context: Application,
private val locationRepository: LocationRepository,
) : MeshLocationManager {
@Single
class AndroidMeshLocationManager(private val context: Application, private val locationRepository: LocationRepository) :
MeshLocationManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var locationFlow: Job? = null

View File

@@ -20,13 +20,12 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import org.koin.core.annotation.Single
import org.meshtastic.app.messaging.domain.worker.SendMessageWorker
import org.meshtastic.core.repository.MeshWorkerManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AndroidMeshWorkerManager @Inject constructor(private val workManager: WorkManager) : MeshWorkerManager {
@Single
class AndroidMeshWorkerManager(private val workManager: WorkManager) : MeshWorkerManager {
override fun enqueueSendMessage(packetId: Int) {
val workRequest =
OneTimeWorkRequestBuilder<SendMessageWorker>()

View File

@@ -19,23 +19,24 @@ package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.PacketRepository
import javax.inject.Inject
/** A [BroadcastReceiver] that handles "Mark as read" actions from notifications. */
@AndroidEntryPoint
class MarkAsReadReceiver : BroadcastReceiver() {
class MarkAsReadReceiver :
BroadcastReceiver(),
KoinComponent {
@Inject lateinit var packetRepository: PacketRepository
private val packetRepository: PacketRepository by inject()
@Inject lateinit var serviceNotifications: MeshServiceNotifications
private val serviceNotifications: MeshServiceNotifications by inject()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

View File

@@ -24,12 +24,12 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.ServiceCompat
import co.touchlab.kermit.Logger
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.android.ext.android.inject
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.ui.connections.NO_DEVICE_SELECTED
import org.meshtastic.core.common.hasLocationPermission
@@ -50,42 +50,37 @@ import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.service.IMeshService
import org.meshtastic.proto.PortNum
import javax.inject.Inject
@AndroidEntryPoint
@Suppress("TooManyFunctions", "LargeClass")
class MeshService : Service() {
@Inject lateinit var radioInterfaceService: RadioInterfaceService
private val radioInterfaceService: RadioInterfaceService by inject()
@Inject lateinit var serviceRepository: ServiceRepository
private val serviceRepository: ServiceRepository by inject()
@Inject lateinit var packetHandler: PacketHandler
private val packetHandler: PacketHandler by inject()
@Inject lateinit var serviceBroadcasts: ServiceBroadcasts
private val serviceBroadcasts: ServiceBroadcasts by inject()
@Inject lateinit var nodeManager: NodeManager
private val nodeManager: NodeManager by inject()
@Inject lateinit var messageProcessor: MeshMessageProcessor
private val messageProcessor: MeshMessageProcessor by inject()
@Inject lateinit var commandSender: CommandSender
private val commandSender: CommandSender by inject()
@Inject lateinit var locationManager: MeshLocationManager
private val locationManager: MeshLocationManager by inject()
@Inject lateinit var connectionManager: MeshConnectionManager
private val connectionManager: MeshConnectionManager by inject()
@Inject lateinit var serviceNotifications: MeshServiceNotifications
private val serviceNotifications: MeshServiceNotifications by inject()
@Inject lateinit var radioConfigRepository: RadioConfigRepository
@Inject lateinit var router: MeshRouter
private val router: MeshRouter by inject()
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)

View File

@@ -36,11 +36,10 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.Single
import org.meshtastic.app.MainActivity
import org.meshtastic.app.R.raw
import org.meshtastic.app.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION
@@ -92,8 +91,6 @@ import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.Telemetry
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.minutes
/**
@@ -103,11 +100,9 @@ import kotlin.time.Duration.Companion.minutes
* notifications for various events like new messages, alerts, and service status changes.
*/
@Suppress("TooManyFunctions", "LongParameterList")
@Singleton
class MeshServiceNotificationsImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
@Single
class MeshServiceNotificationsImpl(
private val context: Context,
private val packetRepository: Lazy<PacketRepository>,
private val nodeRepository: Lazy<NodeRepository>,
) : MeshServiceNotifications {
@@ -304,7 +299,7 @@ constructor(
// Seeding from database if caches are still null (e.g. on restart or reconnection)
if (cachedLocalStats == null || cachedDeviceMetrics == null) {
val repo = nodeRepository.get()
val repo = nodeRepository.value
val myNodeNum = repo.myNodeInfo.value?.myNodeNum
if (myNodeNum != null) {
// We use runBlocking here because this is called from MeshConnectionManager's synchronous methods,
@@ -389,15 +384,14 @@ constructor(
channelName: String?,
isSilent: Boolean = false,
) {
val ourNode = nodeRepository.get().ourNodeInfo.value
val ourNode = nodeRepository.value.ourNodeInfo.value
val history =
packetRepository
.get()
packetRepository.value
.getMessagesFrom(contactKey, includeFiltered = false) { nodeId ->
if (nodeId == DataPacket.ID_LOCAL) {
ourNode ?: nodeRepository.get().getNode(nodeId)
ourNode ?: nodeRepository.value.getNode(nodeId)
} else {
nodeRepository.get().getNode(nodeId ?: "")
nodeRepository.value.getNode(nodeId ?: "")
}
}
.first()
@@ -430,7 +424,7 @@ constructor(
it.id != SUMMARY_ID && it.notification.group == GROUP_KEY_MESSAGES
}
val ourNode = nodeRepository.get().ourNodeInfo.value
val ourNode = nodeRepository.value.ourNodeInfo.value
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
val me =
Person.Builder()
@@ -542,7 +536,7 @@ constructor(
builder.setSilent(true)
}
val ourNode = nodeRepository.get().ourNodeInfo.value
val ourNode = nodeRepository.value.ourNodeInfo.value
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
val me =
Person.Builder()
@@ -574,7 +568,7 @@ constructor(
// Add reactions as separate "messages" in history if they exist
msg.emojis.forEach { reaction ->
val reactorNode = nodeRepository.get().getNode(reaction.user.id)
val reactorNode = nodeRepository.value.getNode(reaction.user.id)
val reactor =
Person.Builder()
.setName(reaction.user.long_name)

View File

@@ -20,19 +20,20 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import co.touchlab.kermit.Logger
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.ServiceRepository
import javax.inject.Inject
@AndroidEntryPoint
class ReactionReceiver : BroadcastReceiver() {
class ReactionReceiver :
BroadcastReceiver(),
KoinComponent {
@Inject lateinit var serviceRepository: ServiceRepository
private val serviceRepository: ServiceRepository by inject()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

View File

@@ -20,16 +20,15 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.ServiceRepository
/**
* A [BroadcastReceiver] that handles inline replies from notifications.
@@ -38,11 +37,12 @@ import org.meshtastic.core.repository.ServiceRepository
* and the contact key from the intent, sends the message using the [ServiceRepository], and then cancels the original
* notification.
*/
@AndroidEntryPoint
class ReplyReceiver : BroadcastReceiver() {
@Inject lateinit var radioController: RadioController
class ReplyReceiver :
BroadcastReceiver(),
KoinComponent {
private val radioController: RadioController by inject()
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
private val meshServiceNotifications: MeshServiceNotifications by inject()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

View File

@@ -20,7 +20,7 @@ import android.content.Context
import android.content.Intent
import android.os.Parcelable
import co.touchlab.kermit.Logger
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koin.core.annotation.Single
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
@@ -29,17 +29,11 @@ import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.repository.ServiceRepository
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import org.meshtastic.core.repository.ServiceBroadcasts as SharedServiceBroadcasts
@Singleton
class ServiceBroadcasts
@Inject
constructor(
@ApplicationContext private val context: Context,
private val serviceRepository: ServiceRepository,
) : SharedServiceBroadcasts {
@Single
class ServiceBroadcasts(private val context: Context, private val serviceRepository: ServiceRepository) :
SharedServiceBroadcasts {
// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf<String, String>()

View File

@@ -0,0 +1,28 @@
/*
* 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.settings
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.domain.usecase.settings.CleanNodeDatabaseUseCase
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.feature.settings.radio.CleanNodeDatabaseViewModel
@KoinViewModel
class AndroidCleanNodeDatabaseViewModel(
cleanNodeDatabaseUseCase: CleanNodeDatabaseUseCase,
alertManager: AlertManager,
) : CleanNodeDatabaseViewModel(cleanNodeDatabaseUseCase, alertManager)

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