diff --git a/AGENTS.md b/AGENTS.md index a7ea32e79..d16cc31ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..87b88d43d --- /dev/null +++ b/GEMINI.md @@ -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). diff --git a/README.md b/README.md index cab5bb9b0..c05a4f17e 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/app/README.md b/app/README.md index 1967019af..b386a45ce 100644 --- a/app/README.md +++ b/app/README.md @@ -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: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f427214e..8327d293f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 0e08e976a..3ff014be2 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -4,6 +4,13 @@ CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController) LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect() + 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, ) + LongParameterList:AndroidNodeListViewModel.kt$AndroidNodeListViewModel$( savedStateHandle: SavedStateHandle, nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, radioController: RadioController, nodeManagementActions: NodeManagementActions, getFilteredNodesUseCase: GetFilteredNodesUseCase, nodeFilterPreferences: NodeFilterPreferences, ) + 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, ) + 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, ) + MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1000L + MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-5 + MagicNumber:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$1e-7 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790 @@ -15,10 +22,9 @@ MagicNumber:StreamInterface.kt$StreamInterface$4 MagicNumber:StreamInterface.kt$StreamInterface$8 MagicNumber:TCPInterface.kt$TCPInterface$1000 - MaxLineLength:DataSourceModule.kt$DataSourceModule$fun - ParameterListWrapping:DataSourceModule.kt$DataSourceModule$(impl: BootloaderOtaQuirksJsonDataSourceImpl) SwallowedException:NsdManager.kt$ex: IllegalArgumentException SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException + TooGenericExceptionCaught:AndroidRadioConfigViewModel.kt$AndroidRadioConfigViewModel$ex: Exception TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface diff --git a/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt b/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt index a4c44e964..f2e806e29 100644 --- a/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt +++ b/app/src/androidTest/kotlin/org/meshtastic/app/filter/MessageFilterIntegrationTest.kt @@ -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 { diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt b/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt index 69d9648d9..7d0daab08 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt @@ -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 diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt index a2716d1e0..42f1f9a88 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt @@ -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 = throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.") diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt b/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt new file mode 100644 index 000000000..5a192d437 --- /dev/null +++ b/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt @@ -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 . + */ +package org.meshtastic.app.di + +import org.koin.core.annotation.Module + +@Module(includes = [FDroidNetworkModule::class]) +class FlavorModule diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt index ba3300a99..290ea8667 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt @@ -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, onTracerouteMappableCountChanged: (Int, Int) -> Unit, ) { - val mapViewModel: MapViewModel = hiltViewModel() + val mapViewModel: MapViewModel = koinViewModel() org.meshtastic.app.map.MapView( modifier = modifier, mapViewModel = mapViewModel, diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt index 8fa664f80..1ba1e02f7 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt @@ -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? = null, diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt index 36b575d6a..83e253e59 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt @@ -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, diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt index bab1171d8..ac438397a 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt @@ -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 { diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt similarity index 88% rename from feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt index e9b3c5054..638dcead9 100644 --- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/component/InlineMap.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt @@ -14,13 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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 } diff --git a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt b/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt similarity index 66% rename from feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt index 2a35798f3..d6515eeb7 100644 --- a/feature/node/src/fdroid/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt @@ -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 . */ - -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, +) diff --git a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt b/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt index 30fa55730..a41eae2d3 100644 --- a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt +++ b/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt @@ -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 diff --git a/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt new file mode 100644 index 000000000..802f3b150 --- /dev/null +++ b/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt @@ -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 . + */ +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 diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt index 2a0894c45..0e88cb0fe 100644 --- a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt +++ b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt @@ -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() - } } diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt deleted file mode 100644 index af63aab83..000000000 --- a/app/src/google/kotlin/org/meshtastic/app/di/GooglePlatformAnalyticsModule.kt +++ /dev/null @@ -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 . - */ -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 -} diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt index 63a7cd8a3..96680ce88 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt @@ -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, onTracerouteMappableCountChanged: (Int, Int) -> Unit, ) { - val mapViewModel: MapViewModel = hiltViewModel() + val mapViewModel: MapViewModel = koinViewModel() org.meshtastic.app.map.MapView( modifier = modifier, mapViewModel = mapViewModel, diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt index d9f12aac0..a67087399 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -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? = null, diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt index 9a501b96c..cb3e00257 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt @@ -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, diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt new file mode 100644 index 000000000..e33fb1f8c --- /dev/null +++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt @@ -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 . + */ +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 = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")), + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + produceFile = { context.preferencesDataStoreFile("google_maps_ds") }, + ) +} diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt deleted file mode 100644 index a8d0a1192..000000000 --- a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt +++ /dev/null @@ -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 . - */ -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 = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("google_maps_ds") }, - ) - } -} diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt index 72760694a..0beba5e92 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt @@ -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) } -@Singleton -class GoogleMapsPrefsImpl -@Inject -constructor( - @GoogleMapsDataStore private val dataStore: DataStore, +@Single +class GoogleMapsPrefsImpl( + @Named("GoogleMapsDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : GoogleMapsPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt index 8d8a1d6cf..6840cb17d 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt @@ -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> @@ -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, diff --git a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt b/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt similarity index 96% rename from feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt rename to app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt index cb94e313f..c86e7a78c 100644 --- a/feature/node/src/google/kotlin/org/meshtastic/feature/node/component/InlineMap.kt +++ b/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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) { diff --git a/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt new file mode 100644 index 000000000..992edf588 --- /dev/null +++ b/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt @@ -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 . + */ +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, +) diff --git a/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt b/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt deleted file mode 100644 index d609d38dd..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/ApplicationModule.kt +++ /dev/null @@ -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 . - */ -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 - } - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index d34038548..8ed01e5d8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -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() + val introViewModel = koinViewModel() AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel) } } diff --git a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt b/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt similarity index 79% rename from core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt rename to app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt index 5eb0b500c..80cc15dde 100644 --- a/core/di/src/commonMain/kotlin/org/meshtastic/core/di/ProcessLifecycle.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt @@ -14,10 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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 diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt b/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt index b683fd380..eacb76cc8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt @@ -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.Stub::asInterface), diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt index daae4a159..6d96616fb 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt @@ -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().close() + get().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) { diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt new file mode 100644 index 000000000..becacee54 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt @@ -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 . + */ +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) +} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt deleted file mode 100644 index 8e9a434fd..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt +++ /dev/null @@ -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 . - */ -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") - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt deleted file mode 100644 index 55a42e183..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/DataSourceModule.kt +++ /dev/null @@ -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 . - */ -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 -} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt index f3dabfe13..58416a139 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt @@ -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) } } } diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt deleted file mode 100644 index 54a91068d..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/NodeDataSourceModule.kt +++ /dev/null @@ -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 . - */ -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 -} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt deleted file mode 100644 index 1d555b5b0..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/PrefsModule.kt +++ /dev/null @@ -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 . - */ -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 = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("analytics_ds") }, - ) - - @Provides - @Singleton - @HomoglyphEncodingDataStore - fun provideHomoglyphEncodingDataStore(@ApplicationContext context: Context): DataStore = - 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 = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("app_ds") }, - ) - - @Provides - @Singleton - @CustomEmojiDataStore - fun provideCustomEmojiDataStore(@ApplicationContext context: Context): DataStore = - 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 = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "map_prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("map_ds") }, - ) - - @Provides - @Singleton - @MapConsentDataStore - fun provideMapConsentDataStore(@ApplicationContext context: Context): DataStore = - 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 = - 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 = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("mesh_ds") }, - ) - - @Provides - @Singleton - @RadioDataStore - fun provideRadioDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("radio_ds") }, - ) - - @Provides - @Singleton - @UiDataStore - fun provideUiDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("ui_ds") }, - ) - - @Provides - @Singleton - @MeshLogDataStore - fun provideMeshLogDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("meshlog_ds") }, - ) - - @Provides - @Singleton - @FilterDataStore - fun provideFilterDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")), - scope = scope, - produceFile = { context.preferencesDataStoreFile("filter_ds") }, - ) - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt deleted file mode 100644 index 98c19f5bc..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/RepositoryModule.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt deleted file mode 100644 index 918da974d..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt +++ /dev/null @@ -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 . - */ -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 -} diff --git a/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt index 4d009e862..badfda791 100644 --- a/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt +++ b/app/src/main/kotlin/org/meshtastic/app/domain/usecase/GetDiscoveredDevicesUseCase.kt @@ -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, @@ -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, + private val usbManagerLazy: Lazy, ) { 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( diff --git a/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt new file mode 100644 index 000000000..182863c9d --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/firmware/AndroidFirmwareUpdateViewModel.kt @@ -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 . + */ +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, +) diff --git a/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt index 0414e37bf..c387f2e20 100644 --- a/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt @@ -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() diff --git a/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt index 24ebe7995..38a2e0746 100644 --- a/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt index a8780be59..63737002a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt index e8a23a17a..8c56a2b62 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt index ee7f4e7bb..a352b1804 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt index b64e5de24..1346b8b54 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt @@ -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) diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt index 3b4b8f4d8..19fb3324e 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt @@ -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) { diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt index ea26e2c6c..cabc51caa 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt @@ -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 = diff --git a/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt index 54b2f6f2a..d82619961 100644 --- a/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/model/UIViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt index 819d72e13..bcc47ddc1 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ChannelsNavigation.kt @@ -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(viewModelStoreOwner = parentEntry), onNavigate = { route -> navController.navigate(route) }, onNavigateUp = { navController.navigateUp() }, ) diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt index 4ece8d6a5..02173ab7a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ConnectionsNavigation.kt @@ -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(viewModelStoreOwner = parentEntry), onClickNodeChip = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) { launchSingleTop = true diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt index 130196bc1..7f4a86e63 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt @@ -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( deepLinks = listOf(navDeepLink(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() - val messageViewModel = hiltViewModel() + val contactsViewModel = koinViewModel() + val messageViewModel = koinViewModel() AdaptiveContactsScreen( navController = navController, @@ -71,11 +71,11 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE ), ) { backStackEntry -> val args = backStackEntry.toRoute() - val uiViewModel: UIViewModel = hiltViewModel() + val uiViewModel: UIViewModel = koinViewModel() val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle() val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle() - val contactsViewModel = hiltViewModel() - val messageViewModel = hiltViewModel() + val contactsViewModel = koinViewModel() + val messageViewModel = koinViewModel() AdaptiveContactsScreen( navController = navController, @@ -101,7 +101,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE ), ) { backStackEntry -> val message = backStackEntry.toRoute().message - val viewModel = hiltViewModel() + val viewModel = koinViewModel() ShareScreen( viewModel = viewModel, onConfirm = { @@ -115,7 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE composable( deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/quick_chat")), ) { - val viewModel = hiltViewModel() + val viewModel = koinViewModel() QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp) } } diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt index 88439d6c8..5ab3efcdd 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/FirmwareNavigation.kt @@ -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(startDestination = FirmwareRoutes.FirmwareUpdate) { - composable { FirmwareUpdateScreen(navController) } + composable { + val viewModel = koinViewModel() + FirmwareUpdateScreen(onNavigateUp = { navController.navigateUp() }, viewModel = viewModel) + } } } diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt index 71adb01cc..28f2ea3e8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt @@ -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(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) { - val viewModel = hiltViewModel() + val viewModel = koinViewModel() MapScreen( viewModel = viewModel, onClickNodeChip = { diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt index 56d44b6f4..a8dc4c131 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt @@ -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(parentGraphBackStackEntry) + val vm = koinViewModel(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(parentGraphBackStackEntry) + val metricsViewModel = + koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry) val args = backStackEntry.toRoute() 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(parentGraphBackStackEntry) + val metricsViewModel = + koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry) val args = backStackEntry.toRoute() metricsViewModel.setNodeId(args.destNum) @@ -277,7 +280,7 @@ private inline fun NavGraphBuilder.addNodeDetailScreenCompos ) { backStackEntry -> val parentGraphBackStackEntry = remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) } - val metricsViewModel = hiltViewModel(parentGraphBackStackEntry) + val metricsViewModel = koinViewModel(viewModelStoreOwner = parentGraphBackStackEntry) val args = backStackEntry.toRoute() val destNum = getDestNum(args) diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt index eebe1db28..f440fdfc3 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt @@ -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(viewModelStoreOwner = parentEntry), + viewModel = koinViewModel(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(viewModelStoreOwner = parentEntry), onBack = navController::popBackStack, onNavigate = { route -> navController.navigate(route) }, ) @@ -109,10 +112,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) { composable { 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(viewModelStoreOwner = parentEntry), excludedModulesUnlocked = excludedModulesUnlocked, onBack = navController::popBackStack, onNavigate = { route -> navController.navigate(route) }, @@ -122,7 +125,10 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController) { composable { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } - AdministrationScreen(viewModel = hiltViewModel(parentEntry), onBack = navController::popBackStack) + AdministrationScreen( + viewModel = koinViewModel(viewModelStoreOwner = parentEntry), + onBack = navController::popBackStack, + ) } composable( @@ -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(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")), ) { - DebugScreen(onNavigateUp = navController::navigateUp) + val viewModel: AndroidDebugViewModel = koinViewModel() + DebugScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp) } composable { AboutScreen(onNavigateUp = navController::navigateUp) } - composable { FilterSettingsScreen(onBack = navController::navigateUp) } + composable { + val viewModel: AndroidFilterSettingsViewModel = koinViewModel() + FilterSettingsScreen(viewModel = viewModel, onBack = navController::navigateUp) + } } } context(_: NavGraphBuilder) inline fun 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 NavHostController.configComposable( route: KClass, parentGraphRoute: KClass, - content: @Composable (RadioConfigViewModel) -> Unit, + content: @Composable (AndroidRadioConfigViewModel) -> Unit, ) { navGraphBuilder.composable(route = route) { backStackEntry -> val parentEntry = remember(backStackEntry) { getBackStackEntry(parentGraphRoute) } - content(hiltViewModel(parentEntry)) + content(koinViewModel(viewModelStoreOwner = parentEntry)) } } diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt similarity index 50% rename from app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt rename to app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt index e20f08582..7feda7282 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/DataModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidCompassViewModel.kt @@ -14,23 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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) diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt new file mode 100644 index 000000000..f7333c8af --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt @@ -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 . + */ +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().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) +} diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt new file mode 100644 index 000000000..74ac78e09 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeDetailViewModel.kt @@ -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 . + */ +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, +) diff --git a/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt new file mode 100644 index 000000000..584c626ee --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/node/AndroidNodeListViewModel.kt @@ -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 . + */ +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, +) diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt index eeda06b17..76d3879a2 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepository.kt @@ -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, - private val connectivityManager: dagger.Lazy, +@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 by lazy { connectivityManager - .get() .networkAvailable() .flowOn(dispatchers.io) .conflate() @@ -57,8 +53,7 @@ constructor( } val resolvedList: Flow> by lazy { - nsdManagerLazy - .get() + nsdManager .serviceList(SERVICE_TYPE) .flowOn(dispatchers.io) .conflate() diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt deleted file mode 100644 index 573ae4d9b..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/repository/network/NetworkRepositoryModule.kt +++ /dev/null @@ -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 . - */ -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 - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt index 4c2547a75..4a4105675 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt @@ -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, 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 diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt index dc6c1204d..548fb37b9 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt @@ -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>>, + private val bluetoothSpec: Lazy, + private val mockSpec: Lazy, + private val serialSpec: Lazy, + private val tcpSpec: Lazy, ) { internal val nopInterface by lazy { nopInterfaceFactory.create("") } + private val specMap: Map> + 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?, 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) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt index 5bfede5cd..ece828cc9 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt @@ -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 { - 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 diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt index 4059b4e33..c2ff1f0e5 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt @@ -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 diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt index f25aa828f..5f8328d3a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt @@ -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) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt index 4a6a1862f..13dcadd50 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt @@ -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 { - override fun createInterface(rest: String): MockInterface = factory.create(rest) +@Single +class MockInterfaceSpec(private val factory: MockInterfaceFactory) : InterfaceSpec { + 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 diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt index 60f30c743..2197bd748 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt @@ -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 } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt index e7b29e93d..56d58b846 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt @@ -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) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt index 791209c1b..149a2469a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt @@ -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 { - override fun createInterface(rest: String): NopInterface = factory.create(rest) +@Single +class NopInterfaceSpec(private val factory: NopInterfaceFactory) : InterfaceSpec { + override fun createInterface(rest: String, service: RadioInterfaceService): NopInterface = factory.create(rest) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt index fd0371af8..3823c6161 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterface.kt @@ -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 -> diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt index 76835ffaf..8ea076ce2 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceFactory.kt @@ -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, + ) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt index d7b03d1a2..ce93bfb71 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceSpec.kt @@ -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 { - 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)) { diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt deleted file mode 100644 index 01a715312..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/RadioRepositoryModule.kt +++ /dev/null @@ -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 . - */ -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> - - @[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<*> -} diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt index 39992f67b..718edf83b 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt @@ -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() @@ -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 { diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt index ef518d324..56f76fd80 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt @@ -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) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt index 874210352..75ab3e006 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt @@ -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, + private val usbManager: UsbManager, private val usbRepository: UsbRepository, ) : InterfaceSpec { - 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 } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt index 4ba551f2e..7f6fb4442 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt @@ -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 { diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt index 1a96d9537..b11916940 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt @@ -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) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt index b5a9e1ed1..b48ee826c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt @@ -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 { - override fun createInterface(rest: String): TCPInterface = factory.create(rest) +@Single +class TCPInterfaceSpec(private val factory: TCPInterfaceFactory) : InterfaceSpec { + override fun createInterface(rest: String, service: RadioInterfaceService): TCPInterface = + factory.create(rest, service) } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt index 9d8a21bae..3ae444175 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/ProbeTableProvider.kt @@ -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 { - 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: diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt index bfd959ef2..568010eea 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/SerialConnectionImpl.kt @@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference internal class SerialConnectionImpl( - private val usbManagerLazy: dagger.Lazy, + private val usbManagerLazy: Lazy, 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) { diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt index 6be9c82c4..9a2904adf 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbBroadcastReceiver.kt @@ -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() = diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt index 3f9aad9ba..397b9ecd3 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt +++ b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepository.kt @@ -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, - private val usbManagerLazy: dagger.Lazy, - private val usbSerialProberLazy: dagger.Lazy, + @Named("ProcessLifecycle") private val processLifecycle: Lifecycle, + private val usbBroadcastReceiverLazy: Lazy, + private val usbManagerLazy: Lazy, + private val usbSerialProberLazy: Lazy, ) { private val _serialDevices = MutableStateFlow(emptyMap()) 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 = - 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()) } } diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt b/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt deleted file mode 100644 index 7396619fa..000000000 --- a/app/src/main/kotlin/org/meshtastic/app/repository/usb/UsbRepositoryModule.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt index f43935611..5749a9e7d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidAppWidgetUpdater.kt @@ -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 diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt index c3d9d58f3..e820c3639 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt @@ -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 diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt index 570996691..25e88a9ff 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt @@ -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() diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt index 76b66bdbf..ebe68c74d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt @@ -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()) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt b/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt index 83e2a996f..72efaf81f 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt @@ -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) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt b/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt index a7680c117..e790d8d0d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt @@ -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, private val nodeRepository: Lazy, ) : 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) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt index cd3f32c5b..fec13effb 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt @@ -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) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt index 190915b3f..e09f6c656 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt @@ -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()) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt b/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt index 86845e25b..8b4ffc1a2 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt @@ -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() diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt new file mode 100644 index 000000000..08f308822 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidCleanNodeDatabaseViewModel.kt @@ -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 . + */ +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) diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt new file mode 100644 index 000000000..1fb85df8a --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidDebugViewModel.kt @@ -0,0 +1,38 @@ +/* + * 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 . + */ +package org.meshtastic.app.settings + +import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.ui.util.AlertManager +import org.meshtastic.feature.settings.debugging.DebugViewModel +import java.util.Locale + +@KoinViewModel +class AndroidDebugViewModel( + meshLogRepository: MeshLogRepository, + nodeRepository: NodeRepository, + meshLogPrefs: MeshLogPrefs, + alertManager: AlertManager, +) : DebugViewModel(meshLogRepository, nodeRepository, meshLogPrefs, alertManager) { + + override fun Int.toHex(length: Int): String = "!%0${length}x".format(Locale.getDefault(), this) + + override fun Byte.toHex(): String = "%02x".format(Locale.getDefault(), this) +} diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt new file mode 100644 index 000000000..03e9ded94 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidFilterSettingsViewModel.kt @@ -0,0 +1,26 @@ +/* + * 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 . + */ +package org.meshtastic.app.settings + +import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.repository.FilterPrefs +import org.meshtastic.core.repository.MessageFilter +import org.meshtastic.feature.settings.filter.FilterSettingsViewModel + +@KoinViewModel +class AndroidFilterSettingsViewModel(filterPrefs: FilterPrefs, messageFilter: MessageFilter) : + FilterSettingsViewModel(filterPrefs, messageFilter) diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt new file mode 100644 index 000000000..ab57c13b8 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidRadioConfigViewModel.kt @@ -0,0 +1,164 @@ +/* + * 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 . + */ +package org.meshtastic.app.settings + +import android.Manifest +import android.app.Application +import android.content.pm.PackageManager +import android.location.Location +import android.net.Uri +import androidx.annotation.RequiresPermission +import androidx.core.content.ContextCompat +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okio.buffer +import okio.sink +import okio.source +import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase +import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase +import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase +import org.meshtastic.core.domain.usecase.settings.ImportProfileUseCase +import org.meshtastic.core.domain.usecase.settings.InstallProfileUseCase +import org.meshtastic.core.domain.usecase.settings.ProcessRadioResponseUseCase +import org.meshtastic.core.domain.usecase.settings.RadioConfigUseCase +import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase +import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase +import org.meshtastic.core.repository.AnalyticsPrefs +import org.meshtastic.core.repository.HomoglyphPrefs +import org.meshtastic.core.repository.LocationRepository +import org.meshtastic.core.repository.MapConsentPrefs +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.settings.radio.RadioConfigViewModel +import org.meshtastic.proto.Config +import org.meshtastic.proto.DeviceProfile +import java.io.FileOutputStream + +@KoinViewModel +class 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, +) : RadioConfigViewModel( + savedStateHandle, + radioConfigRepository, + packetRepository, + serviceRepository, + nodeRepository, + locationRepository, + mapConsentPrefs, + analyticsPrefs, + homoglyphEncodingPrefs, + toggleAnalyticsUseCase, + toggleHomoglyphEncodingUseCase, + importProfileUseCase, + exportProfileUseCase, + exportSecurityConfigUseCase, + installProfileUseCase, + radioConfigUseCase, + adminActionsUseCase, + processRadioResponseUseCase, +) { + @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) + override suspend fun getCurrentLocation(): Location? = if ( + ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + ) { + locationRepository.getLocations().firstOrNull() + } else { + null + } + + override fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) { + if (uri is Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream -> + importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it } + } + } catch (ex: Exception) { + Logger.e { "Import DeviceProfile error: ${ex.message}" } + // Error handling simplified for this example + } + } + } + } + + override fun exportProfile(uri: Any, profile: DeviceProfile) { + if (uri is Uri) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream -> + exportProfileUseCase(outputStream, profile) + .onSuccess { /* Success */ } + .onFailure { throw it } + } + } + } catch (ex: Exception) { + Logger.e { "Can't write file error: ${ex.message}" } + } + } + } + } + } + + override fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) { + if (uri is Uri) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream -> + exportSecurityConfigUseCase(outputStream, securityConfig) + .onSuccess { /* Success */ } + .onFailure { throw it } + } + } + } catch (ex: Exception) { + Logger.e { "Can't write security keys JSON error: ${ex.message}" } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt new file mode 100644 index 000000000..769036c40 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/settings/AndroidSettingsViewModel.kt @@ -0,0 +1,103 @@ +/* + * 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 . + */ +package org.meshtastic.app.settings + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okio.BufferedSink +import okio.buffer +import okio.sink +import org.koin.core.annotation.KoinViewModel +import org.meshtastic.core.common.BuildConfigProvider +import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase +import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase +import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase +import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase +import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase +import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase +import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase +import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase +import org.meshtastic.core.model.RadioController +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.feature.settings.SettingsViewModel +import java.io.FileNotFoundException +import java.io.FileOutputStream + +@KoinViewModel +class 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, +) : SettingsViewModel( + radioConfigRepository, + radioController, + nodeRepository, + uiPrefs, + buildConfigProvider, + databaseManager, + meshLogPrefs, + setThemeUseCase, + setAppIntroCompletedUseCase, + setProvideLocationUseCase, + setDatabaseCacheLimitUseCase, + setMeshLogSettingsUseCase, + meshLocationUseCase, + exportDataUseCase, + isOtaCapableUseCase, +) { + override fun saveDataCsv(uri: Any, filterPortnum: Int?) { + if (uri is Uri) { + viewModelScope.launch { writeToUri(uri) { writer -> performDataExport(writer, filterPortnum) } } + } + } + + private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) { + withContext(Dispatchers.IO) { + try { + app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> + FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer -> + block.invoke(writer) + } + } + } catch (ex: FileNotFoundException) { + Logger.e { "Can't write file error: ${ex.message}" } + } + } + } +} diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt index adcab19c5..fcaf62df7 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -67,7 +67,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hasRoute @@ -79,10 +78,10 @@ import androidx.navigation.compose.rememberNavController import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.app.BuildConfig import org.meshtastic.app.model.UIViewModel import org.meshtastic.app.navigation.channelsGraph @@ -159,7 +158,7 @@ enum class TopLevelDestination(val label: StringResource, val icon: ImageVector, @OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerViewModel = hiltViewModel()) { +fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerViewModel = koinViewModel()) { val navController = rememberNavController() LaunchedEffect(uIViewModel) { uIViewModel.navigationDeepLink.collectLatest { uri -> navController.navigate(uri) } } val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle() @@ -168,10 +167,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie val unreadMessageCount by uIViewModel.unreadMessageCount.collectAsStateWithLifecycle() if (connectionState == ConnectionState.Connected) { - RequestNotificationPermission { - // Nordic handled the trigger for POST_NOTIFICATIONS when connected - } - sharedContactRequested?.let { SharedContactDialog(sharedContact = it, onDismiss = { uIViewModel.clearSharedContactRequested() }) } diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt index 5f4e34e29..ba8d454ab 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsScreen.kt @@ -46,11 +46,11 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.app.model.DeviceListEntry import org.meshtastic.app.ui.connections.components.BLEDevices import org.meshtastic.app.ui.connections.components.ConnectingDeviceInfo @@ -92,9 +92,9 @@ import kotlin.uuid.ExperimentalUuidApi @Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber", "ModifierMissing", "ComposableParamOrder") @Composable fun ConnectionsScreen( - connectionsViewModel: ConnectionsViewModel = hiltViewModel(), - scanModel: ScannerViewModel = hiltViewModel(), - radioConfigViewModel: RadioConfigViewModel = hiltViewModel(), + connectionsViewModel: ConnectionsViewModel = koinViewModel(), + scanModel: ScannerViewModel = koinViewModel(), + radioConfigViewModel: RadioConfigViewModel = koinViewModel(), onClickNodeChip: (Int) -> Unit, onNavigateToNodeDetails: (Int) -> Unit, onConfigNavigate: (Route) -> Unit, diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt index 8205ff0c0..372202c46 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ConnectionsViewModel.kt @@ -17,10 +17,10 @@ package org.meshtastic.app.ui.connections import androidx.lifecycle.ViewModel -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.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.repository.NodeRepository @@ -29,12 +29,9 @@ import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig -import javax.inject.Inject -@HiltViewModel -class ConnectionsViewModel -@Inject -constructor( +@KoinViewModel +class ConnectionsViewModel( radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt index cb03f8446..93005bec1 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/ScannerViewModel.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel import org.meshtastic.app.domain.usecase.GetDiscoveredDevicesUseCase import org.meshtastic.app.model.DeviceListEntry import org.meshtastic.app.repository.usb.UsbRepository @@ -42,13 +42,10 @@ import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed -import javax.inject.Inject -@HiltViewModel +@KoinViewModel @Suppress("LongParameterList", "TooManyFunctions") -class ScannerViewModel -@Inject -constructor( +class ScannerViewModel( private val serviceRepository: ServiceRepository, private val radioController: RadioController, private val bluetoothRepository: BluetoothRepository, diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt index 959c4ff3f..45fcc2fbc 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/BLEDevices.kt @@ -19,6 +19,8 @@ package org.meshtastic.app.ui.connections.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -26,25 +28,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.common.scanner.rememberFilterState -import no.nordicsemi.android.common.scanner.view.ScannerView import org.jetbrains.compose.resources.stringResource -import org.meshtastic.app.model.DeviceListEntry import org.meshtastic.app.ui.connections.ScannerViewModel -import org.meshtastic.core.ble.AndroidBleDevice -import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN -import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.bluetooth_available_devices /** - * Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth - * permissions and hardware state using Nordic Common Libraries' ScannerView. + * Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. * * @param connectionState The current connection state of the MeshService. * @param selectedDevice The full address of the currently selected device. @@ -53,15 +47,6 @@ import org.meshtastic.core.resources.bluetooth_available_devices @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanModel: ScannerViewModel) { - val filterState = - rememberFilterState( - filter = { - Any { - ServiceUuid(SERVICE_UUID) - Name(Regex(BLE_NAME_PATTERN)) - } - }, - ) val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle() Column { @@ -72,17 +57,8 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod color = MaterialTheme.colorScheme.primary, ) - ScannerView( - state = filterState, - onScanResultSelected = { result -> - scanModel.onSelected(DeviceListEntry.Ble(AndroidBleDevice(result.peripheral))) - }, - deviceItem = { result -> - val device = - remember(result.peripheral.address, bleDevices) { - bleDevices.find { it.fullAddress == "x${result.peripheral.address}" } - ?: DeviceListEntry.Ble(AndroidBleDevice(result.peripheral)) - } + LazyColumn(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + items(bleDevices, key = { it.fullAddress }) { device -> Card( modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), shape = MaterialTheme.shapes.large, @@ -94,10 +70,10 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod ?: ConnectionState.Disconnected, device = device, onSelect = { scanModel.onSelected(device) }, - rssi = result.rssi, + rssi = null, ) } - }, - ) + } + } } } diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt index 8fe790763..e25587d41 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/DeviceListItem.kt @@ -16,11 +16,9 @@ */ package org.meshtastic.app.ui.connections.components -import androidx.compose.foundation.Indication -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -50,7 +48,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import no.nordicsemi.android.common.ui.view.RssiIcon @@ -66,7 +63,7 @@ import org.meshtastic.core.ui.component.NodeChip private const val RSSI_UPDATE_RATE_MS = 2000L -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun DeviceListItem( @@ -115,17 +112,11 @@ fun DeviceListItem( is DeviceListEntry.Mock -> stringResource(Res.string.add) } - val useSelectable = modifier == Modifier - val interactionSource = remember { MutableInteractionSource() } - val indication: Indication = LocalIndication.current - val clickableModifier = - if (useSelectable) { - Modifier.indication(interactionSource, indication).pointerInput(device.fullAddress, onDelete) { - detectTapGestures(onTap = { onSelect() }, onLongPress = onDelete?.let { { it() } }) - } + if (onDelete != null) { + Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete) } else { - Modifier + Modifier.clickable(onClick = onSelect) } ListItem( diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt b/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt index f50acc4e7..b637b5080 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/node/AdaptiveNodeListScreen.kt @@ -20,9 +20,11 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -46,6 +48,10 @@ import androidx.navigation.NavHostController import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import org.meshtastic.app.node.AndroidCompassViewModel +import org.meshtastic.app.node.AndroidNodeDetailViewModel +import org.meshtastic.app.node.AndroidNodeListViewModel import org.meshtastic.core.navigation.ChannelsRoutes import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.resources.Res @@ -65,6 +71,7 @@ fun AdaptiveNodeListScreen( initialNodeId: Int? = null, onNavigateToMessages: (String) -> Unit = {}, ) { + val nodeListViewModel: AndroidNodeListViewModel = koinViewModel() val navigator = rememberListDetailPaneScaffoldNavigator() val scope = rememberCoroutineScope() val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange @@ -118,6 +125,7 @@ fun AdaptiveNodeListScreen( // Prevent TextFields from auto-focusing when pane animates in LaunchedEffect(Unit) { focusManager.clearFocus() } NodeListScreen( + viewModel = nodeListViewModel, navigateToNodeDetails = { nodeId -> scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) } }, @@ -134,8 +142,12 @@ fun AdaptiveNodeListScreen( navigator.currentDestination?.contentKey?.let { nodeId -> key(nodeId) { LaunchedEffect(nodeId) { focusManager.clearFocus() } + val nodeDetailViewModel: AndroidNodeDetailViewModel = koinViewModel() + val compassViewModel: AndroidCompassViewModel = koinViewModel() NodeDetailScreen( nodeId = nodeId, + viewModel = nodeDetailViewModel, + compassViewModel = compassViewModel, navigateToMessages = onNavigateToMessages, onNavigate = { route -> navController.navigate(route) }, onNavigateUp = handleBack, @@ -147,6 +159,18 @@ fun AdaptiveNodeListScreen( ) } +@Composable +fun NodeTabTitle() { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = MeshtasticIcons.Nodes, contentDescription = null, modifier = Modifier.padding(end = 8.dp)) + Text( + text = stringResource(Res.string.nodes), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + @Composable private fun PlaceholderScreen() { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt index 627822b9a..eae4214c4 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/Channel.kt @@ -64,11 +64,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.common.util.toPlatformUri import org.meshtastic.core.model.Channel import org.meshtastic.core.model.ConnectionState @@ -112,8 +112,8 @@ import org.meshtastic.proto.Config @Composable @Suppress("LongMethod") fun ChannelScreen( - viewModel: ChannelViewModel = hiltViewModel(), - radioConfigViewModel: RadioConfigViewModel = hiltViewModel(), + viewModel: ChannelViewModel = koinViewModel(), + radioConfigViewModel: RadioConfigViewModel = koinViewModel(), onNavigate: (Route) -> Unit, onNavigateUp: () -> Unit, ) { diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt index 0fad35a09..a6810c3af 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/sharing/ChannelViewModel.kt @@ -20,10 +20,10 @@ 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.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.repository.DataPair @@ -35,12 +35,9 @@ import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig -import javax.inject.Inject -@HiltViewModel -class ChannelViewModel -@Inject -constructor( +@KoinViewModel +class ChannelViewModel( private val radioController: RadioController, private val radioConfigRepository: RadioConfigRepository, private val analytics: PlatformAnalytics, diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt index 5753f8040..c73a0e76a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt +++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidget.kt @@ -63,11 +63,9 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent import org.jetbrains.compose.resources.stringResource +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.util.formatUptime @@ -94,22 +92,16 @@ import org.meshtastic.core.resources.refresh import org.meshtastic.core.resources.updated import org.meshtastic.core.resources.uptime -class LocalStatsWidget : GlanceAppWidget() { +class LocalStatsWidget : + GlanceAppWidget(), + KoinComponent { override val sizeMode: SizeMode = SizeMode.Responsive(RESPONSIVE_SIZES) override val previewSizeMode: androidx.glance.appwidget.PreviewSizeMode = SizeMode.Responsive(RESPONSIVE_SIZES) - @EntryPoint - @InstallIn(SingletonComponent::class) - interface LocalStatsWidgetEntryPoint { - fun widgetStateProvider(): LocalStatsWidgetStateProvider - } + private val stateProvider: LocalStatsWidgetStateProvider by inject() override suspend fun provideGlance(context: Context, id: GlanceId) { - val entryPoint = - EntryPointAccessors.fromApplication(context.applicationContext, LocalStatsWidgetEntryPoint::class.java) - val stateProvider = entryPoint.widgetStateProvider() - provideContent { val state by stateProvider.state.collectAsState() WidgetContent(state) @@ -117,9 +109,6 @@ class LocalStatsWidget : GlanceAppWidget() { } override suspend fun providePreview(context: Context, widgetCategory: Int) { - val entryPoint = - EntryPointAccessors.fromApplication(context.applicationContext, LocalStatsWidgetEntryPoint::class.java) - val stateProvider = entryPoint.widgetStateProvider() val currentState = stateProvider.state.value val stateToRender = diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt index 2b162b9b8..28409d0f5 100644 --- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt +++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetReceiver.kt @@ -18,9 +18,7 @@ package org.meshtastic.app.widget import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver -import dagger.hilt.android.AndroidEntryPoint -@AndroidEntryPoint class LocalStatsWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = LocalStatsWidget() } diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt index b4d643d43..873ff90e8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt +++ b/app/src/main/kotlin/org/meshtastic/app/widget/LocalStatsWidgetState.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node @@ -36,8 +37,6 @@ import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.LocalStats -import javax.inject.Inject -import javax.inject.Singleton data class LocalStatsWidgetUiState( val connectionState: ConnectionState = ConnectionState.Disconnected, @@ -79,10 +78,8 @@ data class LocalStatsWidgetUiState( val updateTimeMillis: Long = 0, ) -@Singleton -class LocalStatsWidgetStateProvider -@Inject -constructor( +@Single +class LocalStatsWidgetStateProvider( nodeRepository: NodeRepository, serviceRepository: ServiceRepository, appWidgetUpdater: AppWidgetUpdater, diff --git a/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt b/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt index e8a060681..291fc395e 100644 --- a/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt +++ b/app/src/main/kotlin/org/meshtastic/app/widget/RefreshLocalStatsAction.kt @@ -20,30 +20,20 @@ import android.content.Context import androidx.glance.GlanceId import androidx.glance.action.ActionParameters import androidx.glance.appwidget.action.ActionCallback -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.NodeManager -class RefreshLocalStatsAction : ActionCallback { +class RefreshLocalStatsAction : + ActionCallback, + KoinComponent { - @EntryPoint - @InstallIn(SingletonComponent::class) - interface RefreshLocalStatsEntryPoint { - fun commandSender(): CommandSender - - fun nodeManager(): NodeManager - } + private val commandSender: CommandSender by inject() + private val nodeManager: NodeManager by inject() override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - val entryPoint = - EntryPointAccessors.fromApplication(context.applicationContext, RefreshLocalStatsEntryPoint::class.java) - val commandSender = entryPoint.commandSender() - val nodeManager = entryPoint.nodeManager() - val myNodeNum = nodeManager.myNodeNum ?: return commandSender.requestTelemetry(commandSender.generatePacketId(), myNodeNum, TelemetryType.LOCAL_STATS.ordinal) diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt b/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt index e4e34a99d..11495b645 100644 --- a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt +++ b/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt @@ -17,40 +17,21 @@ package org.meshtastic.app.worker import android.content.Context -import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import co.touchlab.kermit.Logger -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent +import org.koin.android.annotation.KoinWorker import org.meshtastic.core.repository.MeshLogPrefs import org.meshtastic.core.repository.MeshLogRepository -@HiltWorker -class MeshLogCleanupWorker -@AssistedInject -constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, +@KoinWorker +class MeshLogCleanupWorker( + appContext: Context, + workerParams: WorkerParameters, private val meshLogRepository: MeshLogRepository, private val meshLogPrefs: MeshLogPrefs, ) : CoroutineWorker(appContext, workerParams) { - // Fallback constructor for cases where HiltWorkerFactory is not used (e.g., some WorkManager initializations) - constructor( - appContext: Context, - workerParams: WorkerParameters, - ) : this( - appContext, - workerParams, - entryPoint(appContext).meshLogRepository(), - entryPoint(appContext).meshLogPrefs(), - ) - @Suppress("TooGenericExceptionCaught") override suspend fun doWork(): Result = try { val retentionDays = meshLogPrefs.retentionDays.value @@ -77,18 +58,7 @@ constructor( companion object { const val WORK_NAME = "meshlog_cleanup_worker" - - private fun entryPoint(context: Context): WorkerEntryPoint = - EntryPointAccessors.fromApplication(context.applicationContext, WorkerEntryPoint::class.java) } private val logger = Logger.withTag(WORK_NAME) } - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface WorkerEntryPoint { - fun meshLogRepository(): MeshLogRepository - - fun meshLogPrefs(): MeshLogPrefs -} diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt b/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt index ec443d408..b83fc9aff 100644 --- a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt +++ b/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt @@ -21,13 +21,11 @@ import android.content.Context import android.content.pm.ServiceInfo import android.os.Build import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import co.touchlab.kermit.Logger -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import org.koin.android.annotation.KoinWorker import org.meshtastic.app.R import org.meshtastic.app.service.MeshService import org.meshtastic.app.service.startService @@ -39,12 +37,10 @@ import org.meshtastic.core.repository.SERVICE_NOTIFY_ID * `startForegroundService` is blocked by Android 14+ restrictions. It runs as an Expedited worker to gain temporary * foreground start privileges. */ -@HiltWorker -class ServiceKeepAliveWorker -@AssistedInject -constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, +@KoinWorker +class ServiceKeepAliveWorker( + appContext: Context, + workerParams: WorkerParameters, private val serviceNotifications: MeshServiceNotifications, ) : CoroutineWorker(appContext, workerParams) { diff --git a/feature/settings/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml similarity index 100% rename from feature/settings/src/main/res/xml/locales_config.xml rename to app/src/main/res/xml/locales_config.xml diff --git a/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt b/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt deleted file mode 100644 index 45381aa98..000000000 --- a/app/src/test/kotlin/org/meshtastic/app/MeshTestApplication.kt +++ /dev/null @@ -1,52 +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 . - */ -package org.meshtastic.app - -import androidx.work.Configuration -import dagger.hilt.android.EntryPointAccessors - -/** - * A lightweight application class for Robolectric tests. - * - * It prevents heavy background initialization (WorkManager, DatabaseManager) by default to avoid resource leaks and - * flaky native SQLite issues on the JVM. - */ -class MeshTestApplication : MeshUtilApplication() { - - override fun onCreate() { - // Only run real onCreate logic if a test explicitly asks for it - if (shouldInitialize) { - super.onCreate() - } - } - - override fun onTerminate() { - if (shouldInitialize) { - val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java) - entryPoint.databaseManager().close() - } - super.onTerminate() - } - - override val workManagerConfiguration: Configuration - get() = Configuration.Builder().setMinimumLoggingLevel(android.util.Log.DEBUG).build() - - companion object { - /** Set to true in a test @Before block if you need real DB/WorkManager init. */ - var shouldInitialize = false - } -} diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt new file mode 100644 index 000000000..dce13a652 --- /dev/null +++ b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt @@ -0,0 +1,56 @@ +/* + * 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 . + */ +package org.meshtastic.app.di + +import android.app.Application +import android.content.Context +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.SavedStateHandle +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import kotlinx.coroutines.CoroutineDispatcher +import okhttp3.OkHttpClient +import org.junit.Test +import org.koin.test.verify.verify +import org.meshtastic.core.model.util.NodeIdLookup + +class KoinVerificationTest { + + @Test + fun verifyKoinConfiguration() { + AppKoinModule() + .module() + .verify( + extraTypes = + listOf( + Application::class, + Context::class, + Lifecycle::class, + SavedStateHandle::class, + WorkerParameters::class, + WorkManager::class, + CoroutineDispatcher::class, + NodeIdLookup::class, + HttpClient::class, + HttpClientEngine::class, + OkHttpClient::class, + ), + ) + } +} diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 1208de17f..041693fbb 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { compileOnly(libs.dokka.gradlePlugin) compileOnly(libs.firebase.crashlytics.gradlePlugin) compileOnly(libs.google.services.gradlePlugin) - compileOnly(libs.hilt.gradlePlugin) + compileOnly(libs.koin.gradlePlugin) implementation(libs.kover.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.ksp.gradlePlugin) @@ -144,9 +144,9 @@ gradlePlugin { id = "meshtastic.analytics" implementationClass = "AnalyticsConventionPlugin" } - register("meshtasticHilt") { - id = "meshtastic.hilt" - implementationClass = "HiltConventionPlugin" + register("meshtasticKoin") { + id = "meshtastic.koin" + implementationClass = "KoinConventionPlugin" } register("meshtasticDetekt") { id = "meshtastic.detekt" diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt similarity index 52% rename from build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt rename to build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt index f570e721e..9539f439d 100644 --- a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,33 +19,31 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.dependencies -import org.meshtastic.buildlogic.library import org.meshtastic.buildlogic.libs +import org.meshtastic.buildlogic.plugin -class HiltConventionPlugin : Plugin { +class KoinConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - apply(plugin = "com.google.devtools.ksp") + apply(plugin = libs.plugin("koin-compiler").get().pluginId) - dependencies { - "ksp"(libs.library("hilt-compiler")) - "implementation"(libs.library("hilt-android")) - } + val koinAnnotations = libs.findLibrary("koin-annotations").get() + val koinCore = libs.findLibrary("koin-core").get() - // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm - pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { + pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { dependencies { - "implementation"(libs.library("hilt-core")) + add("commonMainApi", koinCore) + add("commonMainApi", koinAnnotations) } } - pluginManager.withPlugin("com.android.base") { - apply(plugin = "dagger.hilt.android.plugin") - } - - pluginManager.withPlugin("org.jetbrains.kotlin.plugin.compose") { - dependencies { - "implementation"(libs.library("androidx-hilt-lifecycle-viewmodel-compose")) + pluginManager.withPlugin("org.jetbrains.kotlin.android") { + // If this is *only* an Android module (no KMP plugin) + if (!pluginManager.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + dependencies { + add("implementation", koinCore) + add("implementation", koinAnnotations) + } } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt index e3bb46435..6a01d75ba 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt @@ -29,7 +29,7 @@ fun Project.configureDokka() { dokkaSourceSets.configureEach { perPackageOption { - matchingRegex.set("hilt_aggregated_deps") + matchingRegex.set("koin_aggregated_deps") suppress.set(true) } perPackageOption { diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt index b4c4deedd..20b542977 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt @@ -47,8 +47,6 @@ fun Project.configureKover() { // Exclude declarations annotatedBy( - "*.HiltAndroidApp", - "*.AndroidEntryPoint", "*.Module", "*.Provides", "*.Binds", @@ -56,7 +54,7 @@ fun Project.configureKover() { ) // Suppress generated code - packages("hilt_aggregated_deps") + packages("koin_aggregated_deps") packages("org.meshtastic.core.resources") } } diff --git a/build.gradle.kts b/build.gradle.kts index 78b748ae5..94e4fd3c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,10 +24,10 @@ plugins { alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.datadog) apply false alias(libs.plugins.devtools.ksp) apply false + alias(libs.plugins.koin.compiler) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.google.services) apply false - alias(libs.plugins.hilt) apply false alias(libs.plugins.room) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false diff --git a/core/ble/README.md b/core/ble/README.md index 02b893b33..29b3d2756 100644 --- a/core/ble/README.md +++ b/core/ble/README.md @@ -75,7 +75,7 @@ The module follows a clean architecture approach: - **Repository Pattern:** `BluetoothRepository` mediates data access. - **Coroutines & Flow:** All asynchronous operations use Kotlin Coroutines and Flows. -- **Dependency Injection:** Hilt is used for dependency injection. +- **Dependency Injection:** Koin is used for dependency injection. ## Testing diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts index 191a335be..a5e0d36eb 100644 --- a/core/ble/build.gradle.kts +++ b/core/ble/build.gradle.kts @@ -17,7 +17,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -35,11 +35,9 @@ kotlin { implementation(libs.kermit) implementation(libs.kotlinx.coroutines.core) - api(libs.javax.inject) } androidMain.dependencies { - implementation(libs.hilt.android) api(libs.nordic.client.android) api(libs.nordic.ble.env.android) api(libs.nordic.ble.env.android.compose) @@ -65,5 +63,3 @@ kotlin { } } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt index 6166287ef..ff6123a59 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt @@ -18,13 +18,11 @@ package org.meshtastic.core.ble import kotlinx.coroutines.CoroutineScope import no.nordicsemi.kotlin.ble.client.android.CentralManager -import javax.inject.Inject -import javax.inject.Singleton +import org.koin.core.annotation.Single /** An Android implementation of [BleConnectionFactory]. */ -@Singleton -class AndroidBleConnectionFactory @Inject constructor(private val centralManager: CentralManager) : - BleConnectionFactory { +@Single +class AndroidBleConnectionFactory(private val centralManager: CentralManager) : BleConnectionFactory { override fun create(scope: CoroutineScope, tag: String): BleConnection = AndroidBleConnection(centralManager, scope, tag) } diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt index 828ed6d10..8d1ff6008 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import no.nordicsemi.kotlin.ble.client.android.CentralManager import no.nordicsemi.kotlin.ble.client.distinctByPeripheral -import javax.inject.Inject +import org.koin.core.annotation.Single import kotlin.time.Duration /** @@ -28,7 +28,8 @@ import kotlin.time.Duration * * @param centralManager The Nordic [CentralManager] to use for scanning. */ -class AndroidBleScanner @Inject constructor(private val centralManager: CentralManager) : BleScanner { +@Single +class AndroidBleScanner(private val centralManager: CentralManager) : BleScanner { override fun scan(timeout: Duration): Flow = centralManager.scan(timeout).distinctByPeripheral().map { AndroidBleDevice(it.peripheral) } diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt index 24137e8a2..0b5663071 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt @@ -29,20 +29,17 @@ import no.nordicsemi.kotlin.ble.client.RemoteServices import no.nordicsemi.kotlin.ble.client.android.CentralManager import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.di.ProcessLifecycle -import javax.inject.Inject -import javax.inject.Singleton /** Android implementation of [BluetoothRepository]. */ -@Singleton -class AndroidBluetoothRepository -@Inject -constructor( +@Single +class AndroidBluetoothRepository( private val dispatchers: CoroutineDispatchers, - @ProcessLifecycle private val processLifecycle: Lifecycle, + @Named("ProcessLifecycle") private val processLifecycle: Lifecycle, private val centralManager: CentralManager, private val androidEnvironment: AndroidEnvironment, ) : BluetoothRepository { diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt new file mode 100644 index 000000000..8e8a8b128 --- /dev/null +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/di/CoreBleAndroidModule.kt @@ -0,0 +1,49 @@ +/* + * 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 . + */ +package org.meshtastic.core.ble.di + +import android.app.Application +import android.location.LocationManager +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +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.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@ComponentScan("org.meshtastic.core.ble") +class CoreBleAndroidModule { + @Single + fun provideAndroidEnvironment(app: Application): AndroidEnvironment = + NativeAndroidEnvironment.getInstance(app, isNeverForLocationFlagSet = true) + + @Single + fun provideCentralManager(environment: AndroidEnvironment): CentralManager = CentralManager.native( + environment as NativeAndroidEnvironment, + CoroutineScope(SupervisorJob() + Dispatchers.Default), + ) + + @Single + fun provideLocationManager(app: Application): LocationManager = + ContextCompat.getSystemService(app, LocationManager::class.java)!! +} diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt new file mode 100644 index 000000000..f064fcb63 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/di/CoreBleModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.ble.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.ble") +class CoreBleModule diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 09d77c011..21cb3a2b0 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.kotlin.parcelize) + id("meshtastic.koin") } kotlin { diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt new file mode 100644 index 000000000..721a31749 --- /dev/null +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/CoreCommonModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.common.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.common") +class CoreCommonModule diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt index 6046c68b6..31f103879 100644 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/SequentialJob.kt @@ -21,15 +21,16 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout +import org.koin.core.annotation.Factory import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject /** * A helper class that manages a single [Job]. When a new job is launched, any previous job is cancelled. This is useful * for ensuring that only the latest operation of a certain type is running at a time (e.g. for search or settings * updates). */ -class SequentialJob @Inject constructor() { +@Factory +class SequentialJob { private val job = AtomicReference() /** diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e2bd4480b..98bf7e0cd 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -18,7 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -41,7 +41,6 @@ kotlin { implementation(projects.core.prefs) implementation(projects.core.proto) - api(libs.javax.inject) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.paging.common) implementation(libs.kotlinx.serialization.json) @@ -51,7 +50,6 @@ kotlin { } androidMain.dependencies { - implementation(libs.hilt.android) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.location.altitude) @@ -68,5 +66,3 @@ kotlin { } } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/data/detekt-baseline.xml b/core/data/detekt-baseline.xml index 2354a0f89..c373eea43 100644 --- a/core/data/detekt-baseline.xml +++ b/core/data/detekt-baseline.xml @@ -1,7 +1,5 @@ - - MaxLineLength:BootloaderOtaQuirksJsonDataSourceImpl.kt$BootloaderOtaQuirksJsonDataSourceImpl$class - + diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt index aa301ed7c..3bfd72cfa 100644 --- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/BootloaderOtaQuirksJsonDataSourceImpl.kt @@ -22,11 +22,11 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import org.koin.core.annotation.Single import org.meshtastic.core.model.BootloaderOtaQuirk -import javax.inject.Inject -class BootloaderOtaQuirksJsonDataSourceImpl @Inject constructor(private val application: Application) : - BootloaderOtaQuirksJsonDataSource { +@Single +class BootloaderOtaQuirksJsonDataSourceImpl(private val application: Application) : BootloaderOtaQuirksJsonDataSource { @OptIn(ExperimentalSerializationApi::class) override fun loadBootloaderOtaQuirksFromJsonAsset(): List = runCatching { val inputStream = application.assets.open("device_bootloader_ota_quirks.json") diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt index e741ad476..327cddcae 100644 --- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareJsonDataSourceImpl.kt @@ -20,11 +20,11 @@ import android.app.Application import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import org.koin.core.annotation.Single import org.meshtastic.core.model.NetworkDeviceHardware -import javax.inject.Inject -class DeviceHardwareJsonDataSourceImpl @Inject constructor(private val application: Application) : - DeviceHardwareJsonDataSource { +@Single +class DeviceHardwareJsonDataSourceImpl(private val application: Application) : DeviceHardwareJsonDataSource { // Use a tolerant JSON parser so that additional fields in the bundled asset // (e.g., "key") do not break deserialization on older app versions. diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt index bc745898c..c060f4b21 100644 --- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseJsonDataSourceImpl.kt @@ -20,11 +20,11 @@ import android.app.Application import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import org.koin.core.annotation.Single import org.meshtastic.core.model.NetworkFirmwareReleases -import javax.inject.Inject -class FirmwareReleaseJsonDataSourceImpl @Inject constructor(private val application: Application) : - FirmwareReleaseJsonDataSource { +@Single +class FirmwareReleaseJsonDataSourceImpl(private val application: Application) : FirmwareReleaseJsonDataSource { // Match the network client behavior: be tolerant of unknown fields so that // older app versions can read newer snapshots of firmware_releases.json. diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt new file mode 100644 index 000000000..e9fcd0552 --- /dev/null +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/di/CoreDataAndroidModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.data.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.data") +class CoreDataAndroidModule diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt index bea36529e..72460c33e 100644 --- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/repository/LocationRepositoryImpl.kt @@ -34,19 +34,16 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.Location import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.PlatformAnalytics -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class LocationRepositoryImpl -@Inject -constructor( +@Single +class LocationRepositoryImpl( private val context: Application, - private val locationManager: dagger.Lazy, + private val locationManager: Lazy, private val analytics: PlatformAnalytics, private val dispatchers: CoroutineDispatchers, ) : LocationRepository { @@ -125,5 +122,5 @@ constructor( /** Observable flow for location updates */ @RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION]) - override fun getLocations(): Flow = locationManager.get().requestLocationUpdates() + override fun getLocations(): Flow = locationManager.value.requestLocationUpdates() } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt index a73a65899..918ff6c18 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt @@ -17,16 +17,15 @@ package org.meshtastic.core.data.datasource import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.DeviceHardwareEntity import org.meshtastic.core.database.entity.asEntity import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkDeviceHardware -import javax.inject.Inject -class DeviceHardwareLocalDataSource -@Inject -constructor( +@Single +class DeviceHardwareLocalDataSource( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt index 3f1a05c7f..3f93e901e 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.data.datasource import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.FirmwareReleaseEntity import org.meshtastic.core.database.entity.FirmwareReleaseType @@ -24,11 +25,9 @@ import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.database.entity.asEntity import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkFirmwareRelease -import javax.inject.Inject -class FirmwareReleaseLocalDataSource -@Inject -constructor( +@Single +class FirmwareReleaseLocalDataSource( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt index 35d9c0848..5fd91b26f 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt @@ -18,16 +18,14 @@ package org.meshtastic.core.data.datasource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.database.entity.NodeWithRelations -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class SwitchingNodeInfoReadDataSource @Inject constructor(private val dbManager: DatabaseManager) : - NodeInfoReadDataSource { +@Single +class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseManager) : NodeInfoReadDataSource { override fun myNodeInfoFlow(): Flow = dbManager.currentDb.flatMapLatest { db -> db.nodeInfoDao().getMyNodeInfo() } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt index 6b5501910..31d41fe9e 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.kt @@ -17,18 +17,15 @@ package org.meshtastic.core.data.datasource import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.MetadataEntity import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.di.CoroutineDispatchers -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class SwitchingNodeInfoWriteDataSource -@Inject -constructor( +@Single +class SwitchingNodeInfoWriteDataSource( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) : NodeInfoWriteDataSource { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt new file mode 100644 index 000000000..834cff2c2 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt @@ -0,0 +1,29 @@ +/* + * 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 . + */ +package org.meshtastic.core.data.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.core.model.util.MeshDataMapper +import org.meshtastic.core.model.util.NodeIdLookup + +@Module +@ComponentScan("org.meshtastic.core.data") +class CoreDataModule { + @Single fun provideMeshDataMapper(nodeIdLookup: NodeIdLookup): MeshDataMapper = MeshDataMapper(nodeIdLookup) +} diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index c137ea8f6..b296cef01 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import okio.ByteString import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus @@ -46,17 +47,13 @@ import org.meshtastic.proto.Neighbor import org.meshtastic.proto.NeighborInfo import org.meshtastic.proto.PortNum import org.meshtastic.proto.Telemetry -import javax.inject.Inject -import javax.inject.Singleton import kotlin.math.absoluteValue import kotlin.random.Random import kotlin.time.Duration.Companion.hours @Suppress("TooManyFunctions", "CyclomaticComplexMethod") -@Singleton -class CommandSenderImpl -@Inject -constructor( +@Single +class CommandSenderImpl( private val packetHandler: PacketHandler, private val nodeManager: NodeManager, private val radioConfigRepository: RadioConfigRepository, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt index 081d1a207..34bc23128 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.core.data.manager -import dagger.Lazy +import org.koin.core.annotation.Single import org.meshtastic.core.repository.FromRadioPacketHandler import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MeshServiceNotifications @@ -24,14 +24,10 @@ import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.FromRadio -import javax.inject.Inject -import javax.inject.Singleton /** Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */ -@Singleton -class FromRadioPacketHandlerImpl -@Inject -constructor( +@Single +class FromRadioPacketHandlerImpl( private val serviceRepository: ServiceRepository, private val router: Lazy, private val mqttManager: MqttManager, @@ -52,18 +48,18 @@ constructor( val clientNotification = proto.clientNotification when { - myInfo != null -> router.get().configFlowManager.handleMyInfo(myInfo) - metadata != null -> router.get().configFlowManager.handleLocalMetadata(metadata) + myInfo != null -> router.value.configFlowManager.handleMyInfo(myInfo) + metadata != null -> router.value.configFlowManager.handleLocalMetadata(metadata) nodeInfo != null -> { - router.get().configFlowManager.handleNodeInfo(nodeInfo) - serviceRepository.setConnectionProgress("Nodes (${router.get().configFlowManager.newNodeCount})") + router.value.configFlowManager.handleNodeInfo(nodeInfo) + serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})") } - configCompleteId != null -> router.get().configFlowManager.handleConfigComplete(configCompleteId) + configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId) mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage) queueStatus != null -> packetHandler.handleQueueStatus(queueStatus) - config != null -> router.get().configHandler.handleDeviceConfig(config) - moduleConfig != null -> router.get().configHandler.handleModuleConfig(moduleConfig) - channel != null -> router.get().configHandler.handleChannel(channel) + config != null -> router.value.configHandler.handleDeviceConfig(config) + moduleConfig != null -> router.value.configHandler.handleModuleConfig(moduleConfig) + channel != null -> router.value.configHandler.handleChannel(channel) clientNotification != null -> { serviceRepository.setClientNotification(clientNotification) serviceNotifications.showClientNotification(clientNotification) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index 085966a2b..09961847f 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Single import org.meshtastic.core.repository.HistoryManager import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.PacketHandler @@ -26,16 +27,9 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.PortNum import org.meshtastic.proto.StoreAndForward -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class HistoryManagerImpl -@Inject -constructor( - private val meshPrefs: MeshPrefs, - private val packetHandler: PacketHandler, -) : HistoryManager { +@Single +class HistoryManagerImpl(private val meshPrefs: MeshPrefs, private val packetHandler: PacketHandler) : HistoryManager { companion object { private const val HISTORY_TAG = "HistoryReplay" diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index f2a5e7c8b..dcc0cc4a3 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -16,11 +16,11 @@ */ package org.meshtastic.core.data.manager -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.ignoreException @@ -49,14 +49,10 @@ import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.OTAMode import org.meshtastic.proto.PortNum import org.meshtastic.proto.User -import javax.inject.Inject -import javax.inject.Singleton @Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") -@Singleton -class MeshActionHandlerImpl -@Inject -constructor( +@Single +class MeshActionHandlerImpl( private val nodeManager: NodeManager, private val commandSender: CommandSender, private val packetRepository: Lazy, @@ -123,7 +119,7 @@ constructor( } } nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnoredStatus) } - scope.handledLaunch { packetRepository.get().updateFilteredBySender(node.user.id, newIgnoredStatus) } + scope.handledLaunch { packetRepository.value.updateFilteredBySender(node.user.id, newIgnoredStatus) } } private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) { @@ -177,7 +173,7 @@ constructor( to = action.contactKey.substring(1), channel = action.contactKey[0].digitToInt(), ) - packetRepository.get().insertReaction(reaction, myNodeNum) + packetRepository.value.insertReaction(reaction, myNodeNum) } } @@ -190,7 +186,7 @@ constructor( override fun handleSend(p: DataPacket, myNodeNum: Int) { commandSender.sendData(p) serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN) - dataHandler.get().rememberDataPacket(p, myNodeNum, false) + dataHandler.value.rememberDataPacket(p, myNodeNum, false) val bytes = p.bytes ?: okio.ByteString.EMPTY analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType)) } @@ -348,7 +344,7 @@ constructor( meshPrefs.setDeviceAddress(deviceAddr) scope.handledLaunch { nodeManager.clear() - messageProcessor.get().clearEarlyPackets() + messageProcessor.value.clearEarlyPackets() databaseManager.switchActiveDatabase(deviceAddr) serviceNotifications.clearNotifications() nodeManager.loadCachedNodeDB() diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index d0daf20ed..ff20feddb 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -17,12 +17,12 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import okio.IOException +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.repository.CommandSender @@ -40,16 +40,12 @@ import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.NodeInfo import org.meshtastic.proto.ToRadio -import javax.inject.Inject -import javax.inject.Singleton import org.meshtastic.core.model.MyNodeInfo as SharedMyNodeInfo import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo @Suppress("LongParameterList", "TooManyFunctions") -@Singleton -class MeshConfigFlowManagerImpl -@Inject -constructor( +@Single +class MeshConfigFlowManagerImpl( private val nodeManager: NodeManager, private val connectionManager: Lazy, private val nodeRepository: NodeRepository, @@ -101,7 +97,7 @@ constructor( } else { myNodeInfo = finalizedInfo Logger.i { "myNodeInfo committed successfully (nodeNum=${finalizedInfo.myNodeNum})" } - connectionManager.get().onRadioConfigLoaded() + connectionManager.value.onRadioConfigLoaded() } scope.handledLaunch { @@ -109,7 +105,7 @@ constructor( sendHeartbeat() delay(wantConfigDelay) Logger.i { "Requesting NodeInfo (Stage 2)" } - connectionManager.get().startNodeInfoOnly() + connectionManager.value.startNodeInfoOnly() } } @@ -140,7 +136,7 @@ constructor( nodeManager.setAllowNodeDbWrites(true) serviceRepository.setConnectionState(ConnectionState.Connected) serviceBroadcasts.broadcastConnection() - connectionManager.get().onNodeDbReady() + connectionManager.value.onNodeDbReady() } } @@ -172,7 +168,7 @@ constructor( } override fun triggerWantConfig() { - connectionManager.get().startConfigOnly() + connectionManager.value.startConfigOnly() } private fun regenMyNodeInfo(metadata: DeviceMetadata? = null) { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index d5ff32426..652e3bb79 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.MeshConfigHandler import org.meshtastic.core.repository.NodeManager @@ -33,13 +34,9 @@ import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.ModuleConfig -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MeshConfigHandlerImpl -@Inject -constructor( +@Single +class MeshConfigHandlerImpl( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, private val nodeManager: NodeManager, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index eda76a0df..5e706c288 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds @@ -63,17 +64,13 @@ import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.Telemetry import org.meshtastic.proto.ToRadio -import javax.inject.Inject -import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @Suppress("LongParameterList", "TooManyFunctions") -@Singleton -class MeshConnectionManagerImpl -@Inject -constructor( +@Single +class MeshConnectionManagerImpl( private val radioInterfaceService: RadioInterfaceService, private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index ca8e3d01e..df1790709 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -18,7 +18,6 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -29,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okio.ByteString.Companion.toByteString import okio.IOException +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds @@ -76,8 +76,6 @@ import org.meshtastic.proto.StoreForwardPlusPlus import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User import org.meshtastic.proto.Waypoint -import javax.inject.Inject -import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds /** @@ -91,10 +89,8 @@ import kotlin.time.Duration.Companion.milliseconds * 5. Tracking received telemetry for node updates. */ @Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod") -@Singleton -class MeshDataHandlerImpl -@Inject -constructor( +@Single +class MeshDataHandlerImpl( private val nodeManager: NodeManager, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, @@ -291,17 +287,15 @@ constructor( "to=${sfpp.encapsulated_to} myNodeNum=${nodeManager.myNodeNum} status=$status" } scope.handledLaunch { - packetRepository - .get() - .updateSFPPStatus( - packetId = sfpp.encapsulated_id, - from = sfpp.encapsulated_from, - to = sfpp.encapsulated_to, - hash = hash, - status = status, - rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL, - myNodeNum = nodeManager.myNodeNum ?: 0, - ) + packetRepository.value.updateSFPPStatus( + packetId = sfpp.encapsulated_id, + from = sfpp.encapsulated_from, + to = sfpp.encapsulated_to, + hash = hash, + status = status, + rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL, + myNodeNum = nodeManager.myNodeNum ?: 0, + ) serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulated_id, status) } } @@ -309,13 +303,11 @@ constructor( StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE -> { scope.handledLaunch { sfpp.message_hash.let { - packetRepository - .get() - .updateSFPPStatusByHash( - hash = it.toByteArray(), - status = MessageStatus.SFPP_CONFIRMED, - rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL, - ) + packetRepository.value.updateSFPPStatusByHash( + hash = it.toByteArray(), + status = MessageStatus.SFPP_CONFIRMED, + rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL, + ) } } } @@ -359,20 +351,20 @@ constructor( val fromNum = packet.from u.get_module_config_response?.let { if (fromNum == myNodeNum) { - configHandler.get().handleModuleConfig(it) + configHandler.value.handleModuleConfig(it) } else { it.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) } } } if (fromNum == myNodeNum) { - u.get_config_response?.let { configHandler.get().handleDeviceConfig(it) } - u.get_channel_response?.let { configHandler.get().handleChannel(it) } + u.get_config_response?.let { configHandler.value.handleDeviceConfig(it) } + u.get_channel_response?.let { configHandler.value.handleChannel(it) } } u.get_device_metadata_response?.let { if (fromNum == myNodeNum) { - configFlowManager.get().handleLocalMetadata(it) + configFlowManager.value.handleLocalMetadata(it) } else { nodeManager.insertMetadata(fromNum, it) } @@ -414,7 +406,7 @@ constructor( val fromNum = packet.from val isRemote = (fromNum != myNodeNum) if (!isRemote) { - connectionManager.get().updateTelemetry(t) + connectionManager.value.updateTelemetry(t) } nodeManager.updateNode(fromNum) { node: Node -> @@ -508,8 +500,8 @@ constructor( private fun handleAckNak(requestId: Int, fromId: String, routingError: Int, relayNode: Int?) { scope.handledLaunch { val isAck = routingError == Routing.Error.NONE.value - val p = packetRepository.get().getPacketByPacketId(requestId) - val reaction = packetRepository.get().getReactionByPacketId(requestId) + val p = packetRepository.value.getPacketByPacketId(requestId) + val reaction = packetRepository.value.getReactionByPacketId(requestId) @Suppress("MaxLineLength") Logger.d { @@ -527,7 +519,7 @@ constructor( if (p != null && p.status != MessageStatus.RECEIVED) { val updatedPacket = p.copy(status = m, relays = if (isAck) p.relays + 1 else p.relays, relayNode = relayNode) - packetRepository.get().update(updatedPacket) + packetRepository.value.update(updatedPacket) } reaction?.let { r -> @@ -536,7 +528,7 @@ constructor( if (isAck) { updated = updated.copy(relays = updated.relays + 1) } - packetRepository.get().updateReaction(updated) + packetRepository.value.updateReaction(updated) } } @@ -601,7 +593,7 @@ constructor( val contactKey = "${dataPacket.channel}$contactId" scope.handledLaunch { - packetRepository.get().apply { + packetRepository.value.apply { // Check for duplicates before inserting val existingPackets = findPacketsWithId(dataPacket.id) if (existingPackets.isNotEmpty()) { @@ -646,7 +638,7 @@ constructor( contactKey: String, updateNotification: Boolean, ) { - val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted + val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true val isSilent = conversationMuted || nodeMuted if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) { @@ -733,7 +725,7 @@ constructor( ) // Check for duplicates before inserting - val existingReactions = packetRepository.get().findReactionsWithId(packet.id) + val existingReactions = packetRepository.value.findReactionsWithId(packet.id) if (existingReactions.isNotEmpty()) { Logger.d { "Skipping duplicate reaction: packetId=${packet.id} replyId=${decoded.reply_id} " + @@ -742,15 +734,15 @@ constructor( return@handledLaunch } - packetRepository.get().insertReaction(reaction, nodeManager.myNodeNum ?: 0) + packetRepository.value.insertReaction(reaction, nodeManager.myNodeNum ?: 0) // Find the original packet to get the contactKey - packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> + packetRepository.value.getPacketByPacketId(decoded.reply_id)?.let { originalPacket -> // Skip notification if the original message was filtered val targetId = if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from val contactKey = "${originalPacket.channel}$targetId" - val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted + val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true val isSilent = conversationMuted || nodeMuted diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 5ba3605c4..e2d150bc8 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -17,7 +17,6 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger -import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds @@ -43,16 +43,12 @@ import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum -import javax.inject.Inject -import javax.inject.Singleton import kotlin.uuid.Uuid /** Implementation of [MeshMessageProcessor] that handles raw radio messages and prepares mesh packets for routing. */ @Suppress("TooManyFunctions") -@Singleton -class MeshMessageProcessorImpl -@Inject -constructor( +@Single +class MeshMessageProcessorImpl( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val meshLogRepository: Lazy, @@ -246,7 +242,7 @@ constructor( } try { - router.get().dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob) + router.value.dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob) } finally { scope.launch { mapsMutex.withLock { @@ -258,5 +254,5 @@ constructor( } } - private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.get().insert(log) } + private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.value.insert(log) } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt index b079b1d86..d783ae773 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.core.data.manager -import dagger.Lazy import kotlinx.coroutines.CoroutineScope +import org.koin.core.annotation.Single import org.meshtastic.core.repository.MeshActionHandler import org.meshtastic.core.repository.MeshConfigFlowManager import org.meshtastic.core.repository.MeshConfigHandler @@ -26,15 +26,11 @@ import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NeighborInfoHandler import org.meshtastic.core.repository.TracerouteHandler -import javax.inject.Inject -import javax.inject.Singleton /** Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */ @Suppress("LongParameterList") -@Singleton -class MeshRouterImpl -@Inject -constructor( +@Single +class MeshRouterImpl( private val dataHandlerLazy: Lazy, private val configHandlerLazy: Lazy, private val tracerouteHandlerLazy: Lazy, @@ -44,25 +40,25 @@ constructor( private val actionHandlerLazy: Lazy, ) : MeshRouter { override val dataHandler: MeshDataHandler - get() = dataHandlerLazy.get() + get() = dataHandlerLazy.value override val configHandler: MeshConfigHandler - get() = configHandlerLazy.get() + get() = configHandlerLazy.value override val tracerouteHandler: TracerouteHandler - get() = tracerouteHandlerLazy.get() + get() = tracerouteHandlerLazy.value override val neighborInfoHandler: NeighborInfoHandler - get() = neighborInfoHandlerLazy.get() + get() = neighborInfoHandlerLazy.value override val configFlowManager: MeshConfigFlowManager - get() = configFlowManagerLazy.get() + get() = configFlowManagerLazy.value override val mqttManager: MqttManager - get() = mqttManagerLazy.get() + get() = mqttManagerLazy.value override val actionHandler: MeshActionHandler - get() = actionHandlerLazy.get() + get() = actionHandlerLazy.value override fun start(scope: CoroutineScope) { dataHandler.start(scope) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt index 17e7c5091..85693a2b4 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt @@ -17,14 +17,13 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger +import org.koin.core.annotation.Single import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter -import javax.inject.Inject -import javax.inject.Singleton /** Implementation of [MessageFilter] that uses regex and plain text matching. */ -@Singleton -class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs) : MessageFilter { +@Single +class MessageFilterImpl(private val filterPrefs: FilterPrefs) : MessageFilter { private var compiledPatterns: List = emptyList() init { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index 7684ebd20..d57fcc2b3 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -25,19 +25,16 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Single import org.meshtastic.core.network.repository.MQTTRepository import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.ToRadio -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MqttManagerImpl -@Inject -constructor( +@Single +class MqttManagerImpl( private val mqttRepository: MQTTRepository, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt index df19abacf..a9b63086a 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt @@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.NeighborInfoHandler @@ -29,13 +30,9 @@ import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.NeighborInfo import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class NeighborInfoHandlerImpl -@Inject -constructor( +@Single +class NeighborInfoHandlerImpl( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val commandSender: CommandSender, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index 120d79b08..ad477c446 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import okio.ByteString +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DeviceMetrics @@ -35,6 +36,7 @@ import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeInfo import org.meshtastic.core.model.Position +import org.meshtastic.core.model.util.NodeIdLookup import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.NodeRepository @@ -45,17 +47,13 @@ import org.meshtastic.proto.Paxcount import org.meshtastic.proto.StatusMessage import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User -import javax.inject.Inject -import javax.inject.Singleton import org.meshtastic.proto.NodeInfo as ProtoNodeInfo import org.meshtastic.proto.Position as ProtoPosition /** Implementation of [NodeManager] that maintains an in-memory database of the mesh. */ @Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod") -@Singleton -class NodeManagerImpl -@Inject -constructor( +@Single(binds = [NodeManager::class, NodeIdLookup::class]) +class NodeManagerImpl( private val nodeRepository: NodeRepository, private val serviceBroadcasts: ServiceBroadcasts, private val serviceNotifications: MeshServiceNotifications, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 1e6d37f67..85716ce44 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -17,7 +17,6 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger -import dagger.Lazy import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -29,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.entity.MeshLog @@ -48,17 +48,13 @@ import org.meshtastic.proto.FromRadio import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.QueueStatus import org.meshtastic.proto.ToRadio -import javax.inject.Inject -import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.uuid.Uuid @Suppress("TooManyFunctions") -@Singleton -class PacketHandlerImpl -@Inject -constructor( +@Single +class PacketHandlerImpl( private val packetRepository: Lazy, private val serviceBroadcasts: ServiceBroadcasts, private val radioInterfaceService: RadioInterfaceService, @@ -182,7 +178,7 @@ constructor( if (packetId != 0) { getDataPacketById(packetId)?.let { p -> if (p.status == m) return@handledLaunch - packetRepository.get().updateMessageStatus(p, m) + packetRepository.value.updateMessageStatus(p, m) serviceBroadcasts.broadcastMessageStatus(packetId, m) } } @@ -191,7 +187,7 @@ constructor( private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) { var dataPacket: DataPacket? = null while (dataPacket == null) { - dataPacket = packetRepository.get().getPacketById(packetId) + dataPacket = packetRepository.value.getPacketById(packetId) if (dataPacket == null) delay(100.milliseconds) } dataPacket @@ -222,7 +218,7 @@ constructor( "insert: ${packetToSave.message_type} = " + "${packetToSave.raw_message.toOneLineString()} from=${packetToSave.fromNum}" } - meshLogRepository.get().insert(packetToSave) + meshLogRepository.value.insert(packetToSave) } } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 2524e8301..a3d3c5491 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt @@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.data.repository.TracerouteSnapshotRepository @@ -34,13 +35,9 @@ import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.TracerouteHandler import org.meshtastic.proto.MeshPacket import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class TracerouteHandlerImpl -@Inject -constructor( +@Single +class TracerouteHandlerImpl( private val nodeManager: NodeManager, private val serviceRepository: ServiceRepository, private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt index d4901d02b..338a0d6ea 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.data.repository import co.touchlab.kermit.Logger import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource @@ -30,14 +31,10 @@ import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.network.DeviceHardwareRemoteDataSource import org.meshtastic.core.repository.DeviceHardwareRepository -import javax.inject.Inject -import javax.inject.Singleton // Annotating with Singleton to ensure a single instance manages the cache -@Singleton -class DeviceHardwareRepositoryImpl -@Inject -constructor( +@Single +class DeviceHardwareRepositoryImpl( private val remoteDataSource: DeviceHardwareRemoteDataSource, private val localDataSource: DeviceHardwareLocalDataSource, private val jsonDataSource: DeviceHardwareJsonDataSource, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt index 67ccdc091..d7b8340b3 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/FirmwareReleaseRepository.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.data.repository import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource import org.meshtastic.core.data.datasource.FirmwareReleaseLocalDataSource @@ -28,13 +29,9 @@ import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.database.entity.asExternalModel import org.meshtastic.core.model.util.TimeConstants import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class FirmwareReleaseRepository -@Inject -constructor( +@Single +class FirmwareReleaseRepository( private val remoteDataSource: FirmwareReleaseRemoteDataSource, private val localDataSource: FirmwareReleaseLocalDataSource, private val jsonDataSource: FirmwareReleaseJsonDataSource, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt index 7c09f1582..b620984f6 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.data.datasource.NodeInfoReadDataSource import org.meshtastic.core.database.DatabaseManager @@ -37,8 +38,6 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.PortNum import org.meshtastic.proto.Telemetry -import javax.inject.Inject -import javax.inject.Singleton /** * Repository implementation for managing and retrieving logs from the local database. @@ -47,10 +46,8 @@ import javax.inject.Singleton * telemetry and traceroute data. */ @Suppress("TooManyFunctions") -@Singleton -class MeshLogRepositoryImpl -@Inject -constructor( +@Single +class MeshLogRepositoryImpl( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, private val meshLogPrefs: MeshLogPrefs, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index 0b08c806f..8c4a3c1f6 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -34,6 +34,8 @@ import kotlinx.coroutines.flow.onEach 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.data.datasource.NodeInfoReadDataSource import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource import org.meshtastic.core.database.entity.MeshLog @@ -42,7 +44,6 @@ import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.entity.NodeEntity import org.meshtastic.core.datastore.LocalStatsDataSource import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.di.ProcessLifecycle import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node @@ -53,16 +54,12 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.LocalStats import org.meshtastic.proto.User -import javax.inject.Inject -import javax.inject.Singleton /** Repository for managing node-related data, including hardware info, node database, and identity. */ -@Singleton +@Single @Suppress("TooManyFunctions") -class NodeRepositoryImpl -@Inject -constructor( - @ProcessLifecycle private val processLifecycle: Lifecycle, +class NodeRepositoryImpl( + @Named("ProcessLifecycle") private val processLifecycle: Lifecycle, private val nodeInfoReadDataSource: NodeInfoReadDataSource, private val nodeInfoWriteDataSource: NodeInfoWriteDataSource, private val dispatchers: CoroutineDispatchers, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 7164d6876..32ac3f3f2 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.toReaction import org.meshtastic.core.di.CoroutineDispatchers @@ -37,19 +38,15 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.PortNum -import javax.inject.Inject import org.meshtastic.core.database.entity.ContactSettings as ContactSettingsEntity import org.meshtastic.core.database.entity.Packet as RoomPacket import org.meshtastic.core.database.entity.ReactionEntity as RoomReaction import org.meshtastic.core.repository.PacketRepository as SharedPacketRepository @Suppress("TooManyFunctions", "LongParameterList") -class PacketRepositoryImpl -@Inject -constructor( - private val dbManager: DatabaseManager, - private val dispatchers: CoroutineDispatchers, -) : SharedPacketRepository { +@Single +class PacketRepositoryImpl(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) : + SharedPacketRepository { override fun getWaypoints(): Flow> = dbManager.currentDb .flatMapLatest { db -> db.packetDao().getAllWaypointsFlow() } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt index 025518f86..94f4afaea 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/QuickChatActionRepository.kt @@ -19,17 +19,13 @@ package org.meshtastic.core.data.repository import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.QuickChatAction import org.meshtastic.core.di.CoroutineDispatchers -import javax.inject.Inject -class QuickChatActionRepository -@Inject -constructor( - private val dbManager: DatabaseManager, - private val dispatchers: CoroutineDispatchers, -) { +@Single +class QuickChatActionRepository(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) { fun getAllActions() = dbManager.currentDb.flatMapLatest { it.quickChatActionDao().getAll() }.flowOn(dispatchers.io) suspend fun upsert(action: QuickChatAction) = diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index d76ac8eee..b702d9cab 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.data.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.ChannelSetDataSource import org.meshtastic.core.datastore.LocalConfigDataSource import org.meshtastic.core.datastore.ModuleConfigDataSource @@ -32,15 +33,13 @@ import org.meshtastic.proto.DeviceProfile import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.ModuleConfig -import javax.inject.Inject /** * Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] & * [LocalModuleConfig]. */ -open class RadioConfigRepositoryImpl -@Inject -constructor( +@Single +open class RadioConfigRepositoryImpl( private val nodeDB: NodeRepository, private val channelSetDataSource: ChannelSetDataSource, private val localConfigDataSource: LocalConfigDataSource, diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt index e29572ac3..3b890c8f3 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/TracerouteSnapshotRepository.kt @@ -23,15 +23,14 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.TracerouteNodePositionEntity import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.proto.Position -import javax.inject.Inject -class TracerouteSnapshotRepository -@Inject -constructor( +@Single +class TracerouteSnapshotRepository( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, ) { diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index e1b0c414f..25b609198 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt @@ -46,7 +46,13 @@ class FromRadioPacketHandlerImplTest { @Before fun setup() { handler = - FromRadioPacketHandlerImpl(serviceRepository, { router }, mqttManager, packetHandler, serviceNotifications) + FromRadioPacketHandlerImpl( + serviceRepository, + lazy { router }, + mqttManager, + packetHandler, + serviceNotifications, + ) } @Test diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index b4eb95f9d..4ac471ec3 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.data.manager -import dagger.Lazy import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -58,19 +57,19 @@ class MeshDataHandlerTest { private val packetHandler: PacketHandler = mockk(relaxed = true) private val serviceRepository: ServiceRepository = mockk(relaxed = true) private val packetRepository: PacketRepository = mockk(relaxed = true) - private val packetRepositoryLazy: Lazy = mockk { every { get() } returns packetRepository } + private val packetRepositoryLazy: Lazy = lazy { packetRepository } private val serviceBroadcasts: ServiceBroadcasts = mockk(relaxed = true) private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true) private val analytics: PlatformAnalytics = mockk(relaxed = true) private val dataMapper: MeshDataMapper = mockk(relaxed = true) private val configHandler: MeshConfigHandler = mockk(relaxed = true) - private val configHandlerLazy: Lazy = mockk { every { get() } returns configHandler } + private val configHandlerLazy: Lazy = lazy { configHandler } private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true) - private val configFlowManagerLazy: Lazy = mockk { every { get() } returns configFlowManager } + private val configFlowManagerLazy: Lazy = lazy { configFlowManager } private val commandSender: CommandSender = mockk(relaxed = true) private val historyManager: HistoryManager = mockk(relaxed = true) private val connectionManager: MeshConnectionManager = mockk(relaxed = true) - private val connectionManagerLazy: Lazy = mockk { every { get() } returns connectionManager } + private val connectionManagerLazy: Lazy = lazy { connectionManager } private val tracerouteHandler: TracerouteHandler = mockk(relaxed = true) private val neighborInfoHandler: NeighborInfoHandler = mockk(relaxed = true) private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt index 2486922ac..619184abf 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt @@ -60,10 +60,10 @@ class PacketHandlerImplTest { handler = PacketHandlerImpl( - { packetRepository }, + lazy { packetRepository }, serviceBroadcasts, radioInterfaceService, - { meshLogRepository }, + lazy { meshLogRepository }, serviceRepository, ) handler.start(testScope) diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 026a9b410..30df0a046 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -20,6 +20,7 @@ plugins { alias(libs.plugins.meshtastic.android.room) alias(libs.plugins.meshtastic.kotlinx.serialization) alias(libs.plugins.kotlin.parcelize) + id("meshtastic.koin") } kotlin { diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index e5c96cd41..21e1f3f88 100644 --- a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -34,24 +34,19 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon import org.meshtastic.core.di.CoroutineDispatchers import java.io.File -import javax.inject.Inject -import javax.inject.Singleton import org.meshtastic.core.common.database.DatabaseManager as SharedDatabaseManager /** Manages per-device Room database instances for node data, with LRU eviction. */ -@Singleton +@Single @Suppress("TooManyFunctions") @OptIn(ExperimentalCoroutinesApi::class) -open class DatabaseManager -@Inject -constructor( - private val app: Application, - private val dispatchers: CoroutineDispatchers, -) : SharedDatabaseManager { +open class DatabaseManager(private val app: Application, private val dispatchers: CoroutineDispatchers) : + SharedDatabaseManager { val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE) private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt new file mode 100644 index 000000000..26b56484c --- /dev/null +++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.database.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.database") +class CoreDatabaseAndroidModule diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt new file mode 100644 index 000000000..5626c6269 --- /dev/null +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.database.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.database") +class CoreDatabaseModule diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index f94dc4779..c5a3286cd 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -18,7 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -29,12 +29,8 @@ kotlin { implementation(projects.core.proto) api(libs.androidx.datastore) api(libs.androidx.datastore.preferences) - api(libs.javax.inject) implementation(libs.kotlinx.serialization.json) implementation(libs.kermit) } - androidMain.dependencies { implementation(libs.hilt.android) } } } - -dependencies { "kspAndroid"(libs.hilt.compiler) } diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt similarity index 69% rename from app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt rename to core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt index 55611e300..61a991207 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/DataStoreModule.kt +++ b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.di +package org.meshtastic.core.datastore.di import android.content.Context import androidx.datastore.core.DataStore @@ -27,16 +27,12 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile -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 okio.FileSystem import okio.Path.Companion.toOkioPath +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.KEY_APP_INTRO_COMPLETED import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN import org.meshtastic.core.datastore.KEY_NODE_SORT @@ -52,36 +48,23 @@ import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.LocalStats -import javax.inject.Qualifier -import javax.inject.Singleton private const val USER_PREFERENCES_NAME = "user_preferences" -@Retention(AnnotationRetention.BINARY) -@Qualifier -annotation class DataStoreScope - -@InstallIn(SingletonComponent::class) @Module -object DataStoreModule { - - @Provides - @Singleton - @DataStoreScope - fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - @Singleton - @Provides +class PreferencesDataStoreModule { + @Single + @Named("CorePreferencesDataStore") fun providePreferencesDataStore( - @ApplicationContext appContext: Context, - @DataStoreScope scope: CoroutineScope, + context: Context, + @Named("DataStoreScope") scope: CoroutineScope, ): DataStore = PreferenceDataStoreFactory.create( corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }), migrations = listOf( - SharedPreferencesMigration(context = appContext, sharedPreferencesName = USER_PREFERENCES_NAME), + SharedPreferencesMigration(context = context, sharedPreferencesName = USER_PREFERENCES_NAME), SharedPreferencesMigration( - context = appContext, + context = context, sharedPreferencesName = "ui-prefs", keysToMigrate = setOf( @@ -96,70 +79,94 @@ object DataStoreModule { ), ), scope = scope, - produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) }, + produceFile = { context.preferencesDataStoreFile(USER_PREFERENCES_NAME) }, ) +} - @Singleton - @Provides +@Module +class LocalConfigDataStoreModule { + @Single + @Named("CoreLocalConfigDataStore") fun provideLocalConfigDataStore( - @ApplicationContext appContext: Context, - @DataStoreScope scope: CoroutineScope, + context: Context, + @Named("DataStoreScope") scope: CoroutineScope, ): DataStore = DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, serializer = LocalConfigSerializer, - producePath = { appContext.dataStoreFile("local_config.pb").toOkioPath() }, + producePath = { context.dataStoreFile("local_config.pb").toOkioPath() }, ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }), scope = scope, ) +} - @Singleton - @Provides +@Module +class ModuleConfigDataStoreModule { + @Single + @Named("CoreModuleConfigDataStore") fun provideModuleConfigDataStore( - @ApplicationContext appContext: Context, - @DataStoreScope scope: CoroutineScope, + context: Context, + @Named("DataStoreScope") scope: CoroutineScope, ): DataStore = DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, serializer = ModuleConfigSerializer, - producePath = { appContext.dataStoreFile("module_config.pb").toOkioPath() }, + producePath = { context.dataStoreFile("module_config.pb").toOkioPath() }, ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }), scope = scope, ) +} - @Singleton - @Provides +@Module +class ChannelSetDataStoreModule { + @Single + @Named("CoreChannelSetDataStore") fun provideChannelSetDataStore( - @ApplicationContext appContext: Context, - @DataStoreScope scope: CoroutineScope, + context: Context, + @Named("DataStoreScope") scope: CoroutineScope, ): DataStore = DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, serializer = ChannelSetSerializer, - producePath = { appContext.dataStoreFile("channel_set.pb").toOkioPath() }, + producePath = { context.dataStoreFile("channel_set.pb").toOkioPath() }, ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }), scope = scope, ) +} - @Singleton - @Provides +@Module +class LocalStatsDataStoreModule { + @Single + @Named("CoreLocalStatsDataStore") fun provideLocalStatsDataStore( - @ApplicationContext appContext: Context, - @DataStoreScope scope: CoroutineScope, + context: Context, + @Named("DataStoreScope") scope: CoroutineScope, ): DataStore = DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, serializer = LocalStatsSerializer, - producePath = { appContext.dataStoreFile("local_stats.pb").toOkioPath() }, + producePath = { context.dataStoreFile("local_stats.pb").toOkioPath() }, ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalStats() }), scope = scope, ) } + +@Module( + includes = + [ + PreferencesDataStoreModule::class, + LocalConfigDataStoreModule::class, + ModuleConfigDataStoreModule::class, + ChannelSetDataStoreModule::class, + LocalStatsDataStoreModule::class, + ], +) +class CoreDatastoreAndroidModule diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt index 5eda0ca4c..c8d5a5315 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt @@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json -import javax.inject.Inject -import javax.inject.Singleton +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single -@Singleton -class BootloaderWarningDataSource @Inject constructor(private val dataStore: DataStore) { +@Single +class BootloaderWarningDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) { private object PreferencesKeys { val DISMISSED_BOOTLOADER_ADDRESSES = stringPreferencesKey("dismissed-bootloader-addresses") diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt index 9e7cfbcd0..0f3b648b6 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt @@ -21,16 +21,16 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import okio.IOException +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config -import javax.inject.Inject -import javax.inject.Singleton /** Class that handles saving and retrieving [ChannelSet] data. */ -@Singleton -class ChannelSetDataSource @Inject constructor(private val channelSetStore: DataStore) { +@Single +class ChannelSetDataSource(@Named("CoreChannelSetDataStore") private val channelSetStore: DataStore) { val channelSetFlow: Flow = channelSetStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt index f347c710b..b1fe828c5 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt @@ -21,14 +21,14 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import okio.IOException +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig -import javax.inject.Inject -import javax.inject.Singleton /** Class that handles saving and retrieving [LocalConfig] data. */ -@Singleton -class LocalConfigDataSource @Inject constructor(private val localConfigStore: DataStore) { +@Single +class LocalConfigDataSource(@Named("CoreLocalConfigDataStore") private val localConfigStore: DataStore) { val localConfigFlow: Flow = localConfigStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt index 22ee35390..abf9ad5d3 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt @@ -21,13 +21,13 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import okio.IOException +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.proto.LocalStats -import javax.inject.Inject -import javax.inject.Singleton /** Class that handles saving and retrieving [LocalStats] data. */ -@Singleton -class LocalStatsDataSource @Inject constructor(private val localStatsStore: DataStore) { +@Single +class LocalStatsDataSource(@Named("CoreLocalStatsDataStore") private val localStatsStore: DataStore) { val localStatsFlow: Flow = localStatsStore.data.catch { exception -> if (exception is IOException) { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt index c4195d58a..54db1ad0b 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt @@ -21,14 +21,16 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import okio.IOException +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.ModuleConfig -import javax.inject.Inject -import javax.inject.Singleton /** Class that handles saving and retrieving [LocalModuleConfig] data. */ -@Singleton -class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore: DataStore) { +@Single +class ModuleConfigDataSource( + @Named("CoreModuleConfigDataStore") private val moduleConfigStore: DataStore, +) { val moduleConfigFlow: Flow = moduleConfigStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt index 0d3c4c123..82ccf1781 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt @@ -28,12 +28,12 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.json.JSONArray import org.json.JSONObject +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.model.RecentAddress -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class RecentAddressesDataSource @Inject constructor(private val dataStore: DataStore) { +@Single +class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) { private object PreferencesKeys { val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses") } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt index 02634293e..f931e9078 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt @@ -29,8 +29,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single const val KEY_APP_INTRO_COMPLETED = "app_intro_completed" const val KEY_THEME = "theme" @@ -43,8 +43,8 @@ const val KEY_ONLY_ONLINE = "only-online" const val KEY_ONLY_DIRECT = "only-direct" const val KEY_SHOW_IGNORED = "show-ignored" -@Singleton -class UiPreferencesDataSource @Inject constructor(private val dataStore: DataStore) { +@Single +class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt new file mode 100644 index 000000000..9ef808bc3 --- /dev/null +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreModule.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ +package org.meshtastic.core.datastore.di + +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.core.datastore") +class CoreDatastoreModule { + @Single + @Named("DataStoreScope") + fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) +} diff --git a/core/di/README.md b/core/di/README.md index d83fd8c50..7cd07a8a2 100644 --- a/core/di/README.md +++ b/core/di/README.md @@ -1,7 +1,7 @@ # `:core:di` ## Overview -The `:core:di` module defines the core Dagger Hilt modules and provides standard dependencies that are shared across all other modules. +The `:core:di` module defines the core Koin modules and provides standard dependencies that are shared across all other modules. ## Key Components @@ -12,7 +12,7 @@ Defines bindings for application-wide singletons like `Application`, `Context`, Provides a wrapper for standard Kotlin `CoroutineDispatchers` (`IO`, `Default`, `Main`), allowing for easy mocking in unit tests. ### 3. `ProcessLifecycle.kt` -Exposes the application's global process lifecycle as a Hilt binding, enabling components to react to the app entering the foreground or background. +Exposes the application's global process lifecycle as a Koin binding, enabling components to react to the app entering the foreground or background. ## Module dependency graph diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index 59f82dbeb..9cadd064d 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -15,7 +15,10 @@ * along with this program. If not, see . */ -plugins { alias(libs.plugins.meshtastic.kmp.library) } +plugins { + alias(libs.plugins.meshtastic.kmp.library) + id("meshtastic.koin") +} kotlin { @Suppress("UnstableApiUsage") diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt similarity index 62% rename from app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt rename to core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt index ec1efc74d..9ad24502a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/AppModule.kt +++ b/core/di/src/commonMain/kotlin/org/meshtastic/core/di/di/CoreDiModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,28 +14,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.di +package org.meshtastic.core.di.di -import android.content.Context -import androidx.work.WorkManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.Dispatchers +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import javax.inject.Singleton @Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides +class CoreDiModule { + @Single fun provideCoroutineDispatchers(): CoroutineDispatchers = CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default) - - @Provides - @Singleton - fun provideWorkManager(@ApplicationContext context: Context): WorkManager = WorkManager.getInstance(context) } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 64c8fd8f5..69a0b2af8 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.devtools.ksp) + alias(libs.plugins.meshtastic.koin) } kotlin { @@ -53,5 +54,3 @@ kotlin { } } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt new file mode 100644 index 000000000..80cfb26ab --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/di/CoreDomainModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.domain.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.domain") +class CoreDomainModule diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt index b0b7c2c8c..095fbc39c 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/AdminActionsUseCase.kt @@ -16,9 +16,9 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository -import javax.inject.Inject /** * Use case for performing administrative and destructive actions on mesh nodes. @@ -26,8 +26,8 @@ import javax.inject.Inject * This component provides methods for rebooting, shutting down, or resetting nodes within the mesh. It also handles * local database synchronization when these actions are performed on the locally connected device. */ +@Single open class AdminActionsUseCase -@Inject constructor( private val radioController: RadioController, private val nodeRepository: NodeRepository, diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt index 655323caf..491497ba7 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/CleanNodeDatabaseUseCase.kt @@ -16,15 +16,15 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.NodeRepository -import javax.inject.Inject import kotlin.time.Duration.Companion.days /** Use case for cleaning up nodes from the database. */ +@Single open class CleanNodeDatabaseUseCase -@Inject constructor( private val nodeRepository: NodeRepository, private val radioController: RadioController, diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt index 6897f4c9f..4b8863801 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt @@ -21,18 +21,18 @@ import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import okio.BufferedSink +import org.koin.core.annotation.Single import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.positionToMeter import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.PortNum -import javax.inject.Inject import kotlin.math.roundToInt import org.meshtastic.proto.Position as ProtoPosition /** Use case for exporting persisted packet data to a CSV format. */ +@Single open class ExportDataUseCase -@Inject constructor( private val nodeRepository: NodeRepository, private val meshLogRepository: MeshLogRepository, diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt index e9e8995bb..a52c73fc1 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCase.kt @@ -17,11 +17,12 @@ package org.meshtastic.core.domain.usecase.settings import okio.BufferedSink +import org.koin.core.annotation.Single import org.meshtastic.proto.DeviceProfile -import javax.inject.Inject /** Use case for exporting a device profile to an output stream. */ -open class ExportProfileUseCase @Inject constructor() { +@Single +open class ExportProfileUseCase { /** * Exports the provided [DeviceProfile] to the given [BufferedSink]. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt index 55cc5032f..309da69d2 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportSecurityConfigUseCase.kt @@ -19,12 +19,13 @@ package org.meshtastic.core.domain.usecase.settings import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import okio.BufferedSink +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.proto.Config -import javax.inject.Inject /** Use case for exporting security configuration to a JSON format. */ -open class ExportSecurityConfigUseCase @Inject constructor() { +@Single +open class ExportSecurityConfigUseCase { /** * Exports the provided [Config.SecurityConfig] as a JSON string to the given [BufferedSink]. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt index c003b82ef..841421349 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt @@ -17,11 +17,12 @@ package org.meshtastic.core.domain.usecase.settings import okio.BufferedSource +import org.koin.core.annotation.Single import org.meshtastic.proto.DeviceProfile -import javax.inject.Inject /** Use case for importing a device profile from an input stream. */ -open class ImportProfileUseCase @Inject constructor() { +@Single +open class ImportProfileUseCase { /** * Imports a [DeviceProfile] from the provided [BufferedSource]. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt index 88e8319a5..db4ffe82e 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.model.Position import org.meshtastic.core.model.RadioController import org.meshtastic.proto.Config @@ -24,10 +25,10 @@ import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.User -import javax.inject.Inject /** Use case for installing a device profile onto a radio. */ -open class InstallProfileUseCase @Inject constructor(private val radioController: RadioController) { +@Single +open class InstallProfileUseCase constructor(private val radioController: RadioController) { /** * Installs the provided [DeviceProfile] onto the radio at [destNum]. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index 1707a7500..aa410028f 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import org.koin.core.annotation.Single import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController @@ -29,11 +30,10 @@ import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.isBle import org.meshtastic.core.repository.isSerial import org.meshtastic.core.repository.isTcp -import javax.inject.Inject /** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */ +@Single open class IsOtaCapableUseCase -@Inject constructor( private val nodeRepository: NodeRepository, private val radioController: RadioController, diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt index 6f578bc05..ec7f1defe 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/MeshLocationUseCase.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.model.RadioController -import javax.inject.Inject /** Use case for controlling location sharing with the mesh. */ -open class MeshLocationUseCase @Inject constructor(private val radioController: RadioController) { +@Single +open class MeshLocationUseCase constructor(private val radioController: RadioController) { /** Starts providing the phone's location to the mesh. */ fun startProvidingLocation() { radioController.startProvideLocation() diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt index 3e1639469..bfb36de58 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCase.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.domain.usecase.settings import co.touchlab.kermit.Logger +import org.koin.core.annotation.Single import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.resources.UiText import org.meshtastic.proto.AdminMessage @@ -28,7 +29,6 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.proto.Routing import org.meshtastic.proto.User -import javax.inject.Inject /** Sealed class representing the result of processing a radio response packet. */ sealed class RadioResponseResult { @@ -54,7 +54,8 @@ sealed class RadioResponseResult { } /** Use case for processing incoming [MeshPacket]s that are responses to admin requests. */ -open class ProcessRadioResponseUseCase @Inject constructor() { +@Single +open class ProcessRadioResponseUseCase { /** * Decodes and processes the provided [packet]. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt index a65b75209..6db74a3c8 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCase.kt @@ -16,16 +16,17 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.model.Position import org.meshtastic.core.model.RadioController import org.meshtastic.proto.Config import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.User -import javax.inject.Inject /** Use case for interacting with radio configuration components. */ @Suppress("TooManyFunctions") -open class RadioConfigUseCase @Inject constructor(private val radioController: RadioController) { +@Single +open class RadioConfigUseCase constructor(private val radioController: RadioController) { /** * Updates the owner information on the radio. * diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt index d31cc41f3..79737c439 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetAppIntroCompletedUseCase.kt @@ -16,15 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.UiPreferencesDataSource -import javax.inject.Inject /** Use case for setting whether the application intro has been completed. */ -open class SetAppIntroCompletedUseCase -@Inject -constructor( - private val uiPreferencesDataSource: UiPreferencesDataSource, -) { +@Single +open class SetAppIntroCompletedUseCase constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { operator fun invoke(completed: Boolean) { uiPreferencesDataSource.setAppIntroCompleted(completed) } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt index 4b46cd70c..ca23e11d0 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt @@ -16,12 +16,13 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.database.DatabaseConstants -import javax.inject.Inject /** Use case for setting the database cache limit. */ -open class SetDatabaseCacheLimitUseCase @Inject constructor(private val databaseManager: DatabaseManager) { +@Single +open class SetDatabaseCacheLimitUseCase constructor(private val databaseManager: DatabaseManager) { operator fun invoke(limit: Int) { val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT) databaseManager.setCacheLimit(clamped) diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt index b18133635..856be35b6 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt @@ -16,13 +16,13 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.repository.MeshLogPrefs import org.meshtastic.core.repository.MeshLogRepository -import javax.inject.Inject /** Use case for managing mesh log settings. */ +@Single open class SetMeshLogSettingsUseCase -@Inject constructor( private val meshLogRepository: MeshLogRepository, private val meshLogPrefs: MeshLogPrefs, diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt index e66651f9c..19e606f7a 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.repository.UiPrefs -import javax.inject.Inject /** Use case for setting whether to provide the node location to the mesh. */ -open class SetProvideLocationUseCase @Inject constructor(private val uiPrefs: UiPrefs) { +@Single +open class SetProvideLocationUseCase constructor(private val uiPrefs: UiPrefs) { operator fun invoke(myNodeNum: Int, provideLocation: Boolean) { uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation) } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt index fd1ae35a0..831d9a529 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/SetThemeUseCase.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.UiPreferencesDataSource -import javax.inject.Inject /** Use case for setting the application theme. */ -open class SetThemeUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { +@Single +open class SetThemeUseCase constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { operator fun invoke(themeMode: Int) { uiPreferencesDataSource.setTheme(themeMode) } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt index 92aa6933c..ab6e5dce4 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.repository.AnalyticsPrefs -import javax.inject.Inject /** Use case for toggling the analytics preference. */ -open class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) { +@Single +open class ToggleAnalyticsUseCase constructor(private val analyticsPrefs: AnalyticsPrefs) { operator fun invoke() { analyticsPrefs.setAnalyticsAllowed(!analyticsPrefs.analyticsAllowed.value) } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt index 37d693e1f..5c403b2dd 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.koin.core.annotation.Single import org.meshtastic.core.repository.HomoglyphPrefs -import javax.inject.Inject /** Use case for toggling the homoglyph encoding preference. */ -open class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) { +@Single +open class ToggleHomoglyphEncodingUseCase constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) { operator fun invoke() { homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value) } diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 4d7f209df..9d6c56a7b 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -14,30 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import com.android.build.api.dsl.LibraryExtension - -/* - * Copyright (c) 2025 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 . - */ plugins { - alias(libs.plugins.meshtastic.android.library) + alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) } -configure { namespace = "org.meshtastic.core.navigation" } +kotlin { + android { namespace = "org.meshtastic.core.navigation" } -dependencies { implementation(libs.kotlinx.serialization.core) } + sourceSets { commonMain.dependencies { implementation(libs.kotlinx.serialization.core) } } +} diff --git a/core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt similarity index 100% rename from core/navigation/src/main/kotlin/org/meshtastic/core/navigation/Routes.kt rename to core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 7085433ce..5ff29055d 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -18,7 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -35,7 +35,6 @@ kotlin { implementation(projects.core.model) implementation(projects.core.proto) - api(libs.javax.inject) implementation(libs.okio) implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.core) @@ -43,8 +42,8 @@ kotlin { implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.kermit) } + androidMain.dependencies { - implementation(libs.hilt.android) implementation(libs.org.eclipse.paho.client.mqttv3) implementation(libs.coil.network.okhttp) implementation(libs.coil.svg) @@ -61,5 +60,3 @@ configurations.all { attributes.attribute(marketplaceAttr, "fdroid") } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt similarity index 51% rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt index 47d3e7fd5..ab46023eb 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/di/FdroidPlatformAnalyticsModule.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,22 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.di +package org.meshtastic.core.network.di -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.meshtastic.app.analytics.FdroidPlatformAnalytics -import org.meshtastic.core.repository.PlatformAnalytics -import javax.inject.Singleton +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 org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single -/** Hilt module to provide the [FdroidPlatformAnalytics] for the fdroid flavor. */ @Module -@InstallIn(SingletonComponent::class) -abstract class FdroidPlatformAnalyticsModule { - - @Binds - @Singleton - abstract fun bindPlatformHelper(fdroidPlatformAnalytics: FdroidPlatformAnalytics): PlatformAnalytics +@ComponentScan("org.meshtastic.core.network") +class CoreNetworkAndroidModule { + @Single + fun provideHttpClient(json: Json): HttpClient = HttpClient(OkHttp) { install(ContentNegotiation) { json(json) } } } diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 86590e6cb..d9589eb0a 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -30,6 +30,7 @@ import org.eclipse.paho.client.mqttv3.MqttCallbackExtended import org.eclipse.paho.client.mqttv3.MqttConnectOptions import org.eclipse.paho.client.mqttv3.MqttMessage import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.ignoreException import org.meshtastic.core.model.util.subscribeList import org.meshtastic.core.repository.NodeRepository @@ -37,14 +38,11 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.proto.MqttClientProxyMessage import java.net.URI import java.security.SecureRandom -import javax.inject.Inject -import javax.inject.Singleton import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager -@Singleton +@Single class MQTTRepositoryImpl -@Inject constructor( private val radioConfigRepository: RadioConfigRepository, private val nodeRepository: NodeRepository, diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt index 826de8c12..99f93dbf7 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt @@ -17,14 +17,13 @@ package org.meshtastic.core.network import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkDeviceHardware import org.meshtastic.core.network.service.ApiService -import javax.inject.Inject -class DeviceHardwareRemoteDataSource -@Inject -constructor( +@Single +class DeviceHardwareRemoteDataSource( private val apiService: ApiService, private val dispatchers: CoroutineDispatchers, ) { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt index 056cdce43..0248110a9 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt @@ -17,14 +17,13 @@ package org.meshtastic.core.network import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.network.service.ApiService -import javax.inject.Inject -class FirmwareReleaseRemoteDataSource -@Inject -constructor( +@Single +class FirmwareReleaseRemoteDataSource( private val apiService: ApiService, private val dispatchers: CoroutineDispatchers, ) { diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt similarity index 61% rename from app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt index 055f5c0cb..37d5726b9 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/di/CoreNetworkModule.kt @@ -14,18 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.messaging.di +package org.meshtastic.core.network.di -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.meshtastic.app.messaging.domain.worker.WorkManagerMessageQueue -import org.meshtastic.core.repository.MessageQueue +import kotlinx.serialization.json.Json +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single @Module -@InstallIn(SingletonComponent::class) -abstract class MessagingModule { - - @Binds abstract fun bindMessageQueue(impl: WorkManagerMessageQueue): MessageQueue +@ComponentScan("org.meshtastic.core.network") +class CoreNetworkModule { + @Single + fun provideJson(): Json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + } } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt index a8a813614..1e12344b4 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt @@ -19,9 +19,9 @@ package org.meshtastic.core.network.service import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get +import org.koin.core.annotation.Single import org.meshtastic.core.model.NetworkDeviceHardware import org.meshtastic.core.model.NetworkFirmwareReleases -import javax.inject.Inject interface ApiService { suspend fun getDeviceHardware(): List @@ -29,7 +29,8 @@ interface ApiService { suspend fun getFirmwareReleases(): NetworkFirmwareReleases } -class ApiServiceImpl @Inject constructor(private val client: HttpClient) : ApiService { +@Single +class ApiServiceImpl(private val client: HttpClient) : ApiService { override suspend fun getDeviceHardware(): List = client.get("https://api.meshtastic.org/resource/deviceHardware").body() diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts index f2d34d56e..6939dc64a 100644 --- a/core/prefs/build.gradle.kts +++ b/core/prefs/build.gradle.kts @@ -17,7 +17,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -34,7 +34,6 @@ kotlin { implementation(projects.core.common) implementation(projects.core.di) - api(libs.javax.inject) implementation(libs.androidx.datastore.preferences) implementation(libs.kotlinx.coroutines.core) } @@ -46,5 +45,3 @@ kotlin { } } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt new file mode 100644 index 000000000..dfd9d048c --- /dev/null +++ b/core/prefs/src/androidMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsAndroidModule.kt @@ -0,0 +1,132 @@ +/* + * 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 . + */ +package org.meshtastic.core.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.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single + +@Suppress("TooManyFunctions") +@Module +class CorePrefsAndroidModule { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + @Single + @Named("AnalyticsDataStore") + fun provideAnalyticsDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("analytics_ds") }, + ) + + @Single + @Named("HomoglyphEncodingDataStore") + fun provideHomoglyphEncodingDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") }, + ) + + @Single + @Named("AppDataStore") + fun provideAppDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("app_ds") }, + ) + + @Single + @Named("CustomEmojiDataStore") + fun provideCustomEmojiDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") }, + ) + + @Single + @Named("MapDataStore") + fun provideMapDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_ds") }, + ) + + @Single + @Named("MapConsentDataStore") + fun provideMapConsentDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_consent_ds") }, + ) + + @Single + @Named("MapTileProviderDataStore") + fun provideMapTileProviderDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") }, + ) + + @Single + @Named("MeshDataStore") + fun provideMeshDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("mesh_ds") }, + ) + + @Single + @Named("RadioDataStore") + fun provideRadioDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("radio_ds") }, + ) + + @Single + @Named("UiDataStore") + fun provideUiDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("ui_ds") }, + ) + + @Single + @Named("MeshLogDataStore") + fun provideMeshLogDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("meshlog_ds") }, + ) + + @Single + @Named("FilterDataStore") + fun provideFilterDataStore(context: Context): DataStore = PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("filter_ds") }, + ) +} diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt index 4fe087be0..8d52c4c0b 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt @@ -28,20 +28,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.AnalyticsDataStore -import org.meshtastic.core.prefs.di.AppDataStore import org.meshtastic.core.repository.AnalyticsPrefs -import javax.inject.Inject -import javax.inject.Singleton import kotlin.uuid.Uuid -@Singleton -class AnalyticsPrefsImpl -@Inject -constructor( - @AnalyticsDataStore private val analyticsDataStore: DataStore, - @AppDataStore private val appDataStore: DataStore, +@Single +class AnalyticsPrefsImpl( + @Named("AnalyticsDataStore") private val analyticsDataStore: DataStore, + @Named("AppDataStore") private val appDataStore: DataStore, dispatchers: CoroutineDispatchers, ) : AnalyticsPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt new file mode 100644 index 000000000..ef11bac13 --- /dev/null +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/CorePrefsModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.prefs.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.prefs") +class CorePrefsModule diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt index 9bc7f1805..257ffba81 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt @@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.CustomEmojiDataStore import org.meshtastic.core.repository.CustomEmojiPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class CustomEmojiPrefsImpl -@Inject -constructor( - @CustomEmojiDataStore private val dataStore: DataStore, +@Single +class CustomEmojiPrefsImpl( + @Named("CustomEmojiDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : CustomEmojiPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt index 6ea9e24dd..121925e71 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt @@ -28,17 +28,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.FilterDataStore import org.meshtastic.core.repository.FilterPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class FilterPrefsImpl -@Inject -constructor( - @FilterDataStore private val dataStore: DataStore, +@Single +class FilterPrefsImpl( + @Named("FilterDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : FilterPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt index 42b4f8faa..092367db5 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt @@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore import org.meshtastic.core.repository.HomoglyphPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class HomoglyphPrefsImpl -@Inject -constructor( - @HomoglyphEncodingDataStore private val dataStore: DataStore, +@Single +class HomoglyphPrefsImpl( + @Named("HomoglyphEncodingDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : HomoglyphPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt index bf22eb27d..86a6ab40d 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt @@ -27,18 +27,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.MapConsentDataStore import org.meshtastic.core.repository.MapConsentPrefs import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MapConsentPrefsImpl -@Inject -constructor( - @MapConsentDataStore private val dataStore: DataStore, +@Single +class MapConsentPrefsImpl( + @Named("MapConsentDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : MapConsentPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt index 52167812f..506d5ac5e 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt @@ -29,17 +29,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.MapDataStore import org.meshtastic.core.repository.MapPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MapPrefsImpl -@Inject -constructor( - @MapDataStore private val dataStore: DataStore, +@Single +class MapPrefsImpl( + @Named("MapDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : MapPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt index c3a686e97..30192f98a 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt @@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.MapTileProviderDataStore import org.meshtastic.core.repository.MapTileProviderPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MapTileProviderPrefsImpl -@Inject -constructor( - @MapTileProviderDataStore private val dataStore: DataStore, +@Single +class MapTileProviderPrefsImpl( + @Named("MapTileProviderDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : MapTileProviderPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt index c247788f2..7807a6c32 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt @@ -29,19 +29,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.MeshDataStore import org.meshtastic.core.repository.MeshPrefs import java.util.Locale import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MeshPrefsImpl -@Inject -constructor( - @MeshDataStore private val dataStore: DataStore, +@Single +class MeshPrefsImpl( + @Named("MeshDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : MeshPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt index a10c27da8..494579e72 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt @@ -28,17 +28,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.MeshLogDataStore import org.meshtastic.core.repository.MeshLogPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MeshLogPrefsImpl -@Inject -constructor( - @MeshLogDataStore private val dataStore: DataStore, +@Single +class MeshLogPrefsImpl( + @Named("MeshLogDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : MeshLogPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt index 916bb892c..d551f9333 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt @@ -27,17 +27,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.RadioDataStore import org.meshtastic.core.repository.RadioPrefs -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class RadioPrefsImpl -@Inject -constructor( - @RadioDataStore private val dataStore: DataStore, +@Single +class RadioPrefsImpl( + @Named("RadioDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : RadioPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt index 13c8ed336..0393a762f 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt @@ -27,18 +27,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.UiDataStore import org.meshtastic.core.repository.UiPrefs import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class UiPrefsImpl -@Inject -constructor( - @UiDataStore private val dataStore: DataStore, +@Single +class UiPrefsImpl( + @Named("UiDataStore") private val dataStore: DataStore, dispatchers: CoroutineDispatchers, ) : UiPrefs { private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 44e49f491..9a74a9c32 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -15,7 +15,10 @@ * along with this program. If not, see . */ -plugins { alias(libs.plugins.meshtastic.kmp.library) } +plugins { + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.meshtastic.koin) +} kotlin { @Suppress("UnstableApiUsage") diff --git a/app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt similarity index 81% rename from app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt rename to core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt index f0b078cea..e0f08ee86 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/UseCaseModule.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/di/CoreRepositoryModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 Meshtastic LLC + * 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 @@ -14,26 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.di +package org.meshtastic.core.repository.di -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.MessageQueue import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.usecase.SendMessageUseCase -import javax.inject.Singleton @Module -@InstallIn(SingletonComponent::class) -object UseCaseModule { - - @Provides - @Singleton +@ComponentScan("org.meshtastic.core.repository") +class CoreRepositoryModule { + @Single fun provideSendMessageUseCase( nodeRepository: NodeRepository, packetRepository: PacketRepository, diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts index 93f251c88..790cb73c6 100644 --- a/core/service/build.gradle.kts +++ b/core/service/build.gradle.kts @@ -17,7 +17,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.devtools.ksp) + id("meshtastic.koin") } kotlin { @@ -35,15 +35,12 @@ kotlin { implementation(projects.core.model) implementation(projects.core.prefs) implementation(projects.core.proto) - implementation(libs.javax.inject) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) } - androidMain.dependencies { - api(projects.core.api) - implementation(libs.hilt.android) - } + androidMain.dependencies { api(projects.core.api) } commonTest.dependencies { implementation(libs.junit) @@ -53,5 +50,3 @@ kotlin { } } } - -dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index 9790eeec3..b6a1b7273 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -17,23 +17,19 @@ package org.meshtastic.core.service import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.StateFlow +import org.koin.core.annotation.Single import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.ClientNotification -import javax.inject.Inject -import javax.inject.Singleton -@Singleton +@Single @Suppress("TooManyFunctions") -class AndroidRadioControllerImpl -@Inject -constructor( - @ApplicationContext private val context: Context, +class AndroidRadioControllerImpl( + private val context: Context, private val serviceRepository: AndroidServiceRepository, private val nodeRepository: NodeRepository, ) : RadioController { diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt index 07a53aa16..91cac4d41 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt @@ -25,19 +25,18 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow +import org.koin.core.annotation.Single import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.MeshPacket -import javax.inject.Inject -import javax.inject.Singleton /** Repository class for managing the [IMeshService] instance and connection state */ @Suppress("TooManyFunctions") -@Singleton -open class AndroidServiceRepository @Inject constructor() : ServiceRepository { +@Single(binds = [ServiceRepository::class, AndroidServiceRepository::class]) +open class AndroidServiceRepository : ServiceRepository { var meshService: IMeshService? = null private set diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt new file mode 100644 index 000000000..f5104739c --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/di/CoreServiceAndroidModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.service.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.service") +class CoreServiceAndroidModule diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt new file mode 100644 index 000000000..d007f1ea3 --- /dev/null +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.service.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.service") +class CoreServiceModule diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index a25a6b8bb..58b31de48 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -19,7 +19,7 @@ import com.android.build.api.dsl.LibraryExtension plugins { alias(libs.plugins.meshtastic.android.library) alias(libs.plugins.meshtastic.android.library.compose) - alias(libs.plugins.meshtastic.hilt) + alias(libs.plugins.meshtastic.koin) } configure { namespace = "org.meshtastic.core.ui" } @@ -44,6 +44,7 @@ dependencies { implementation(libs.zxing.core) implementation(libs.kermit) implementation(libs.nordic.common.core) + implementation(libs.koin.compose.viewmodel) debugImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml index 6748a79ba..cbe00c8b4 100644 --- a/core/ui/detekt-baseline.xml +++ b/core/ui/detekt-baseline.xml @@ -9,5 +9,6 @@ MagicNumber:EditListPreference.kt$12345 MagicNumber:EditListPreference.kt$67890 MagicNumber:LazyColumnDragAndDropDemo.kt$50 + MatchingDeclarationName:LocalTracerouteMapOverlayInsetsProvider.kt$TracerouteMapOverlayInsets diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt new file mode 100644 index 000000000..077533641 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/di/CoreUiModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.core.ui") +class CoreUiModule diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt index 21536eeda..5421b22d5 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPicker.kt @@ -26,12 +26,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.ui.component.BottomSheetDialog @Composable fun EmojiPicker( - viewModel: EmojiPickerViewModel = hiltViewModel(), + viewModel: EmojiPickerViewModel = koinViewModel(), onDismiss: () -> Unit = {}, onConfirm: (String) -> Unit, ) { diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt index 8a30006d8..097a58048 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt @@ -17,12 +17,11 @@ package org.meshtastic.core.ui.emoji import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel +import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.repository.CustomEmojiPrefs -import javax.inject.Inject -@HiltViewModel -class EmojiPickerViewModel @Inject constructor(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() { +@KoinViewModel +class EmojiPickerViewModel(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() { var customEmojiFrequency: String? get() = customEmojiPrefs.customEmojiFrequency.value diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt index 33e721a3e..7f64f18b5 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt @@ -47,9 +47,9 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.model.Channel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.accept @@ -66,7 +66,7 @@ import org.meshtastic.proto.ChannelSet fun ScannedQrCodeDialog( incoming: ChannelSet, onDismiss: () -> Unit, - viewModel: ScannedQrCodeViewModel = hiltViewModel(), + viewModel: ScannedQrCodeViewModel = koinViewModel(), ) { val channels by viewModel.channels.collectAsStateWithLifecycle() diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt index cf3ab3404..2c10206aa 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt @@ -18,8 +18,8 @@ package org.meshtastic.core.ui.qr import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.util.getChannelList @@ -28,12 +28,9 @@ import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig -import javax.inject.Inject -@HiltViewModel -class ScannedQrCodeViewModel -@Inject -constructor( +@KoinViewModel +class ScannedQrCodeViewModel( private val radioConfigRepository: RadioConfigRepository, private val radioController: RadioController, ) : ViewModel() { diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt index 50588f547..549af6072 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt @@ -22,9 +22,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.model.util.compareUsers import org.meshtastic.core.model.util.userFieldsToString import org.meshtastic.core.resources.Res @@ -42,7 +42,7 @@ import org.meshtastic.proto.User fun SharedContactDialog( sharedContact: SharedContact, onDismiss: () -> Unit, - viewModel: SharedContactViewModel = hiltViewModel(), + viewModel: SharedContactViewModel = koinViewModel(), ) { val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle() diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt index d0feb933d..345c5b8ed 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt @@ -18,24 +18,19 @@ package org.meshtastic.core.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.model.Node import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.SharedContact -import javax.inject.Inject -@HiltViewModel -class SharedContactViewModel -@Inject -constructor( - nodeRepository: NodeRepository, - private val serviceRepository: ServiceRepository, -) : ViewModel() { +@KoinViewModel +class SharedContactViewModel(nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository) : + ViewModel() { val unfilteredNodes: StateFlow> = nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt index d6282b5c2..623939bbd 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/AlertManager.kt @@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.jetbrains.compose.resources.StringResource -import javax.inject.Inject -import javax.inject.Singleton +import org.koin.core.annotation.Single fun interface ComposableContent { @Composable fun Content() @@ -32,8 +31,8 @@ fun interface ComposableContent { * A global manager for displaying alerts across the application. This allows ViewModels to trigger alerts without * direct dependencies on UI components. */ -@Singleton -class AlertManager @Inject constructor() { +@Single +class AlertManager { data class AlertData( val title: String? = null, val titleRes: StringResource? = null, diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt new file mode 100644 index 000000000..e2a3206d1 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalInlineMapProvider.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier +import org.meshtastic.core.model.Node + +val LocalInlineMapProvider = compositionLocalOf<@Composable (node: Node, modifier: Modifier) -> Unit> { { _, _ -> } } diff --git a/feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt similarity index 71% rename from feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt index ad5d33784..40b174e8d 100644 --- a/feature/node/src/google/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapOverlayInsets.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalTracerouteMapOverlayInsetsProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,15 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -package org.meshtastic.feature.node.metrics +package org.meshtastic.core.ui.util import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp -internal object TracerouteMapOverlayInsets { - val overlayAlignment: Alignment = Alignment.BottomCenter - val overlayPadding: PaddingValues = PaddingValues(bottom = 16.dp) - val contentHorizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally -} +data class TracerouteMapOverlayInsets( + val overlayAlignment: Alignment = Alignment.BottomCenter, + val overlayPadding: PaddingValues = PaddingValues(bottom = 16.dp), + val contentHorizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, +) + +val LocalTracerouteMapOverlayInsetsProvider = compositionLocalOf { TracerouteMapOverlayInsets() } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 9305aa57b..32b845ad0 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -14,54 +14,77 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import com.android.build.api.dsl.LibraryExtension plugins { - alias(libs.plugins.meshtastic.android.library) - alias(libs.plugins.meshtastic.android.library.compose) - alias(libs.plugins.meshtastic.hilt) - alias(libs.plugins.kover) + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kotlinx.serialization) + alias(libs.plugins.meshtastic.koin) } -configure { namespace = "org.meshtastic.feature.firmware" } +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.feature.firmware" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } -dependencies { - implementation(projects.core.ble) - implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) - implementation(projects.core.model) - implementation(projects.core.navigation) - implementation(projects.core.network) - implementation(projects.core.prefs) - implementation(projects.core.proto) - implementation(projects.core.service) - implementation(projects.core.resources) - implementation(projects.core.ui) + sourceSets { + commonMain.dependencies { + implementation(projects.core.ble) + implementation(projects.core.common) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.datastore) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(projects.core.network) + implementation(projects.core.prefs) + implementation(projects.core.proto) + implementation(projects.core.service) + implementation(projects.core.resources) + implementation(projects.core.ui) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.navigation.compose) - implementation(libs.kotlinx.collections.immutable) - implementation(libs.kermit) - implementation(libs.ktor.client.core) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.kermit) + implementation(libs.kotlinx.collections.immutable) + implementation(libs.ktor.client.core) + } - implementation(libs.nordic.client.android) - implementation(libs.nordic.dfu) - implementation(libs.coil) - implementation(libs.coil.network.okhttp) - implementation(libs.markdown.renderer) - implementation(libs.markdown.renderer.m3) + androidMain.dependencies { + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.accompanist.permissions) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.navigation.common) + implementation(libs.coil) + implementation(libs.coil.network.okhttp) + implementation(libs.markdown.renderer.android) + implementation(libs.markdown.renderer.m3) + implementation(libs.markdown.renderer) - testImplementation(libs.junit) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.nordic.client.android.mock) - testImplementation(libs.nordic.client.core.mock) - testImplementation(libs.nordic.core.mock) - testImplementation(libs.mockk) + // DFU / Nordic specific dependencies + implementation(libs.nordic.client.android) + implementation(libs.nordic.dfu) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.mockk) + implementation(libs.robolectric) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.compose.ui.test.junit4) + implementation(libs.androidx.test.ext.junit) + implementation(libs.nordic.client.android.mock) + implementation(libs.nordic.client.core.mock) + implementation(libs.nordic.core.mock) + } + } } diff --git a/feature/firmware/src/main/AndroidManifest.xml b/feature/firmware/src/androidMain/AndroidManifest.xml similarity index 100% rename from feature/firmware/src/main/AndroidManifest.xml rename to feature/firmware/src/androidMain/AndroidManifest.xml diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt similarity index 72% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt index 75985a0ed..505d263c1 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.kt @@ -17,9 +17,7 @@ package org.meshtastic.feature.firmware import android.content.Context -import android.net.Uri import co.touchlab.kermit.Logger -import dagger.hilt.android.qualifiers.ApplicationContext import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.request.head @@ -31,14 +29,15 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.common.util.toPlatformUri import org.meshtastic.core.model.DeviceHardware import java.io.File -import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.util.zip.ZipEntry import java.util.zip.ZipInputStream -import javax.inject.Inject private const val DOWNLOAD_BUFFER_SIZE = 8192 @@ -46,15 +45,11 @@ private const val DOWNLOAD_BUFFER_SIZE = 8192 * Helper class to handle file operations related to firmware updates, such as downloading, copying from URI, and * extracting specific files from Zip archives. */ -class FirmwareFileHandler -@Inject -constructor( - @ApplicationContext private val context: Context, - private val client: HttpClient, -) { +@Single +class AndroidFirmwareFileHandler(private val context: Context, private val client: HttpClient) : FirmwareFileHandler { private val tempDir = File(context.cacheDir, "firmware_update") - fun cleanupAllTemporaryFiles() { + override fun cleanupAllTemporaryFiles() { runCatching { if (tempDir.exists()) { tempDir.deleteRecursively() @@ -64,7 +59,7 @@ constructor( .onFailure { e -> Logger.w(e) { "Failed to cleanup temp directory" } } } - suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) { + override suspend fun checkUrlExists(url: String): Boolean = withContext(Dispatchers.IO) { try { client.head(url).status.isSuccess() } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { @@ -73,7 +68,7 @@ constructor( } } - suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): File? = + override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): String? = withContext(Dispatchers.IO) { val response = try { @@ -93,10 +88,10 @@ constructor( if (!tempDir.exists()) tempDir.mkdirs() - val targetFile = File(tempDir, fileName) + val targetFile = java.io.File(tempDir, fileName) body.toInputStream().use { input -> - FileOutputStream(targetFile).use { output -> + java.io.FileOutputStream(targetFile).use { output -> val buffer = ByteArray(DOWNLOAD_BUFFER_SIZE) var bytesRead: Int var totalBytesRead = 0L @@ -116,15 +111,16 @@ constructor( } } } - targetFile + targetFile.absolutePath } - suspend fun extractFirmware( - zipFile: File, + override suspend fun extractFirmwareFromZip( + zipFilePath: String, hardware: DeviceHardware, fileExtension: String, - preferredFilename: String? = null, - ): File? = withContext(Dispatchers.IO) { + preferredFilename: String?, + ): String? = withContext(Dispatchers.IO) { + val zipFile = java.io.File(zipFilePath) val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug } if (target.isEmpty() && preferredFilename == null) return@withContext null @@ -153,21 +149,21 @@ constructor( matchingEntries.add(entry to outFile) if (preferredFilenameLower != null) { - return@withContext outFile + return@withContext outFile.absolutePath } } entry = zipInput.nextEntry } } - matchingEntries.minByOrNull { it.first.name.length }?.second + matchingEntries.minByOrNull { it.first.name.length }?.second?.absolutePath } - suspend fun extractFirmware( - uri: Uri, + override suspend fun extractFirmware( + uri: CommonUri, hardware: DeviceHardware, fileExtension: String, - preferredFilename: String? = null, - ): File? = withContext(Dispatchers.IO) { + preferredFilename: String?, + ): String? = withContext(Dispatchers.IO) { val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug } if (target.isEmpty() && preferredFilename == null) return@withContext null @@ -178,7 +174,8 @@ constructor( if (!tempDir.exists()) tempDir.mkdirs() try { - val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext null + val platformUri = uri.toPlatformUri() as android.net.Uri + val inputStream = context.contentResolver.openInputStream(platformUri) ?: return@withContext null ZipInputStream(inputStream).use { zipInput -> var entry = zipInput.nextEntry while (entry != null) { @@ -198,7 +195,7 @@ constructor( matchingEntries.add(entry to outFile) if (preferredFilenameLower != null) { - return@withContext outFile + return@withContext outFile.absolutePath } } entry = zipInput.nextEntry @@ -208,7 +205,17 @@ constructor( Logger.w(e) { "Failed to extract firmware from URI" } return@withContext null } - matchingEntries.minByOrNull { it.first.name.length }?.second + matchingEntries.minByOrNull { it.first.name.length }?.second?.absolutePath + } + + override suspend fun getFileSize(path: String): Long = withContext(Dispatchers.IO) { + val file = File(path) + if (file.exists()) file.length() else 0L + } + + override suspend fun deleteFile(path: String) = withContext(Dispatchers.IO) { + val file = File(path) + if (file.exists()) file.delete() } private fun isValidFirmwareFile(filename: String, target: String, fileExtension: String): Boolean { @@ -218,22 +225,25 @@ constructor( (regex.matches(filename) || filename.startsWith("$target-") || filename.startsWith("$target.")) } - suspend fun copyFileToUri(sourceFile: File, destinationUri: Uri) = withContext(Dispatchers.IO) { - val inputStream = FileInputStream(sourceFile) - val outputStream = - context.contentResolver.openOutputStream(destinationUri) - ?: throw IOException("Cannot open content URI for writing") + override suspend fun copyFileToUri(sourcePath: String, destinationUri: CommonUri): Long = + withContext(Dispatchers.IO) { + val inputStream = java.io.FileInputStream(java.io.File(sourcePath)) + val outputStream = + context.contentResolver.openOutputStream(destinationUri.toPlatformUri() as android.net.Uri) + ?: throw IOException("Cannot open content URI for writing") - inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } - } + inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } + } - suspend fun copyUriToUri(sourceUri: Uri, destinationUri: Uri) = withContext(Dispatchers.IO) { - val inputStream = - context.contentResolver.openInputStream(sourceUri) ?: throw IOException("Cannot open source URI") - val outputStream = - context.contentResolver.openOutputStream(destinationUri) - ?: throw IOException("Cannot open destination URI") + override suspend fun copyUriToUri(sourceUri: CommonUri, destinationUri: CommonUri): Long = + withContext(Dispatchers.IO) { + val inputStream = + context.contentResolver.openInputStream(sourceUri.toPlatformUri() as android.net.Uri) + ?: throw IOException("Cannot open source URI") + val outputStream = + context.contentResolver.openOutputStream(destinationUri.toPlatformUri() as android.net.Uri) + ?: throw IOException("Cannot open destination URI") - inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } - } + inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } + } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt similarity index 91% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt index 16c5f5cfb..0d9cb38eb 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUpdateManager.kt @@ -16,8 +16,9 @@ */ package org.meshtastic.feature.firmware -import android.net.Uri import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.repository.RadioPrefs @@ -25,29 +26,24 @@ import org.meshtastic.core.repository.isBle import org.meshtastic.core.repository.isSerial import org.meshtastic.core.repository.isTcp import org.meshtastic.feature.firmware.ota.Esp32OtaUpdateHandler -import java.io.File -import javax.inject.Inject -import javax.inject.Singleton /** Orchestrates the firmware update process by choosing the correct handler. */ -@Singleton -class FirmwareUpdateManager -@Inject -constructor( +@Single +class AndroidFirmwareUpdateManager( private val radioPrefs: RadioPrefs, private val nordicDfuHandler: NordicDfuHandler, private val usbUpdateHandler: UsbUpdateHandler, private val esp32OtaUpdateHandler: Esp32OtaUpdateHandler, -) { +) : FirmwareUpdateManager { /** Start the update process based on the current connection and hardware. */ - suspend fun startUpdate( + override suspend fun startUpdate( release: FirmwareRelease, hardware: DeviceHardware, address: String, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri? = null, - ): File? { + firmwareUri: CommonUri?, + ): String? { val handler = getHandler(hardware) val target = getTarget(address) @@ -60,7 +56,7 @@ constructor( ) } - fun dfuProgressFlow(): Flow = nordicDfuHandler.progressFlow() + override fun dfuProgressFlow(): Flow = nordicDfuHandler.progressFlow() private fun getHandler(hardware: DeviceHardware): FirmwareUpdateHandler = when { radioPrefs.isSerial() -> { diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt similarity index 88% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt index 9e8954280..0bf674f84 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbManager.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareUsbManager.kt @@ -23,18 +23,16 @@ import android.content.IntentFilter import android.hardware.usb.UsbManager import android.os.Build import co.touchlab.kermit.Logger -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject -import javax.inject.Singleton +import org.koin.core.annotation.Single /** Manages USB-related interactions for firmware updates. */ -@Singleton -class UsbManager @Inject constructor(@ApplicationContext private val context: Context) { +@Single +class AndroidFirmwareUsbManager(private val context: Context) : FirmwareUsbManager { /** Observe when a USB device is detached. */ - fun deviceDetachFlow(): Flow = callbackFlow { + override fun deviceDetachFlow(): Flow = callbackFlow { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt similarity index 86% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt index d23274478..79a5a48a0 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareDfuService.kt @@ -50,11 +50,13 @@ class FirmwareDfuService : DfuBaseService() { } override fun getNotificationTarget(): Class? = try { - // Best effort to find the main activity + // Best effort to find the main activity dynamically + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + val className = launchIntent?.component?.className ?: "org.meshtastic.app.MainActivity" @Suppress("UNCHECKED_CAST") - Class.forName("com.geeksville.mesh.MainActivity") as Class - } catch (_: ClassNotFoundException) { - null + Class.forName(className) as Class + } catch (_: Exception) { + Activity::class.java } override fun isDebug(): Boolean = isDebugFlag diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt similarity index 92% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt index a485c1957..6d9f83286 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareRetriever.kt @@ -17,18 +17,18 @@ package org.meshtastic.feature.firmware import co.touchlab.kermit.Logger +import org.koin.core.annotation.Single import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware -import java.io.File -import javax.inject.Inject /** Retrieves firmware files, either by direct download or by extracting from a release asset. */ -class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFileHandler) { +@Single +class FirmwareRetriever(private val fileHandler: FirmwareFileHandler) { suspend fun retrieveOtaFirmware( release: FirmwareRelease, hardware: DeviceHardware, onProgress: (Float) -> Unit, - ): File? = retrieve( + ): String? = retrieve( release = release, hardware = hardware, onProgress = onProgress, @@ -40,7 +40,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil release: FirmwareRelease, hardware: DeviceHardware, onProgress: (Float) -> Unit, - ): File? = retrieve( + ): String? = retrieve( release = release, hardware = hardware, onProgress = onProgress, @@ -52,7 +52,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil release: FirmwareRelease, hardware: DeviceHardware, onProgress: (Float) -> Unit, - ): File? { + ): String? { val mcu = hardware.architecture.replace("-", "") val otaFilename = "mt-$mcu-ota.bin" retrieve( @@ -84,7 +84,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil fileSuffix: String, internalFileExtension: String, preferredFilename: String? = null, - ): File? { + ): String? { val version = release.id.removePrefix("v") val target = hardware.platformioTarget.ifEmpty { hardware.hwModelSlug } val filename = preferredFilename ?: "firmware-$target-$version$fileSuffix" @@ -105,7 +105,7 @@ class FirmwareRetriever @Inject constructor(private val fileHandler: FirmwareFil val zipUrl = getDeviceFirmwareUrl(release.zipUrl, hardware.architecture) val downloadedZip = fileHandler.downloadFile(zipUrl, "firmware_release.zip", onProgress) return downloadedZip?.let { - fileHandler.extractFirmware(it, hardware, internalFileExtension, preferredFilename) + fileHandler.extractFirmwareFromZip(it, hardware, internalFileExtension, preferredFilename) } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt similarity index 97% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt index d00daacba..c3e986d7d 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt @@ -79,15 +79,14 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade import com.mikepenz.markdown.m3.Markdown import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.model.DeviceHardware @@ -153,11 +152,7 @@ private const val CYCLE_DELAY_MS = 4500L @Composable @Suppress("LongMethod") -fun FirmwareUpdateScreen( - navController: NavController, - modifier: Modifier = Modifier, - viewModel: FirmwareUpdateViewModel = hiltViewModel(), -) { +fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateViewModel, modifier: Modifier = Modifier) { val state by viewModel.state.collectAsStateWithLifecycle() val selectedReleaseType by viewModel.selectedReleaseType.collectAsStateWithLifecycle() val deviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle() @@ -165,21 +160,19 @@ fun FirmwareUpdateScreen( val selectedRelease by viewModel.selectedRelease.collectAsStateWithLifecycle() var showExitConfirmation by remember { mutableStateOf(false) } - - val getFileLauncher = + val filePickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> - uri?.let { viewModel.startUpdateFromFile(it) } + uri?.let { viewModel.startUpdateFromFile(CommonUri(it)) } } - val saveFileLauncher = + val createDocumentLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("application/octet-stream"), ) { uri: Uri? -> - uri?.let { viewModel.saveDfuFile(it) } + uri?.let { viewModel.saveDfuFile(CommonUri(it)) } } - val actions = - remember(viewModel, navController, state) { + remember(viewModel, onNavigateUp, state) { FirmwareUpdateActions( onReleaseTypeSelect = viewModel::setReleaseType, onStartUpdate = viewModel::startUpdate, @@ -190,16 +183,16 @@ fun FirmwareUpdateScreen( readyState.updateMethod is FirmwareUpdateMethod.Ble || readyState.updateMethod is FirmwareUpdateMethod.Wifi ) { - getFileLauncher.launch("*/*") + filePickerLauncher.launch("*/*") } else if (readyState.updateMethod is FirmwareUpdateMethod.Usb) { - getFileLauncher.launch("*/*") + filePickerLauncher.launch("*/*") } } }, - onSaveFile = { fileName -> saveFileLauncher.launch(fileName) }, + onSaveFile = { fileName -> createDocumentLauncher.launch(fileName) }, onRetry = viewModel::checkForUpdates, onCancel = { showExitConfirmation = true }, - onDone = { navController.navigateUp() }, + onDone = { onNavigateUp() }, onDismissBootloaderWarning = viewModel::dismissBootloaderWarningForCurrentDevice, ) } @@ -217,7 +210,7 @@ fun FirmwareUpdateScreen( onConfirm = { showExitConfirmation = false viewModel.cancelUpdate() - navController.navigateUp() + onNavigateUp() }, dismissText = stringResource(Res.string.back), ) @@ -225,7 +218,7 @@ fun FirmwareUpdateScreen( FirmwareUpdateScaffold( modifier = modifier, - navController = navController, + onNavigateUp = onNavigateUp, state = state, selectedReleaseType = selectedReleaseType, actions = actions, @@ -237,7 +230,7 @@ fun FirmwareUpdateScreen( @Composable private fun FirmwareUpdateScaffold( - navController: NavController, + onNavigateUp: () -> Unit, state: FirmwareUpdateState, selectedReleaseType: FirmwareReleaseType, actions: FirmwareUpdateActions, @@ -252,7 +245,7 @@ private fun FirmwareUpdateScaffold( CenterAlignedTopAppBar( title = { Text(stringResource(Res.string.firmware_update_title)) }, navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { + IconButton(onClick = { onNavigateUp() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.back)) } }, diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt similarity index 85% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt index 72cd5ed5f..d9ae92624 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/NordicDfuHandler.kt @@ -17,10 +17,8 @@ package org.meshtastic.feature.firmware import android.content.Context -import android.net.Uri import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -31,6 +29,9 @@ import no.nordicsemi.android.dfu.DfuProgressListenerAdapter import no.nordicsemi.android.dfu.DfuServiceInitiator import no.nordicsemi.android.dfu.DfuServiceListenerHelper import org.jetbrains.compose.resources.getString +import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.common.util.toPlatformUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.RadioController @@ -39,8 +40,6 @@ import org.meshtastic.core.resources.firmware_update_downloading_percent import org.meshtastic.core.resources.firmware_update_nordic_failed import org.meshtastic.core.resources.firmware_update_not_found_in_release import org.meshtastic.core.resources.firmware_update_starting_service -import java.io.File -import javax.inject.Inject private const val SCAN_TIMEOUT = 5000L private const val PACKETS_BEFORE_PRN = 8 @@ -48,11 +47,10 @@ private const val PERCENT_MAX = 100 private const val PREPARE_DATA_DELAY = 400L /** Handles Over-the-Air (OTA) firmware updates for nRF52-based devices using the Nordic DFU library. */ -class NordicDfuHandler -@Inject -constructor( +@Single +class NordicDfuHandler( private val firmwareRetriever: FirmwareRetriever, - @ApplicationContext private val context: Context, + private val context: Context, private val radioController: RadioController, ) : FirmwareUpdateHandler { @@ -61,8 +59,8 @@ constructor( hardware: DeviceHardware, target: String, // Bluetooth address updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri?, - ): File? = + firmwareUri: CommonUri?, + ): String? = try { val downloadingMsg = getString(Res.string.firmware_update_downloading_percent, 0) @@ -90,7 +88,7 @@ constructor( updateState(FirmwareUpdateState.Error(errorMsg)) null } else { - initiateDfu(target, hardware, Uri.fromFile(firmwareFile), updateState) + initiateDfu(target, hardware, CommonUri.parse("file://$firmwareFile"), updateState) firmwareFile } } @@ -106,7 +104,7 @@ constructor( private suspend fun initiateDfu( address: String, deviceHardware: DeviceHardware, - firmwareUri: Uri, + firmwareUri: CommonUri, updateState: (FirmwareUpdateState) -> Unit, ) { val startingMsg = getString(Res.string.firmware_update_starting_service) @@ -127,7 +125,7 @@ constructor( .setPacketsReceiptNotificationsEnabled(true) .setScanTimeout(SCAN_TIMEOUT) .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true) - .setZip(firmwareUri) + .setZip(firmwareUri.toPlatformUri() as android.net.Uri) .start(context, FirmwareDfuService::class.java) } @@ -215,36 +213,3 @@ constructor( } } } - -sealed interface DfuInternalState { - val address: String - - data class Connecting(override val address: String) : DfuInternalState - - data class Connected(override val address: String) : DfuInternalState - - data class Starting(override val address: String) : DfuInternalState - - data class EnablingDfuMode(override val address: String) : DfuInternalState - - data class Progress( - override val address: String, - val percent: Int, - val speed: Float, - val avgSpeed: Float, - val currentPart: Int, - val partsTotal: Int, - ) : DfuInternalState - - data class Validating(override val address: String) : DfuInternalState - - data class Disconnecting(override val address: String) : DfuInternalState - - data class Disconnected(override val address: String) : DfuInternalState - - data class Completed(override val address: String) : DfuInternalState - - data class Aborted(override val address: String) : DfuInternalState - - data class Error(override val address: String, val message: String?) : DfuInternalState -} diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt similarity index 95% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt index 19534440c..50d1361fa 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/UsbUpdateHandler.kt @@ -16,11 +16,12 @@ */ package org.meshtastic.feature.firmware -import android.net.Uri import co.touchlab.kermit.Logger import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.jetbrains.compose.resources.getString +import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.RadioController @@ -30,16 +31,13 @@ import org.meshtastic.core.resources.firmware_update_downloading_percent import org.meshtastic.core.resources.firmware_update_rebooting import org.meshtastic.core.resources.firmware_update_retrieval_failed import org.meshtastic.core.resources.firmware_update_usb_failed -import java.io.File -import javax.inject.Inject private const val REBOOT_DELAY = 5000L private const val PERCENT_MAX = 100 /** Handles firmware updates via USB Mass Storage (UF2). */ -class UsbUpdateHandler -@Inject -constructor( +@Single +class UsbUpdateHandler( private val firmwareRetriever: FirmwareRetriever, private val radioController: RadioController, private val nodeRepository: NodeRepository, @@ -50,8 +48,8 @@ constructor( hardware: DeviceHardware, target: String, // Unused for USB updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri?, - ): File? = + firmwareUri: CommonUri?, + ): String? = try { val downloadingMsg = getString(Res.string.firmware_update_downloading_percent, 0) @@ -91,7 +89,7 @@ constructor( radioController.rebootToDfu(myNodeNum) delay(REBOOT_DELAY) - updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, firmwareFile.name)) + updateState(FirmwareUpdateState.AwaitingFileSave(firmwareFile, java.io.File(firmwareFile).name)) firmwareFile } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt similarity index 100% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt similarity index 82% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt index 890c23a3e..2f992b6f4 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt @@ -17,9 +17,7 @@ package org.meshtastic.feature.firmware.ota import android.content.Context -import android.net.Uri import co.touchlab.kermit.Logger -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -27,9 +25,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.getString +import org.koin.core.annotation.Single import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.common.util.toPlatformUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.RadioController @@ -38,10 +39,10 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.firmware_update_connecting_attempt import org.meshtastic.core.resources.firmware_update_downloading_percent import org.meshtastic.core.resources.firmware_update_erasing +import org.meshtastic.core.resources.firmware_update_extracting import org.meshtastic.core.resources.firmware_update_hash_rejected -import org.meshtastic.core.resources.firmware_update_loading +import org.meshtastic.core.resources.firmware_update_not_found_in_release import org.meshtastic.core.resources.firmware_update_ota_failed -import org.meshtastic.core.resources.firmware_update_retrieval_failed import org.meshtastic.core.resources.firmware_update_starting_ota import org.meshtastic.core.resources.firmware_update_uploading import org.meshtastic.core.resources.firmware_update_waiting_reboot @@ -49,8 +50,6 @@ import org.meshtastic.feature.firmware.FirmwareRetriever import org.meshtastic.feature.firmware.FirmwareUpdateHandler import org.meshtastic.feature.firmware.FirmwareUpdateState import org.meshtastic.feature.firmware.ProgressState -import java.io.File -import javax.inject.Inject private const val RETRY_DELAY = 2000L private const val PERCENT_MAX = 100 @@ -68,15 +67,14 @@ private const val GATT_RELEASE_DELAY_MS = 1000L * UnifiedOtaProtocol. */ @Suppress("TooManyFunctions") -class Esp32OtaUpdateHandler -@Inject -constructor( +@Single +class Esp32OtaUpdateHandler( private val firmwareRetriever: FirmwareRetriever, private val radioController: RadioController, private val nodeRepository: NodeRepository, private val bleScanner: BleScanner, private val bleConnectionFactory: BleConnectionFactory, - @ApplicationContext private val context: Context, + private val context: Context, ) : FirmwareUpdateHandler { /** Entry point for FirmwareUpdateHandler interface. Decides between BLE and WiFi based on target format. */ @@ -85,8 +83,8 @@ constructor( hardware: DeviceHardware, target: String, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri?, - ): File? = if (target.contains(":")) { + firmwareUri: CommonUri?, + ): String? = if (target.contains(":")) { startBleUpdate(release, hardware, target, updateState, firmwareUri) } else { startWifiUpdate(release, hardware, target, updateState, firmwareUri) @@ -97,8 +95,8 @@ constructor( hardware: DeviceHardware, address: String, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri? = null, - ): File? = performUpdate( + firmwareUri: CommonUri? = null, + ): String? = performUpdate( release = release, hardware = hardware, updateState = updateState, @@ -113,8 +111,8 @@ constructor( hardware: DeviceHardware, deviceIp: String, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri? = null, - ): File? = performUpdate( + firmwareUri: CommonUri? = null, + ): String? = performUpdate( release = release, hardware = hardware, updateState = updateState, @@ -128,18 +126,18 @@ constructor( release: FirmwareRelease, hardware: DeviceHardware, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri?, + firmwareUri: CommonUri?, transportFactory: () -> UnifiedOtaProtocol, rebootMode: Int, connectionAttempts: Int, - ): File? = try { + ): String? = try { withContext(Dispatchers.IO) { // Step 1: Get firmware file val firmwareFile = obtainFirmwareFile(release, hardware, firmwareUri, updateState) ?: return@withContext null // Step 2: Calculate Hash and Trigger Reboot - val sha256Bytes = FirmwareHashUtil.calculateSha256Bytes(firmwareFile) + val sha256Bytes = FirmwareHashUtil.calculateSha256Bytes(java.io.File(firmwareFile)) val sha256Hash = FirmwareHashUtil.bytesToHex(sha256Bytes) Logger.i { "ESP32 OTA: Firmware hash: $sha256Hash" } triggerRebootOta(rebootMode, sha256Bytes) @@ -180,11 +178,12 @@ constructor( null } + @Suppress("UnusedPrivateMember") private suspend fun downloadFirmware( release: FirmwareRelease, hardware: DeviceHardware, updateState: (FirmwareUpdateState) -> Unit, - ): File? { + ): String? { val downloadingMsg = getString(Res.string.firmware_update_downloading_percent, 0).replace(Regex(":?\\s*%1\\\$d%?"), "").trim() updateState(FirmwareUpdateState.Downloading(ProgressState(message = downloadingMsg, progress = 0f))) @@ -198,12 +197,14 @@ constructor( } } - private suspend fun getFirmwareFromUri(uri: Uri): File? = withContext(Dispatchers.IO) { - val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext null - val tempFile = File(context.cacheDir, "firmware_update/ota_firmware.bin") + private suspend fun getFirmwareFromUri(uri: CommonUri): String? = withContext(Dispatchers.IO) { + val inputStream = + context.contentResolver.openInputStream(uri.toPlatformUri() as android.net.Uri) + ?: return@withContext null + val tempFile = java.io.File(context.cacheDir, "firmware_update/ota_firmware.bin") tempFile.parentFile?.mkdirs() inputStream.use { input -> tempFile.outputStream().use { output -> input.copyTo(output) } } - tempFile + tempFile.absolutePath } private fun triggerRebootOta(mode: Int, hash: ByteArray?) { @@ -227,24 +228,37 @@ constructor( private suspend fun obtainFirmwareFile( release: FirmwareRelease, hardware: DeviceHardware, - firmwareUri: Uri?, + firmwareUri: CommonUri?, updateState: (FirmwareUpdateState) -> Unit, - ): File? { - val firmwareFile = - if (firmwareUri != null) { - val loadingMsg = getString(Res.string.firmware_update_loading) - updateState(FirmwareUpdateState.Processing(ProgressState(loadingMsg))) - getFirmwareFromUri(firmwareUri) - } else { - downloadFirmware(release, hardware, updateState) - } + ): String? { + val downloadingMsg = + getString(Res.string.firmware_update_downloading_percent, 0).replace(Regex(":?\\s*%1\\\$d%?"), "").trim() - if (firmwareFile == null) { - val retrievalFailedMsg = getString(Res.string.firmware_update_retrieval_failed) - updateState(FirmwareUpdateState.Error(retrievalFailedMsg)) - return null + updateState(FirmwareUpdateState.Downloading(ProgressState(message = downloadingMsg, progress = 0f))) + + return if (firmwareUri != null) { + val extractingMsg = getString(Res.string.firmware_update_extracting) + updateState(FirmwareUpdateState.Processing(ProgressState(message = extractingMsg))) + getFirmwareFromUri(firmwareUri) + } else { + val firmwareFile = + firmwareRetriever.retrieveEsp32Firmware(release, hardware) { progress -> + val percent = (progress * PERCENT_MAX).toInt() + updateState( + FirmwareUpdateState.Downloading( + ProgressState(message = downloadingMsg, progress = progress, details = "$percent%"), + ), + ) + } + + if (firmwareFile == null) { + val errorMsg = getString(Res.string.firmware_update_not_found_in_release, hardware.displayName) + updateState(FirmwareUpdateState.Error(errorMsg)) + null + } else { + firmwareFile + } } - return firmwareFile } private suspend fun connectToDevice( @@ -273,16 +287,17 @@ constructor( @Suppress("LongMethod") private suspend fun executeOtaSequence( transport: UnifiedOtaProtocol, - firmwareFile: File, + firmwareFile: String, sha256Hash: String, rebootMode: Int, updateState: (FirmwareUpdateState) -> Unit, ) { + val file = java.io.File(firmwareFile) // Step 5: Start OTA val startingOtaMsg = getString(Res.string.firmware_update_starting_ota) updateState(FirmwareUpdateState.Processing(ProgressState(startingOtaMsg))) transport - .startOta(sizeBytes = firmwareFile.length(), sha256Hash = sha256Hash) { status -> + .startOta(sizeBytes = file.length(), sha256Hash = sha256Hash) { status -> when (status) { OtaHandshakeStatus.Erasing -> { val erasingMsg = getString(Res.string.firmware_update_erasing) @@ -295,7 +310,7 @@ constructor( // Step 6: Stream val uploadingMsg = getString(Res.string.firmware_update_uploading) updateState(FirmwareUpdateState.Updating(ProgressState(uploadingMsg, 0f))) - val firmwareData = firmwareFile.readBytes() + val firmwareData = file.readBytes() val chunkSize = if (rebootMode == 1) { BleOtaTransport.RECOMMENDED_CHUNK_SIZE diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt similarity index 100% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/FirmwareHashUtil.kt diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt similarity index 100% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocol.kt diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt similarity index 100% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt rename to feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/ota/WifiOtaTransport.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/FirmwareRetrieverTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt b/feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt similarity index 100% rename from feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt rename to feature/firmware/src/androidUnitTest/kotlin/org/meshtastic/feature/firmware/ota/UnifiedOtaProtocolTest.kt diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt new file mode 100644 index 000000000..a7253ba53 --- /dev/null +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DfuInternalState.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware + +sealed interface DfuInternalState { + val address: String + + data class Connecting(override val address: String) : DfuInternalState + + data class Connected(override val address: String) : DfuInternalState + + data class Starting(override val address: String) : DfuInternalState + + data class EnablingDfuMode(override val address: String) : DfuInternalState + + data class Progress( + override val address: String, + val percent: Int, + val speed: Float, + val avgSpeed: Float, + val currentPart: Int, + val partsTotal: Int, + ) : DfuInternalState + + data class Validating(override val address: String) : DfuInternalState + + data class Disconnecting(override val address: String) : DfuInternalState + + data class Disconnected(override val address: String) : DfuInternalState + + data class Completed(override val address: String) : DfuInternalState + + data class Aborted(override val address: String) : DfuInternalState + + data class Error(override val address: String, val message: String?) : DfuInternalState +} diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt new file mode 100644 index 000000000..b746c1a8c --- /dev/null +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.firmware + +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.model.DeviceHardware + +interface FirmwareFileHandler { + fun cleanupAllTemporaryFiles() + + suspend fun checkUrlExists(url: String): Boolean + + suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): String? + + suspend fun extractFirmware( + uri: CommonUri, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String? = null, + ): String? + + suspend fun extractFirmwareFromZip( + zipFilePath: String, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String? = null, + ): String? + + suspend fun getFileSize(path: String): Long + + suspend fun deleteFile(path: String) + + suspend fun copyFileToUri(sourcePath: String, destinationUri: CommonUri): Long + + suspend fun copyUriToUri(sourceUri: CommonUri, destinationUri: CommonUri): Long +} diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt similarity index 100% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.kt diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt similarity index 87% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt index df5ce6e78..b2bce3696 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateHandler.kt @@ -16,10 +16,9 @@ */ package org.meshtastic.feature.firmware -import android.net.Uri +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware -import java.io.File /** Common interface for all firmware update handlers (BLE DFU, ESP32 OTA, USB). */ interface FirmwareUpdateHandler { @@ -31,13 +30,13 @@ interface FirmwareUpdateHandler { * @param target The target identifier (e.g., Bluetooth address, IP address, or empty for USB) * @param updateState Callback to report back state changes * @param firmwareUri Optional URI for a local firmware file (bypasses download) - * @return The downloaded/extracted firmware file, or null if it was a local file or update finished + * @return The downloaded/extracted firmware file path, or null if it was a local file or update finished */ suspend fun startUpdate( release: FirmwareRelease, hardware: DeviceHardware, target: String, updateState: (FirmwareUpdateState) -> Unit, - firmwareUri: Uri? = null, - ): File? + firmwareUri: CommonUri? = null, + ): String? } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt new file mode 100644 index 000000000..bbe804178 --- /dev/null +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ +package org.meshtastic.feature.firmware + +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.DeviceHardware + +interface FirmwareUpdateManager { + suspend fun startUpdate( + release: FirmwareRelease, + hardware: DeviceHardware, + address: String, + updateState: (FirmwareUpdateState) -> Unit, + firmwareUri: CommonUri? = null, + ): String? + + fun dfuProgressFlow(): kotlinx.coroutines.flow.Flow +} diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt similarity index 93% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt index 3a3055391..48dc7cef5 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateState.kt @@ -16,10 +16,9 @@ */ package org.meshtastic.feature.firmware -import android.net.Uri +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware -import java.io.File /** * Represents the progress of a long-running firmware update task. @@ -58,6 +57,6 @@ sealed interface FirmwareUpdateState { data object Success : FirmwareUpdateState - data class AwaitingFileSave(val uf2File: File?, val fileName: String, val sourceUri: Uri? = null) : + data class AwaitingFileSave(val uf2FilePath: String?, val fileName: String, val sourceUri: CommonUri? = null) : FirmwareUpdateState } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt similarity index 96% rename from feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt rename to feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 2f3b9e449..4ae8b6af6 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -16,11 +16,9 @@ */ package org.meshtastic.feature.firmware -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.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -37,6 +35,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareReleaseType @@ -73,8 +72,6 @@ import org.meshtastic.core.resources.firmware_update_unknown_hardware import org.meshtastic.core.resources.firmware_update_updating import org.meshtastic.core.resources.firmware_update_validating import org.meshtastic.core.resources.unknown -import java.io.File -import javax.inject.Inject private const val DFU_RECONNECT_PREFIX = "x" private const val PERCENT_MAX_VALUE = 100f @@ -87,11 +84,8 @@ private const val MILLIS_PER_SECOND = 1000L private val BLUETOOTH_ADDRESS_REGEX = Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}") -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class FirmwareUpdateViewModel -@Inject -constructor( +open class FirmwareUpdateViewModel( private val firmwareReleaseRepository: FirmwareReleaseRepository, private val deviceHardwareRepository: DeviceHardwareRepository, private val nodeRepository: NodeRepository, @@ -99,7 +93,7 @@ constructor( private val radioPrefs: RadioPrefs, private val bootloaderWarningDataSource: BootloaderWarningDataSource, private val firmwareUpdateManager: FirmwareUpdateManager, - private val usbManager: UsbManager, + private val usbManager: FirmwareUsbManager, private val fileHandler: FirmwareFileHandler, ) : ViewModel() { @@ -121,7 +115,7 @@ constructor( val currentFirmwareVersion = _currentFirmwareVersion.asStateFlow() private var updateJob: Job? = null - private var tempFirmwareFile: File? = null + private var tempFirmwareFile: String? = null private var originalDeviceAddress: String? = null init { @@ -135,7 +129,7 @@ constructor( override fun onCleared() { super.onCleared() - tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + viewModelScope.launch { tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) } } fun setReleaseType(type: FirmwareReleaseType) { @@ -251,9 +245,9 @@ constructor( } } - fun saveDfuFile(uri: Uri) { + fun saveDfuFile(uri: CommonUri) { val currentState = _state.value as? FirmwareUpdateState.AwaitingFileSave ?: return - val firmwareFile = currentState.uf2File + val firmwareFile = currentState.uf2FilePath val sourceUri = currentState.sourceUri viewModelScope.launch { @@ -284,7 +278,7 @@ constructor( } } - fun startUpdateFromFile(uri: Uri) { + fun startUpdateFromFile(uri: CommonUri) { val currentState = _state.value as? FirmwareUpdateState.Ready ?: return if (currentState.updateMethod is FirmwareUpdateMethod.Ble && !isValidBluetoothAddress(currentState.address)) { viewModelScope.launch { @@ -305,7 +299,7 @@ constructor( val extractedFile = fileHandler.extractFirmware(uri, currentState.deviceHardware, extension) tempFirmwareFile = extractedFile - val firmwareUri = if (extractedFile != null) Uri.fromFile(extractedFile) else uri + val firmwareUri = if (extractedFile != null) CommonUri.parse("file://$extractedFile") else uri tempFirmwareFile = firmwareUpdateManager.startUpdate( @@ -385,7 +379,7 @@ constructor( } } - private fun handleDfuProgress(dfuState: DfuInternalState.Progress) { + private suspend fun handleDfuProgress(dfuState: DfuInternalState.Progress) { val progress = dfuState.percent / PERCENT_MAX_VALUE val percentText = "${dfuState.percent}%" @@ -394,7 +388,7 @@ constructor( val speedKib = speedBytesPerSec / KIB_DIVISOR // Calculate ETA - val totalBytes = tempFirmwareFile?.length() ?: 0L + val totalBytes = tempFirmwareFile?.let { fileHandler.getFileSize(it) } ?: 0L val etaText = if (totalBytes > 0 && speedBytesPerSec > 0 && dfuState.percent > 0) { val remainingBytes = totalBytes * (1f - progress) @@ -483,9 +477,9 @@ constructor( } } -private fun cleanupTemporaryFiles(fileHandler: FirmwareFileHandler, tempFirmwareFile: File?): File? { +private suspend fun cleanupTemporaryFiles(fileHandler: FirmwareFileHandler, tempFirmwareFile: String?): String? { runCatching { - tempFirmwareFile?.takeIf { it.exists() }?.delete() + tempFirmwareFile?.let { fileHandler.deleteFile(it) } fileHandler.cleanupAllTemporaryFiles() } .onFailure { e -> Logger.w(e) { "Failed to cleanup temp files" } } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt new file mode 100644 index 000000000..d102ed4e4 --- /dev/null +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUsbManager.kt @@ -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 . + */ +package org.meshtastic.feature.firmware + +import kotlinx.coroutines.flow.Flow + +interface FirmwareUsbManager { + fun deviceDetachFlow(): Flow +} diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt new file mode 100644 index 000000000..fbb78ffd9 --- /dev/null +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/di/FeatureFirmwareModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.firmware.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.firmware") +class FeatureFirmwareModule diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index bf7667a61..f3f63c7ea 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -19,7 +19,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kmp.library.compose) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.devtools.ksp) + alias(libs.plugins.meshtastic.koin) } kotlin { @@ -39,13 +39,12 @@ kotlin { implementation(projects.core.resources) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.koin.compose.viewmodel) implementation(libs.androidx.navigation3.runtime) - implementation(libs.javax.inject) } androidMain.dependencies { implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(libs.accompanist.permissions) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) @@ -53,7 +52,6 @@ kotlin { implementation(libs.androidx.compose.ui.text) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.navigation3.ui) - implementation(libs.hilt.android) } androidUnitTest.dependencies { @@ -67,8 +65,3 @@ kotlin { } } } - -dependencies { - add("kspAndroid", libs.androidx.hilt.compiler) - add("kspAndroid", libs.hilt.compiler) -} diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt new file mode 100644 index 000000000..4d15389be --- /dev/null +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.intro.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.intro") +class FeatureIntroModule diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index d701a243b..a03257bcc 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -18,7 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kmp.library.compose) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.devtools.ksp) + alias(libs.plugins.meshtastic.koin) } kotlin { @@ -45,12 +45,11 @@ kotlin { implementation(projects.core.di) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.javax.inject) + implementation(libs.koin.compose.viewmodel) } androidMain.dependencies { implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(libs.androidx.datastore) implementation(libs.androidx.datastore.preferences) implementation(libs.accompanist.permissions) @@ -68,7 +67,6 @@ kotlin { implementation(libs.androidx.savedstate.ktx) implementation(libs.material) implementation(libs.kermit) - implementation(libs.hilt.android) } androidUnitTest.dependencies { @@ -81,8 +79,3 @@ kotlin { } } } - -dependencies { - add("kspAndroid", libs.androidx.hilt.compiler) - add("kspAndroid", libs.hilt.compiler) -} diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt index df3787a31..7443b2e6d 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt @@ -16,15 +16,14 @@ */ package org.meshtastic.feature.map +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 javax.inject.Inject -open class SharedMapViewModel -@Inject -constructor( +@KoinViewModel +open class SharedMapViewModel( mapPrefs: MapPrefs, nodeRepository: NodeRepository, packetRepository: PacketRepository, diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt new file mode 100644 index 000000000..a6ff74b17 --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/di/FeatureMapModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.map.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.map") +class FeatureMapModule diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 481737827..de7ea9d28 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -18,7 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kmp.library.compose) - alias(libs.plugins.devtools.ksp) + alias(libs.plugins.meshtastic.koin) } kotlin { @@ -44,13 +44,12 @@ kotlin { implementation(projects.core.ui) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.koin.compose.viewmodel) implementation(libs.kermit) - implementation(libs.javax.inject) } androidMain.dependencies { implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(libs.accompanist.permissions) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) @@ -64,8 +63,6 @@ kotlin { implementation(libs.androidx.navigation.compose) implementation(libs.androidx.paging.compose) implementation(libs.androidx.work.runtime.ktx) - implementation(libs.androidx.hilt.work) - implementation(libs.hilt.android) } commonTest.dependencies { @@ -82,8 +79,3 @@ kotlin { } } } - -dependencies { - add("kspAndroid", libs.androidx.hilt.compiler) - add("kspAndroid", libs.hilt.compiler) -} diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt new file mode 100644 index 000000000..bbb7679f2 --- /dev/null +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.messaging.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.messaging") +class FeatureMessagingModule diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index de857e9d9..e875ce3c1 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -14,60 +14,82 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import com.android.build.api.dsl.LibraryExtension plugins { - alias(libs.plugins.meshtastic.android.library) - alias(libs.plugins.meshtastic.android.library.flavors) - alias(libs.plugins.meshtastic.android.library.compose) - alias(libs.plugins.meshtastic.hilt) + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kotlinx.serialization) + alias(libs.plugins.meshtastic.koin) } -configure { - namespace = "org.meshtastic.feature.node" +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.feature.node" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } - defaultConfig { manifestPlaceholders["MAPS_API_KEY"] = "DEBUG_KEY" } + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.datastore) + implementation(projects.core.domain) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(projects.core.proto) + implementation(projects.core.repository) + implementation(projects.core.resources) + implementation(projects.core.service) + implementation(projects.core.ui) + implementation(projects.core.di) + implementation(projects.feature.map) - testOptions { unitTests { isIncludeAndroidResources = true } } -} - -dependencies { - implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) - implementation(projects.core.di) - implementation(projects.core.model) - implementation(projects.core.proto) - implementation(projects.core.service) - implementation(projects.core.resources) - implementation(projects.core.ui) - implementation(projects.core.navigation) - implementation(projects.feature.map) - - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.navigation.common) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.kermit) - implementation(libs.coil) - implementation(libs.markdown.renderer.android) - implementation(libs.markdown.renderer.m3) - implementation(libs.markdown.renderer) - implementation(libs.vico.compose) - implementation(libs.vico.compose.m2) - implementation(libs.vico.compose.m3) - - googleImplementation(libs.location.services) - googleImplementation(libs.maps.compose) - testImplementation(libs.junit) - testImplementation(libs.mockk) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.androidx.compose.ui.test.junit4) - testImplementation(libs.androidx.test.ext.junit) - testImplementation(libs.robolectric) - debugImplementation(libs.androidx.compose.ui.test.manifest) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.kermit) + implementation(libs.kotlinx.collections.immutable) + } + + androidMain.dependencies { + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.accompanist.permissions) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.navigation.common) + implementation(libs.coil) + implementation(libs.markdown.renderer.android) + implementation(libs.markdown.renderer.m3) + implementation(libs.markdown.renderer) + implementation(libs.vico.compose) + implementation(libs.vico.compose.m2) + implementation(libs.vico.compose.m3) + implementation(libs.nordic.common.core) + implementation(libs.nordic.common.permissions.ble) + + // These were in googleImplementation, but KMP with android-kotlin-multiplatform-library + // handles flavors differently. For now, we put them in androidMain if they are needed. + // In a real KMP flavored module, we'd use different source sets. + // But Priority 4b suggests Option A: extract flavored stuff to app module. + // So InlineMap will move to app module soon. + implementation(libs.location.services) + implementation(libs.maps.compose) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.mockk) + implementation(libs.robolectric) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.compose.ui.test.junit4) + implementation(libs.androidx.test.ext.junit) + } + } } diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml index 2465cc012..c71bc233d 100644 --- a/feature/node/detekt-baseline.xml +++ b/feature/node/detekt-baseline.xml @@ -5,8 +5,8 @@ CyclomaticComplexMethod:CompassViewModel.kt$CompassViewModel$@Suppress("ReturnCount") private fun calculatePositionalAccuracyMeters(): Float? CyclomaticComplexMethod:NodeDetailActions.kt$NodeDetailActions$fun handleNodeMenuAction(scope: CoroutineScope, action: NodeMenuAction) CyclomaticComplexMethod:NodeDetailViewModel.kt$NodeDetailViewModel$fun handleNodeMenuAction(action: NodeMenuAction) - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5 - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7 + MagicNumber:CompassViewModel.kt$CompassViewModel$180.0 + TooGenericExceptionCaught:MetricsViewModel.kt$MetricsViewModel$e: Exception + TooGenericExceptionCaught:NodeManagementActions.kt$NodeManagementActions$ex: Exception diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt similarity index 86% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt index 5bbda223a..416abc37c 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidCompassHeadingProvider.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.compass import android.content.Context @@ -22,29 +21,19 @@ import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject +import org.koin.core.annotation.Single private const val ROTATION_MATRIX_SIZE = 9 private const val ORIENTATION_SIZE = 3 private const val FULL_CIRCLE_DEGREES = 360f -data class HeadingState( - val heading: Float? = null, // 0..360 degrees - val hasSensor: Boolean = true, - val accuracy: Int = SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM, -) +@Single +class AndroidCompassHeadingProvider(private val context: Context) : CompassHeadingProvider { -class CompassHeadingProvider @Inject constructor(@ApplicationContext private val context: Context) { - - /** - * Emits compass heading in degrees (magnetic). Callers can correct for true north using the latest location data - * when available. - */ - fun headingUpdates(): Flow = callbackFlow { + override fun headingUpdates(): Flow = callbackFlow { val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager if (sensorManager == null) { trySend(HeadingState(hasSensor = false)) @@ -93,7 +82,7 @@ class CompassHeadingProvider @Inject constructor(@ApplicationContext private val } SensorManager.getOrientation(rotationMatrix, orientation) - var azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat() + val azimuth = Math.toDegrees(orientation[0].toDouble()).toFloat() val heading = (azimuth + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES trySend(HeadingState(heading = heading, hasSensor = true, accuracy = event.accuracy)) diff --git a/app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt similarity index 59% rename from app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt index 7a5f389ae..9cdac1e2d 100644 --- a/app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidMagneticFieldProvider.kt @@ -14,15 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app +package org.meshtastic.feature.node.compass -import android.app.Application -import android.content.Context -import androidx.test.runner.AndroidJUnitRunner -import dagger.hilt.android.testing.HiltTestApplication +import android.hardware.GeomagneticField +import org.koin.core.annotation.Single -@Suppress("unused") -class TestRunner : AndroidJUnitRunner() { - override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application = - super.newApplication(cl, HiltTestApplication::class.java.name, context) +@Single +class AndroidMagneticFieldProvider : MagneticFieldProvider { + override fun getDeclination(latitude: Double, longitude: Double, altitude: Double, timeMillis: Long): Float { + val geomagneticField = GeomagneticField(latitude.toFloat(), longitude.toFloat(), altitude.toFloat(), timeMillis) + return geomagneticField.declination + } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt similarity index 84% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt index ade08492e..48241dd12 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/compass/AndroidPhoneLocationProvider.kt @@ -25,31 +25,18 @@ import androidx.core.content.ContextCompat import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationRequestCompat -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn +import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers -import javax.inject.Inject -data class PhoneLocationState( - val permissionGranted: Boolean, - val providerEnabled: Boolean, - val location: Location? = null, -) { - val hasFix: Boolean - get() = location != null -} +@Single +class AndroidPhoneLocationProvider(private val context: Context, private val dispatchers: CoroutineDispatchers) : + PhoneLocationProvider { -class PhoneLocationProvider -@Inject -constructor( - @ApplicationContext private val context: Context, - private val dispatchers: CoroutineDispatchers, -) { - // Streams phone location (and permission/provider state) so the compass stays gated on real fixes. - fun locationUpdates(): Flow = callbackFlow { + override fun locationUpdates(): Flow = callbackFlow { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager if (locationManager == null) { trySend(PhoneLocationState(permissionGranted = false, providerEnabled = false)) @@ -59,7 +46,7 @@ constructor( if (!hasLocationPermission()) { trySend(PhoneLocationState(permissionGranted = false, providerEnabled = false)) - close() // Just closing it off, like how I'll close my legs around your waist + close() return@callbackFlow } @@ -70,7 +57,7 @@ constructor( PhoneLocationState( permissionGranted = true, providerEnabled = LocationManagerCompat.isLocationEnabled(locationManager), - location = lastLocation, + location = lastLocation?.toPhoneLocation(), ), ) } @@ -96,7 +83,6 @@ constructor( val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER) try { - // Get initial fix if available lastLocation = providers .mapNotNull { provider -> locationManager.getLastKnownLocation(provider) } @@ -131,6 +117,9 @@ constructor( ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED + private fun Location.toPhoneLocation() = + PhoneLocation(latitude = latitude, longitude = longitude, altitude = altitude, timeMillis = time) + companion object { private const val MIN_UPDATE_INTERVAL_MS = 1_000L } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/DistanceInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/ElevationInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/FirmwareReleaseSheetContent.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/HopsInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/IconInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/IconInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/IconInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCard.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCard.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt index f7d46a939..1eb5a75b1 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/InfoCardPreview.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.component import androidx.compose.material.icons.Icons diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt index 8821065a0..5bdf6b125 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.PreviewLightDark import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeItem.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeItem.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NodeStatusIcons.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NotesSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt index f4e3bb454..57c7980df 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PositionSection.kt @@ -52,6 +52,7 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.exchange_position import org.meshtastic.core.resources.open_compass import org.meshtastic.core.resources.position +import org.meshtastic.core.ui.util.LocalInlineMapProvider import org.meshtastic.feature.node.model.LogsType import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.NodeDetailAction @@ -59,6 +60,7 @@ import org.meshtastic.proto.Config private const val EXCHANGE_BUTTON_WEIGHT = 1.1f private const val COMPASS_BUTTON_WEIGHT = 0.9f +private const val MAP_HEIGHT_DP = 200 /** * Displays node position details, last update time, distance, and related actions like requesting position and @@ -126,8 +128,8 @@ fun PositionSection( @Composable private fun PositionMap(node: Node, distance: String?) { Box(modifier = Modifier.padding(vertical = 4.dp)) { - Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth().height(200.dp)) { - InlineMap(node = node, Modifier.fillMaxSize()) + Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth().height(MAP_HEIGHT_DP.dp)) { + LocalInlineMapProvider.current(node, Modifier.fillMaxSize()) } if (distance != null && distance.isNotEmpty()) { Surface( diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/PowerMetrics.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/SatelliteCountInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt index c43829787..1dc5d2905 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailActions.kt @@ -17,15 +17,13 @@ package org.meshtastic.feature.node.detail import kotlinx.coroutines.CoroutineScope +import org.koin.core.annotation.Single import org.meshtastic.core.model.Position import org.meshtastic.core.model.TelemetryType import org.meshtastic.feature.node.component.NodeMenuAction -import javax.inject.Inject -import javax.inject.Singleton -@Singleton +@Single class NodeDetailActions -@Inject constructor( private val nodeManagementActions: NodeManagementActions, private val nodeRequestActions: NodeRequestActions, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt similarity index 93% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt index 8f4c9dd09..223cc5e5e 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt @@ -21,10 +21,7 @@ import android.content.Intent import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -55,9 +52,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.Node import org.meshtastic.core.navigation.Route @@ -94,10 +91,11 @@ private sealed interface NodeDetailOverlay { fun NodeDetailScreen( nodeId: Int, modifier: Modifier = Modifier, - viewModel: NodeDetailViewModel = hiltViewModel(), + viewModel: NodeDetailViewModel, navigateToMessages: (String) -> Unit = {}, onNavigate: (Route) -> Unit = {}, onNavigateUp: () -> Unit = {}, + compassViewModel: CompassViewModel? = null, ) { LaunchedEffect(nodeId) { viewModel.start(nodeId) } @@ -120,6 +118,7 @@ fun NodeDetailScreen( navigateToMessages = navigateToMessages, onNavigate = onNavigate, onNavigateUp = onNavigateUp, + compassViewModel = compassViewModel, ) } @@ -133,12 +132,13 @@ private fun NodeDetailScaffold( navigateToMessages: (String) -> Unit, onNavigate: (Route) -> Unit, onNavigateUp: () -> Unit, + compassViewModel: CompassViewModel? = null, ) { var activeOverlay by remember { mutableStateOf(null) } val inspectionMode = LocalInspectionMode.current - val compassViewModel = if (inspectionMode) null else hiltViewModel() + val actualCompassViewModel = compassViewModel ?: if (inspectionMode) null else koinViewModel() val compassUiState by - compassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) } + actualCompassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) } val node = uiState.node val listState = rememberLazyListState() @@ -167,7 +167,7 @@ private fun NodeDetailScaffold( when (action) { is NodeDetailAction.ShareContact -> activeOverlay = NodeDetailOverlay.SharedContact is NodeDetailAction.OpenCompass -> { - compassViewModel?.start(action.node, action.displayUnits) + actualCompassViewModel?.start(action.node, action.displayUnits) activeOverlay = NodeDetailOverlay.Compass } else -> @@ -186,7 +186,7 @@ private fun NodeDetailScaffold( ) } - NodeDetailOverlays(activeOverlay, node, compassUiState, compassViewModel, { activeOverlay = null }) { + NodeDetailOverlays(activeOverlay, node, compassUiState, actualCompassViewModel, { activeOverlay = null }) { viewModel.handleNodeMenuAction(NodeMenuAction.RequestPosition(it)) } } @@ -200,12 +200,7 @@ private fun NodeDetailContent( onFirmwareSelect: (FirmwareRelease) -> Unit, modifier: Modifier = Modifier, ) { - AnimatedContent( - targetState = uiState.node != null, - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "NodeDetailContent", - modifier = modifier, - ) { isNodePresent -> + Crossfade(targetState = uiState.node != null, label = "NodeDetailContent", modifier = modifier) { isNodePresent -> if (isNodePresent && uiState.node != null) { NodeDetailList( node = uiState.node, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index bdaa2a97a..107a0a9dc 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -61,7 +61,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -96,8 +95,8 @@ import org.meshtastic.proto.SharedContact @Composable fun NodeListScreen( navigateToNodeDetails: (Int) -> Unit, + viewModel: NodeListViewModel, onNavigateToChannels: () -> Unit = {}, - viewModel: NodeListViewModel = hiltViewModel(), scrollToTopEvents: Flow? = null, activeNodeId: Int? = null, ) { @@ -156,7 +155,9 @@ fun NodeListScreen( alignment = Alignment.BottomEnd, ), onImport = { uri -> - viewModel.handleScannedUri(uri) { scope.launch { context.showToast(Res.string.channel_invalid) } } + viewModel.handleScannedUri(uri.toString()) { + scope.launch { context.showToast(Res.string.channel_invalid) } + } }, onDismissSharedContact = { viewModel.setSharedContactRequested(null) }, isContactContext = true, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/ChartStyling.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt similarity index 99% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt index d7ee8782e..851f199a3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState import com.patrykandpatrick.vico.compose.cartesian.axis.Axis @@ -123,7 +122,7 @@ private val LEGEND_DATA = @Suppress("LongMethod") @Composable -fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentCharts.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt similarity index 99% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index cffc3d383..376f8b0ef 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowSeconds @@ -73,7 +72,7 @@ import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.Telemetry @Composable -fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val graphData by viewModel.environmentGraphingData.collectAsStateWithLifecycle() val filteredTelemetries by viewModel.filteredEnvironmentMetrics.collectAsStateWithLifecycle() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HardwareModelExtensions.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt index c870b5e2c..d3d29dc05 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowSeconds @@ -78,7 +77,7 @@ import java.text.DecimalFormat @OptIn(ExperimentalFoundationApi::class) @Composable -fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by metricsViewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt index 006e02fcf..a9f5d8c00 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt @@ -38,7 +38,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter @@ -61,11 +60,7 @@ import org.meshtastic.feature.node.detail.NodeRequestEffect @OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -fun NeighborInfoLogScreen( - modifier: Modifier = Modifier, - viewModel: MetricsViewModel = hiltViewModel(), - onNavigateUp: () -> Unit, -) { +fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt index f566fd088..4873d0c0a 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt @@ -43,7 +43,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState import com.patrykandpatrick.vico.compose.cartesian.axis.HorizontalAxis @@ -174,7 +173,7 @@ private fun PaxMetricsChart( @Composable @Suppress("MagicNumber", "LongMethod") -fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by metricsViewModel.state.collectAsStateWithLifecycle() val paxMetrics by metricsViewModel.filteredPaxMetrics.collectAsStateWithLifecycle() val timeFrame by metricsViewModel.timeFrame.collectAsStateWithLifecycle() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt index 55d793957..551fe54f2 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt @@ -59,7 +59,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.nowSeconds @@ -172,7 +171,7 @@ private fun ActionButtons( @Suppress("LongMethod") @Composable -fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt similarity index 99% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt index bdd89a059..f07feed67 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState import com.patrykandpatrick.vico.compose.cartesian.axis.Axis @@ -107,7 +106,7 @@ private val LEGEND_DATA = @Suppress("LongMethod") @Composable -fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt similarity index 98% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt index 0cee152ce..a3a8feec8 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState import com.patrykandpatrick.vico.compose.cartesian.axis.Axis @@ -85,7 +84,7 @@ private val LEGEND_DATA = @Suppress("LongMethod") @Composable -fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) { +fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt similarity index 99% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index 1fdd5cf5b..602bcebae 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource @@ -83,7 +82,7 @@ import org.meshtastic.proto.RouteDiscovery @Composable fun TracerouteLogScreen( modifier: Modifier = Modifier, - viewModel: MetricsViewModel = hiltViewModel(), + viewModel: MetricsViewModel, onNavigateUp: () -> Unit, onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> }, ) { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt similarity index 94% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt index 162af7350..ec3cf5ea5 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.flowOf import org.jetbrains.compose.resources.stringResource @@ -53,12 +52,13 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Route import org.meshtastic.core.ui.theme.TracerouteColors import org.meshtastic.core.ui.util.LocalMapViewProvider +import org.meshtastic.core.ui.util.LocalTracerouteMapOverlayInsetsProvider import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.proto.Position @Composable fun TracerouteMapScreen( - metricsViewModel: MetricsViewModel = hiltViewModel(), + metricsViewModel: MetricsViewModel, requestId: Int, logUuid: String? = null, onNavigateUp: () -> Unit, @@ -102,6 +102,7 @@ private fun TracerouteMapScaffold( ) { var tracerouteNodesShown by remember { mutableStateOf(0) } var tracerouteNodesTotal by remember { mutableStateOf(0) } + val insets = LocalTracerouteMapOverlayInsetsProvider.current Scaffold( topBar = { MainAppBar( @@ -128,10 +129,8 @@ private fun TracerouteMapScaffold( }, ) Column( - modifier = - Modifier.align(TracerouteMapOverlayInsets.overlayAlignment) - .padding(TracerouteMapOverlayInsets.overlayPadding), - horizontalAlignment = TracerouteMapOverlayInsets.contentHorizontalAlignment, + modifier = Modifier.align(insets.overlayAlignment).padding(insets.overlayPadding), + horizontalAlignment = insets.contentHorizontalAlignment, verticalArrangement = Arrangement.spacedBy(8.dp), ) { TracerouteNodeCount(shown = tracerouteNodesShown, total = tracerouteNodesTotal) diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/MetricInfo.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt rename to feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt similarity index 63% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt index 4864abe7a..4680fc111 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceMapKey.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassHeadingProvider.kt @@ -14,13 +14,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.feature.node.compass -import dagger.MapKey -import org.meshtastic.core.model.InterfaceId +import kotlinx.coroutines.flow.Flow -/** Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key. */ -@MapKey -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER) -@Retention(AnnotationRetention.RUNTIME) -annotation class InterfaceMapKey(val value: InterfaceId) +data class HeadingState( + val heading: Float? = null, // 0..360 degrees + val hasSensor: Boolean = true, + val accuracy: Int = 0, +) + +interface CompassHeadingProvider { + fun headingUpdates(): Flow +} diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassUiState.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt similarity index 95% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt index 3043ef499..9ce9d789c 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt @@ -16,11 +16,9 @@ */ package org.meshtastic.feature.node.compass -import android.hardware.GeomagneticField import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -39,7 +37,6 @@ import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.ui.component.precisionBitsToMeters import org.meshtastic.proto.Config import org.meshtastic.proto.Position -import javax.inject.Inject import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.min @@ -54,13 +51,11 @@ private const val SECONDS_PER_MINUTE = 60 private const val HUNDRED = 100f private const val MILLIMETERS_PER_METER = 1000f -@HiltViewModel @Suppress("TooManyFunctions") -class CompassViewModel -@Inject -constructor( +open class CompassViewModel( private val headingProvider: CompassHeadingProvider, private val phoneLocationProvider: PhoneLocationProvider, + private val magneticFieldProvider: MagneticFieldProvider, private val dispatchers: CoroutineDispatchers, ) : ViewModel() { @@ -192,9 +187,8 @@ constructor( private fun applyTrueNorthCorrection(heading: Float?, locationState: PhoneLocationState): Float? { val loc = locationState.location ?: return heading val baseHeading = heading ?: return null - val geomagnetic = - GeomagneticField(loc.latitude.toFloat(), loc.longitude.toFloat(), loc.altitude.toFloat(), nowMillis) - return (baseHeading + geomagnetic.declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES + val declination = magneticFieldProvider.getDeclination(loc.latitude, loc.longitude, loc.altitude, nowMillis) + return (baseHeading + declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES } private fun formatElapsed(timestampSec: Long): String { @@ -246,6 +240,8 @@ constructor( if (distance <= 0) return FULL_CIRCLE_DEGREES / 2 val radians = atan2(accuracy.toDouble(), distance.toDouble()) - return Math.toDegrees(radians).toFloat().coerceIn(0f, FULL_CIRCLE_DEGREES / 2) + return radiansToDegrees(radians).toFloat().coerceIn(0f, FULL_CIRCLE_DEGREES / 2) } + + private fun radiansToDegrees(radians: Double): Double = radians * 180.0 / kotlin.math.PI } diff --git a/app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt similarity index 62% rename from app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt index 059330e7a..7e0ce4983 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/DatabaseModule.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/MagneticFieldProvider.kt @@ -14,21 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.di +package org.meshtastic.feature.node.compass -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@InstallIn(SingletonComponent::class) -@Module -interface DatabaseModule { - - @Binds - @Singleton - fun bindDatabaseManager( - impl: org.meshtastic.core.database.DatabaseManager, - ): org.meshtastic.core.common.database.DatabaseManager +interface MagneticFieldProvider { + fun getDeclination(latitude: Double, longitude: Double, altitude: Double, timeMillis: Long): Float } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt new file mode 100644 index 000000000..e7f39b9a5 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/PhoneLocationProvider.kt @@ -0,0 +1,34 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.compass + +import kotlinx.coroutines.flow.Flow + +data class PhoneLocation(val latitude: Double, val longitude: Double, val altitude: Double, val timeMillis: Long) + +data class PhoneLocationState( + val permissionGranted: Boolean, + val providerEnabled: Boolean, + val location: PhoneLocation? = null, +) { + val hasFix: Boolean + get() = location != null +} + +interface PhoneLocationProvider { + fun locationUpdates(): Flow +} diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeMenuAction.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt similarity index 88% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 8d6bb18ae..ebe720bb3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -43,20 +42,8 @@ import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.metrics.EnvironmentMetricsState import org.meshtastic.feature.node.model.LogsType import org.meshtastic.feature.node.model.MetricsState -import javax.inject.Inject -/** - * UI state for the Node Details screen. - * - * @property node The node being viewed, or null if loading. - * @property nodeName The display name for the node, resolved in the UI. - * @property ourNode Information about the locally connected node. - * @property metricsState Aggregated sensor and signal metrics. - * @property environmentState Standardized environmental sensor data. - * @property availableLogs a set of log types available for this node. - * @property lastTracerouteTime Timestamp of the last successful traceroute request. - * @property lastRequestNeighborsTime Timestamp of the last successful neighbor info request. - */ +/** UI state for the Node Details screen. */ @androidx.compose.runtime.Stable data class NodeDetailUiState( val node: Node? = null, @@ -73,11 +60,8 @@ data class NodeDetailUiState( * ViewModel for the Node Details screen, coordinating data from the node database, mesh logs, and radio configuration. */ @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class NodeDetailViewModel -@Inject -constructor( - savedStateHandle: SavedStateHandle, +open class NodeDetailViewModel( + private val savedStateHandle: SavedStateHandle, private val nodeManagementActions: NodeManagementActions, private val nodeRequestActions: NodeRequestActions, private val serviceRepository: ServiceRepository, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt similarity index 93% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index fbf79a4d7..3dcc1c593 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString +import org.koin.core.annotation.Single import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.service.ServiceAction @@ -40,12 +41,9 @@ import org.meshtastic.core.resources.remove import org.meshtastic.core.resources.remove_node_text import org.meshtastic.core.resources.unmute import org.meshtastic.core.ui.util.AlertManager -import javax.inject.Inject -import javax.inject.Singleton -@Singleton +@Single class NodeManagementActions -@Inject constructor( private val nodeRepository: NodeRepository, private val serviceRepository: ServiceRepository, @@ -127,10 +125,8 @@ constructor( scope.launch(Dispatchers.IO) { try { nodeRepository.setNodeNotes(nodeNum, notes) - } catch (ex: java.io.IOException) { - Logger.e { "Set node notes IO error: ${ex.message}" } - } catch (ex: java.sql.SQLException) { - Logger.e { "Set node notes SQL error: ${ex.message}" } + } catch (ex: Exception) { + Logger.e(ex) { "Set node notes error" } } } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt index 1ca64fae9..45bfb95a5 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.Position import org.meshtastic.core.model.RadioController @@ -45,15 +46,13 @@ import org.meshtastic.core.resources.requesting_from import org.meshtastic.core.resources.signal_quality import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.user_info -import javax.inject.Inject -import javax.inject.Singleton sealed class NodeRequestEffect { data class ShowFeedback(val text: UiText) : NodeRequestEffect() } -@Singleton -class NodeRequestActions @Inject constructor(private val radioController: RadioController) { +@Single +class NodeRequestActions constructor(private val radioController: RadioController) { private val _effects = MutableSharedFlow() val effects: SharedFlow = _effects.asSharedFlow() diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt new file mode 100644 index 000000000..e32e96818 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/di/FeatureNodeModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.node") +class FeatureNodeModule diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt similarity index 94% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt index bf5b7e4f4..039939871 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt @@ -18,15 +18,16 @@ package org.meshtastic.feature.node.domain.usecase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import org.koin.core.annotation.Single import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.repository.NodeRepository import org.meshtastic.feature.node.list.NodeFilterState import org.meshtastic.feature.node.model.isEffectivelyUnmessageable import org.meshtastic.proto.Config -import javax.inject.Inject -class GetFilteredNodesUseCase @Inject constructor(private val nodeRepository: NodeRepository) { +@Single +class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeRepository) { @Suppress("CyclomaticComplexMethod", "LongMethod") operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow> = nodeRepository .getNodes( diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt similarity index 99% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt index 16614f012..d4e6280da 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import org.koin.core.annotation.Single import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.MeshLog @@ -49,10 +50,9 @@ import org.meshtastic.proto.FirmwareEdition import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.proto.Telemetry -import javax.inject.Inject +@Single class GetNodeDetailsUseCase -@Inject constructor( private val nodeRepository: NodeRepository, private val meshLogRepository: MeshLogRepository, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt similarity index 93% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt index 4af6eaaea..e11721371 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt @@ -17,11 +17,12 @@ package org.meshtastic.feature.node.list import kotlinx.coroutines.flow.map +import org.koin.core.annotation.Single import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.NodeSortOption -import javax.inject.Inject -class NodeFilterPreferences @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { +@Single +class NodeFilterPreferences constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) { val includeUnknown = uiPreferencesDataSource.includeUnknown val excludeInfrastructure = uiPreferencesDataSource.excludeInfrastructure val onlyOnline = uiPreferencesDataSource.onlyOnline diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt similarity index 97% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 38e51602c..d4fe6243b 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -16,11 +16,9 @@ */ package org.meshtastic.feature.node.list -import android.net.Uri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -28,6 +26,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch +import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.RadioController @@ -41,13 +40,9 @@ import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.Config import org.meshtastic.proto.SharedContact -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class NodeListViewModel -@Inject -constructor( +open class NodeListViewModel( private val savedStateHandle: SavedStateHandle, private val nodeRepository: NodeRepository, private val radioConfigRepository: RadioConfigRepository, @@ -138,7 +133,8 @@ constructor( } /** Unified handler for scanned Meshtastic URIs (contacts or channels). */ - fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) { + fun handleScannedUri(uriString: String, onInvalid: () -> Unit) { + val uri = CommonUri.parse(uriString) uri.dispatchMeshtasticUri( onContact = { _sharedContactRequested.value = it }, onChannel = { _requestChannelSet.value = it }, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt similarity index 81% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 29d948898..eda175a62 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -16,17 +16,12 @@ */ package org.meshtastic.feature.node.metrics -import android.app.Application -import android.net.Uri import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Text import androidx.compose.ui.text.AnnotatedString -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import co.touchlab.kermit.Logger -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -40,11 +35,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.nowSeconds -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.database.entity.MeshLog import org.meshtastic.core.di.CoroutineDispatchers @@ -52,7 +44,6 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.util.UnitConversions -import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.ServiceRepository @@ -71,26 +62,15 @@ import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.TimeFrame import org.meshtastic.proto.PortNum import org.meshtastic.proto.Telemetry -import java.io.BufferedWriter -import java.io.FileNotFoundException -import java.io.FileWriter -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Locale -import javax.inject.Inject import org.meshtastic.proto.Paxcount as ProtoPaxcount /** * ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node. */ @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class MetricsViewModel -@Inject -constructor( - savedStateHandle: SavedStateHandle, - private val app: Application, - private val dispatchers: CoroutineDispatchers, +open class MetricsViewModel( + val destNum: Int, + protected val dispatchers: CoroutineDispatchers, private val meshLogRepository: MeshLogRepository, private val serviceRepository: ServiceRepository, private val nodeRepository: NodeRepository, @@ -100,8 +80,8 @@ constructor( private val getNodeDetailsUseCase: GetNodeDetailsUseCase, ) : ViewModel() { - private val nodeIdFromRoute: Int? = - runCatching { savedStateHandle.toRoute().destNum }.getOrNull() + private val nodeIdFromRoute: Int? + get() = destNum private val manualNodeId = MutableStateFlow(null) private val activeNodeId = @@ -134,7 +114,8 @@ constructor( val availableTimeFrames: StateFlow> = combine(state, environmentState) { currentState, envState -> val stateOldest = currentState.oldestTimestampSeconds() - val envOldest = envState.environmentMetrics.minOfOrNull { it.time.toLong() }?.takeIf { it > 0 } + val envOldest = + envState.environmentMetrics.minOfOrNull { it.time.toLong() }?.takeIf { it > 0 } ?: nowSeconds val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds TimeFrame.entries.filter { it.isAvailable(oldest) } } @@ -331,44 +312,10 @@ constructor( Logger.d { "MetricsViewModel cleared" } } - fun savePositionCSV(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\"", - ) - } - } + open fun savePositionCSV(uri: Any) { + // To be implemented in platform-specific subclass } - 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" } - } - } - @Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount") fun decodePaxFromLog(log: MeshLog): ProtoPaxcount? { try { @@ -379,25 +326,26 @@ constructor( val pax = ProtoPaxcount.ADAPTER.decode(decoded.payload) if (pax.ble != 0 || pax.wifi != 0 || pax.uptime != 0) return pax } - } catch (e: IOException) { + } catch (e: Exception) { Logger.e(e) { "Failed to parse Paxcount from binary data" } } try { val base64 = log.raw_message.trim() if (base64.matches(Regex("^[A-Za-z0-9+/=\\r\\n]+$"))) { - val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT) + val bytes = decodeBase64(base64) return ProtoPaxcount.ADAPTER.decode(bytes) } else if (base64.matches(Regex("^[0-9a-fA-F]+$")) && base64.length % 2 == 0) { val bytes = base64.chunked(2).map { it.toInt(16).toByte() }.toByteArray() return ProtoPaxcount.ADAPTER.decode(bytes) } - } catch (e: IllegalArgumentException) { - Logger.e(e) { "Failed to parse Paxcount from decoded data" } - } catch (e: IOException) { - Logger.e(e) { "Failed to parse Paxcount from decoded data" } - } catch (e: NumberFormatException) { + } catch (e: Exception) { Logger.e(e) { "Failed to parse Paxcount from decoded data" } } return null } + + protected open fun decodeBase64(base64: String): ByteArray { + // To be overridden in platform-specific subclass or use KMP library + return ByteArray(0) + } } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/IsEffectivelyUnmessageable.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/LogsType.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/LogsType.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/LogsType.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/LogsType.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/MetricsState.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt similarity index 100% rename from feature/node/src/main/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt rename to feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 5c02a427e..e40e40e91 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -14,57 +14,86 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import com.android.build.api.dsl.LibraryExtension plugins { - alias(libs.plugins.meshtastic.android.library) - alias(libs.plugins.meshtastic.android.library.compose) - alias(libs.plugins.meshtastic.android.library.flavors) - alias(libs.plugins.meshtastic.hilt) + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kotlinx.serialization) + alias(libs.plugins.meshtastic.koin) } -configure { - namespace = "org.meshtastic.feature.settings" - testOptions { unitTests { isIncludeAndroidResources = true } } +kotlin { + android { + namespace = "org.meshtastic.feature.settings" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } + + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.datastore) + implementation(projects.core.domain) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(projects.core.proto) + implementation(projects.core.repository) + implementation(projects.core.service) + implementation(projects.core.resources) + implementation(projects.core.ui) + implementation(projects.core.di) + + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.kermit) + implementation(libs.kotlinx.collections.immutable) + } + + androidMain.dependencies { + implementation(projects.core.barcode) + implementation(projects.core.nfc) + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.accompanist.permissions) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.navigation.common) + implementation(libs.coil) + implementation(libs.markdown.renderer.android) + implementation(libs.markdown.renderer.m3) + implementation(libs.markdown.renderer) + implementation(libs.aboutlibraries.compose.m3) + implementation(libs.nordic.common.core) + implementation(libs.nordic.common.permissions.ble) + + // These were in googleImplementation + implementation(libs.location.services) + implementation(libs.maps.compose) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.mockk) + implementation(libs.robolectric) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.compose.ui.test.junit4) + implementation(libs.androidx.test.ext.junit) + } + } } -dependencies { - implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.datastore) - implementation(projects.core.domain) - implementation(projects.core.model) - implementation(projects.core.navigation) - implementation(projects.core.nfc) - implementation(projects.core.prefs) - implementation(projects.core.proto) - implementation(projects.core.service) - implementation(projects.core.resources) - implementation(projects.core.ui) - implementation(projects.core.barcode) +val marketplaceAttr = Attribute.of("com.android.build.api.attributes.ProductFlavor:marketplace", String::class.java) - implementation(libs.aboutlibraries.compose.m3) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.navigation.compose) - implementation(libs.kotlinx.collections.immutable) - implementation(libs.kermit) - implementation(libs.nordic.common.core) - implementation(libs.nordic.common.permissions.ble) - - testImplementation(libs.junit) - testImplementation(libs.mockk) - testImplementation(libs.robolectric) - testImplementation(libs.turbine) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.androidx.compose.ui.test.junit4) - testImplementation(libs.androidx.test.ext.junit) - - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - androidTestImplementation(libs.androidx.test.ext.junit) +configurations.all { + if (isCanBeResolved && !isCanBeConsumed) { + if (name.contains("android", ignoreCase = true)) { + attributes.attribute(marketplaceAttr, "google") + } + } } diff --git a/feature/settings/detekt-baseline.xml b/feature/settings/detekt-baseline.xml index 21932a978..11b95ac86 100644 --- a/feature/settings/detekt-baseline.xml +++ b/feature/settings/detekt-baseline.xml @@ -2,25 +2,27 @@ - CyclomaticComplexMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - CyclomaticComplexMethod:ExternalNotificationConfigItemList.kt$@Suppress("LongMethod", "TooGenericExceptionCaught") @Composable fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: RadioConfigViewModel = hiltViewModel(), ) - CyclomaticComplexMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) + CyclomaticComplexMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + CyclomaticComplexMethod:ExternalNotificationConfigItemList.kt$@Suppress("LongMethod", "TooGenericExceptionCaught") @Composable fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: RadioConfigViewModel, ) + CyclomaticComplexMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket) - LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) + LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) - LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) + LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$fun setResponseStateLoading(route: Enum<*>) LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket) - LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) - LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) + LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) + LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) MagicNumber:Debug.kt$3 + MagicNumber:DebugViewModel.kt$DebugViewModel$8 MagicNumber:EditChannelDialog.kt$16 MagicNumber:EditChannelDialog.kt$32 MagicNumber:EditDeviceProfileDialog.kt$ProfileField.CHANNEL_URL$3 @@ -31,7 +33,7 @@ ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket) TooGenericExceptionCaught:DebugViewModel.kt$DebugViewModel$e: Exception TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception - TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel + UnusedPrivateProperty:RadioConfigViewModel.kt$RadioConfigViewModel$private val locationRepository: LocationRepository diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AboutScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AboutScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt index 477f1b5b4..d63620ff7 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/AdministrationScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Node @@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.radio.component.ShutdownConfirmationDialo import org.meshtastic.feature.settings.radio.component.WarningDialog @Composable -fun AdministrationScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun AdministrationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() val enabled = state.connected && !state.responseState.isWaiting() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt similarity index 94% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt index 61d551d8e..0c3ec91f7 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/DeviceConfigurationScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.navigation.Route @@ -40,11 +39,7 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute import org.meshtastic.feature.settings.radio.RadioConfigViewModel @Composable -fun DeviceConfigurationScreen( - viewModel: RadioConfigViewModel = hiltViewModel(), - onBack: () -> Unit, - onNavigate: (Route) -> Unit, -) { +fun DeviceConfigurationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onNavigate: (Route) -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt similarity index 95% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt index 788292573..faf2f792e 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.navigation.Route @@ -42,8 +41,8 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel @Composable fun ModuleConfigurationScreen( - viewModel: RadioConfigViewModel = hiltViewModel(), - excludedModulesUnlocked: Boolean = false, + viewModel: RadioConfigViewModel, + excludedModulesUnlocked: Boolean, onBack: () -> Unit, onNavigate: (Route) -> Unit, ) { diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/HomoglyphSetting.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PrivacySection.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt index ea91f78fe..d0328e23c 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt @@ -74,7 +74,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import kotlinx.collections.immutable.toImmutableList @@ -125,7 +124,7 @@ private var redactedKeys: List = listOf("session_passkey", "private_key" @Suppress("LongMethod") @Composable -fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewModel()) { +fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { val listState = rememberLazyListState() val logs by viewModel.meshLog.collectAsStateWithLifecycle() val searchState by viewModel.searchState.collectAsStateWithLifecycle() @@ -194,7 +193,8 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo targetValue = if (!listState.isScrollInProgress) 1.0f else 0f, label = "alpha", ) - DebugSearchStateviewModelDefaults( + DebugSearchStateWithViewModel( + viewModel = viewModel, modifier = Modifier.graphicsLayer(alpha = animatedAlpha), searchState = searchState, filterTexts = filterTexts, diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt index f1db9005b..430c935e9 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/DebugSearch.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.debug_default_search @@ -208,7 +207,8 @@ fun DebugSearchState( } @Composable -fun DebugSearchStateviewModelDefaults( +fun DebugSearchStateWithViewModel( + viewModel: DebugViewModel, modifier: Modifier = Modifier, searchState: SearchState, filterTexts: List, @@ -218,7 +218,6 @@ fun DebugSearchStateviewModelDefaults( onFilterModeChange: (FilterMode) -> Unit, onExportLogs: (() -> Unit)? = null, ) { - val viewModel: DebugViewModel = hiltViewModel() DebugSearchState( modifier = modifier, searchState = searchState, diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt index 0c8737e52..0a6b4d814 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -63,7 +62,7 @@ import org.meshtastic.core.resources.filter_words_summary import org.meshtastic.core.ui.component.MainAppBar @Composable -fun FilterSettingsScreen(viewModel: FilterSettingsViewModel = hiltViewModel(), onBack: () -> Unit) { +fun FilterSettingsScreen(viewModel: FilterSettingsViewModel, onBack: () -> Unit) { val filterEnabled by viewModel.filterEnabled.collectAsStateWithLifecycle() val filterWords by viewModel.filterWords.collectAsStateWithLifecycle() var newWord by remember { mutableStateOf("") } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt similarity index 95% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt index 59b533579..ae0e03a15 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavUtils.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.navigation import org.meshtastic.core.navigation.Route diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt index daa04a79d..b8bf1715a 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Node @@ -55,7 +54,7 @@ import org.meshtastic.core.ui.component.NodeChip * nodes to be deleted updates automatically as filter criteria change. */ @Composable -fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel = hiltViewModel()) { +fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel) { val olderThanDays by viewModel.olderThanDays.collectAsStateWithLifecycle() val onlyUnknownNodes by viewModel.onlyUnknownNodes.collectAsStateWithLifecycle() val nodesToDelete by viewModel.nodesToDelete.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelCard.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelConfigHeader.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/ChannelLegend.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt index fe6efefe9..f3b96fa52 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -40,7 +39,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val ambientLightingConfig = state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig() val formState = rememberConfigState(initialValue = ambientLightingConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt index 9b009352b..c03dd0c3b 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig() val formState = rememberConfigState(initialValue = audioConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt index f05efd1f8..43eaee5dc 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.Config @Composable -fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig() val formState = rememberConfigState(initialValue = bluetoothConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt index e96e00f0a..a53a022ae 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -54,7 +53,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig() val messages = state.cannedMessageMessages diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ConfigState.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt index e6c8d9a17..4f91e4d40 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -51,7 +50,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig @Composable -fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val detectionSensorConfig = state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig() val formState = rememberConfigState(initialValue = detectionSensorConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt index d2151165f..5a13cacd8 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.core.registerReceiver import org.jetbrains.compose.resources.StringResource @@ -155,7 +154,7 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig() val formState = rememberConfigState(initialValue = deviceConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt index a7f05cb6b..1e8e658db 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.Config @Composable -fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val displayConfig = state.radioConfig.display ?: Config.DisplayConfig() val formState = rememberConfigState(initialValue = displayConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt index 00800c844..d5ae5aa33 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import org.jetbrains.compose.resources.stringResource @@ -87,7 +86,7 @@ private const val MAX_RINGTONE_SIZE = 230 fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: RadioConfigViewModel = hiltViewModel(), + viewModel: RadioConfigViewModel, ) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val extNotificationConfig = state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index 47c98eaf8..92c72ff54 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -52,7 +51,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() val destNum = destNode?.num diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt index 4a2944195..ff2e6069a 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig() val formState = rememberConfigState(initialValue = neighborInfoConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt index edb4a4950..b9373c6fe 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.barcode.extractWifiCredentials @@ -91,7 +90,7 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) = MeshtasticDialog(titleRes = Res.string.error, messageRes = Res.string.wifi_qr_code_error, onDismiss = onDismiss) @Composable -fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val networkConfig = state.radioConfig.network ?: Config.NetworkConfig() val formState = rememberConfigState(initialValue = networkConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt index 804ae8f4a..fe9675e6d 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.radio.component import androidx.compose.foundation.layout.Row diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt index b268bbece..68c7322f6 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig @Composable -fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig() val formState = rememberConfigState(initialValue = paxcounterConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt index 7b33f74ac..c0c34b16b 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalFocusManager import androidx.core.location.LocationCompat -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import no.nordicsemi.android.common.permissions.ble.RequireLocation @@ -79,7 +78,7 @@ import org.meshtastic.proto.Config @Composable @Suppress("LongMethod", "CyclomaticComplexMethod") -fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun PositionConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() var phoneLocation: Location? by remember { mutableStateOf(null) } @@ -257,7 +256,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa enabled = state.connected && !isLocationRequiredAndDisabled, onClick = { @SuppressLint("MissingPermission") - coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() } + coroutineScope.launch { + phoneLocation = viewModel.getCurrentLocation() as? android.location.Location + } }, ) { Text(text = stringResource(Res.string.position_config_set_fixed_from_phone)) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt index 6b6b349c1..4184a141e 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -48,7 +47,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.Config @Composable -fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val powerConfig = state.radioConfig.power ?: Config.PowerConfig() val formState = rememberConfigState(initialValue = powerConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt index ea78843d0..1bd6ebeb6 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig @Composable -fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig() val formState = rememberConfigState(initialValue = rangeTestConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt index 1fba75ddb..b245f5561 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -38,7 +37,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig() val formState = rememberConfigState(initialValue = remoteHardwareConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt index 561048393..94627644f 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt @@ -35,7 +35,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import okio.ByteString import okio.ByteString.Companion.toByteString @@ -77,7 +76,7 @@ import java.security.SecureRandom @Composable @Suppress("LongMethod") -fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun SecurityConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val node by viewModel.destNode.collectAsStateWithLifecycle() val securityConfig = state.radioConfig.security ?: Config.SecurityConfig() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt index 779030aad..5cc441c64 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -42,7 +41,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig() val formState = rememberConfigState(initialValue = serialConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt index de0e0b4cc..a81867265 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StatusMessageConfigItemList.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -42,7 +41,7 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel @Composable -fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt index 11a75d37e..4d702c317 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -41,7 +40,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig() val formState = rememberConfigState(initialValue = storeForwardConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt similarity index 95% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 7da9f7b3c..800ef7042 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.getColorFrom @@ -37,7 +36,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.proto.ModuleConfig @Composable -fun TAKConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig() val formState = rememberConfigState(initialValue = takConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt similarity index 98% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt index 2921adccd..04c74876f 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Capabilities @@ -49,7 +48,7 @@ import org.meshtastic.feature.settings.util.toDisplayString import org.meshtastic.proto.ModuleConfig @Composable -fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig() val formState = rememberConfigState(initialValue = telemetryConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt index c05ff42d1..4fea68b9d 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/TrafficManagementConfigItemList.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalFocusManager -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res @@ -51,7 +50,7 @@ import org.meshtastic.proto.ModuleConfig @Suppress("LongMethod") @Composable -fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val tmConfig = state.moduleConfig.traffic_management ?: ModuleConfig.TrafficManagementConfig() val formState = rememberConfigState(initialValue = tmConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt similarity index 97% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt index 55ae3ab75..9599d5f16 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Capabilities @@ -49,7 +48,7 @@ import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel @Composable -fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) { +fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val userConfig = state.userConfig val formState = rememberConfigState(initialValue = userConfig) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/WarningDialog.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt index c56946c1d..ad2444799 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/Formatting.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.util import androidx.compose.runtime.Composable diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt similarity index 66% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt index 2553d2561..64d0295b4 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/LanguageUtils.kt @@ -19,9 +19,7 @@ package org.meshtastic.feature.settings.util import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalResources import androidx.core.os.LocaleListCompat -import co.touchlab.kermit.Logger import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.fr_HT @@ -29,7 +27,6 @@ import org.meshtastic.core.resources.preferences_system_default import org.meshtastic.core.resources.pt_BR import org.meshtastic.core.resources.zh_CN import org.meshtastic.core.resources.zh_TW -import org.xmlpull.v1.XmlPullParser import java.util.Locale object LanguageUtils { @@ -50,32 +47,54 @@ object LanguageUtils { ) } - /** Using locales_config.xml, maps language tags to their localized language names (e.g.: "en" -> "English") */ - @Suppress("CyclomaticComplexMethod") + /** Using a hardcoded list, maps language tags to their localized language names (e.g.: "en" -> "English") */ + @Suppress("CyclomaticComplexMethod", "LongMethod") @Composable fun languageMap(): Map { - val resources = LocalResources.current - val languageTags = - remember(resources) { - buildList { - add(SYSTEM_DEFAULT) - - try { - resources.getXml(org.meshtastic.feature.settings.R.xml.locales_config).use { parser -> - while (parser.eventType != XmlPullParser.END_DOCUMENT) { - if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") { - val languageTag = - parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name") - languageTag?.let { add(it) } - } - parser.next() - } - } - } catch (e: Exception) { - Logger.e { "Error parsing locale_config.xml: ${e.message}" } - } - } - } + val languageTags = remember { + listOf( + SYSTEM_DEFAULT, + "en", + "ar", + "bg", + "ca", + "cs", + "de", + "el", + "es", + "et", + "fi", + "fr", + "ga", + "gl", + "hr", + "ht", + "hu", + "is", + "it", + "iw", + "ja", + "ko", + "lt", + "nl", + "nb", + "pl", + "pt", + "pt-BR", + "ro", + "ru", + "sk", + "sl", + "sq", + "sr", + "srp", + "sv", + "tr", + "uk", + "zh-CN", + "zh-TW", + ) + } return languageTags.associateWith { languageTag -> when (languageTag) { diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt similarity index 95% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt rename to feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt index 779e8b878..66dd171de 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt @@ -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,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.util val gpioPins = (0..48).map { it to "Pin $it" } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt similarity index 79% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index e609b2565..77acc7d98 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -16,12 +16,8 @@ */ package org.meshtastic.feature.settings -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.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -30,10 +26,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import okio.BufferedSink -import okio.buffer -import okio.sink import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase @@ -53,16 +46,9 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig -import java.io.FileNotFoundException -import java.io.FileOutputStream -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class SettingsViewModel -@Inject -constructor( - private val app: android.app.Application, +open class SettingsViewModel( radioConfigRepository: RadioConfigRepository, private val radioController: RadioController, private val nodeRepository: NodeRepository, @@ -163,32 +149,15 @@ constructor( /** * Export all persisted packet data to a CSV file at the given URI. * - * The CSV will include all packets, or only those matching the given port number if specified. Each row contains: - * date, time, sender node number, sender name, sender latitude, sender longitude, receiver latitude, receiver - * longitude, receiver elevation, received SNR, distance, hop limit, and payload. - * * @param uri The destination URI for the CSV file. * @param filterPortnum If provided, only packets with this port number will be exported. */ - @Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod") - fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) { - viewModelScope.launch { - val myNodeNum = myNodeNum ?: return@launch - writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) } - } + open fun saveDataCsv(uri: Any, filterPortnum: Int? = null) { + // To be implemented in platform-specific subclass } - private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) { - withContext(Dispatchers.IO) { - try { - app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> - FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer -> - block.invoke(writer) - } - } - } catch (ex: FileNotFoundException) { - Logger.e { "Can't write file error: ${ex.message}" } - } - } + protected suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) { + val myNodeNum = myNodeNum ?: return + exportDataUseCase(writer, myNodeNum, filterPortnum) } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt similarity index 99% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt index 9a9addff3..09185904c 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt @@ -290,8 +290,3 @@ fun DebugActiveFilters( } } } - -enum class FilterMode { - OR, - AND, -} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt similarity index 86% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index deccdc951..0f4c889d0 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -33,9 +32,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowInstant -import org.meshtastic.core.common.util.toDate -import org.meshtastic.core.common.util.toInstant import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.model.getTracerouteResponse @@ -62,9 +60,6 @@ import org.meshtastic.proto.StoreForwardPlusPlus import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User import org.meshtastic.proto.Waypoint -import java.text.DateFormat -import java.util.Locale -import javax.inject.Inject data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String) @@ -75,6 +70,11 @@ data class SearchState( val hasMatches: Boolean = false, ) +enum class FilterMode { + AND, + OR, +} + // --- Search and Filter Managers --- class LogSearchManager { data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String) @@ -141,24 +141,24 @@ class LogSearchManager { return filteredLogs .flatMapIndexed { logIndex, log -> searchText.split(" ").flatMap { term -> - val escapedTerm = Regex.escape(term) + val escapedTerm = term // Simple regex escape or just use contains val regex = escapedTerm.toRegex(RegexOption.IGNORE_CASE) val messageMatches = - regex.findAll(log.logMessage).map { match -> - SearchMatch(logIndex, match.range.first, match.range.last, "message") + regex.findAll(log.logMessage).map { + SearchMatch(logIndex, it.range.first, it.range.last, "message") } val typeMatches = - regex.findAll(log.messageType).map { match -> - SearchMatch(logIndex, match.range.first, match.range.last, "type") + regex.findAll(log.messageType).map { + SearchMatch(logIndex, it.range.first, it.range.last, "type") } val dateMatches = - regex.findAll(log.formattedReceivedDate).map { match -> - SearchMatch(logIndex, match.range.first, match.range.last, "date") + regex.findAll(log.formattedReceivedDate).map { + SearchMatch(logIndex, it.range.first, it.range.last, "date") } val decodedPayloadMatches = - log.decodedPayload?.let { decoded -> - regex.findAll(decoded).map { match -> - SearchMatch(logIndex, match.range.first, match.range.last, "decodedPayload") + log.decodedPayload?.let { + regex.findAll(it).map { + SearchMatch(logIndex, it.range.first, it.range.last, "decodedPayload") } } ?: emptySequence() messageMatches + typeMatches + dateMatches + decodedPayloadMatches @@ -189,35 +189,30 @@ class LogFilterManager { filterMode: FilterMode, ): List { if (filterTexts.isEmpty()) return logs - return logs.filter { log -> + return logs.filter { logItem -> when (filterMode) { FilterMode.OR -> - filterTexts.any { filterText -> - log.logMessage.contains(filterText, ignoreCase = true) || - log.messageType.contains(filterText, ignoreCase = true) || - log.formattedReceivedDate.contains(filterText, ignoreCase = true) || - (log.decodedPayload?.contains(filterText, ignoreCase = true) == true) + filterTexts.any { + it.contains(logItem.logMessage, ignoreCase = true) || + it.contains(logItem.messageType, ignoreCase = true) || + it.contains(logItem.formattedReceivedDate, ignoreCase = true) || + (logItem.decodedPayload?.contains(it, ignoreCase = true) == true) } FilterMode.AND -> - filterTexts.all { filterText -> - log.logMessage.contains(filterText, ignoreCase = true) || - log.messageType.contains(filterText, ignoreCase = true) || - log.formattedReceivedDate.contains(filterText, ignoreCase = true) || - (log.decodedPayload?.contains(filterText, ignoreCase = true) == true) + filterTexts.all { + it.contains(logItem.logMessage, ignoreCase = true) || + it.contains(logItem.messageType, ignoreCase = true) || + it.contains(logItem.formattedReceivedDate, ignoreCase = true) || + (logItem.decodedPayload?.contains(it, ignoreCase = true) == true) } } } } } -private const val HEX_FORMAT = "%02x" - @Suppress("TooManyFunctions") -@HiltViewModel -class DebugViewModel -@Inject -constructor( +open class DebugViewModel( private val meshLogRepository: MeshLogRepository, private val nodeRepository: NodeRepository, private val meshLogPrefs: MeshLogPrefs, @@ -304,13 +299,13 @@ constructor( } private fun toUiState(databaseLogs: List) = databaseLogs - .map { log -> + .map { UiMeshLog( - uuid = log.uuid, - messageType = log.message_type, - formattedReceivedDate = TIME_FORMAT.format(log.received_date.toInstant().toDate()), - logMessage = annotateMeshLogMessage(log), - decodedPayload = decodePayloadFromMeshLog(log), + uuid = it.uuid, + messageType = it.message_type, + formattedReceivedDate = DateFormatter.formatDateTime(it.received_date), + logMessage = annotateMeshLogMessage(it), + decodedPayload = decodePayloadFromMeshLog(it), ) } .toImmutableList() @@ -387,18 +382,21 @@ constructor( private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean { val nodeIdStr = nodeId.toUInt().toString() // Only match if whitespace before and after - val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""") + val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""", RegexOption.DOT_MATCHES_ALL) regex.find(this)?.let { _ -> - regex.findAll(this).toList().asReversed().forEach { match -> - val idx = match.range.last + 1 - insert(idx, " (${nodeId.asNodeId()})") + regex.findAll(this).toList().asReversed().forEach { + val idx = it.range.last + 1 + insert(idx, " (${nodeId.toHex(8)})") } return true } return false } - private fun Int.asNodeId(): String = "!%08x".format(Locale.getDefault(), this) + protected open fun Int.toHex(length: Int): String { + // Platform specific hex implementation + return "!$this" + } fun requestDeleteAllLogs() { alertManager.showAlert( @@ -419,20 +417,16 @@ constructor( val decodedPayload: String? = null, ) - companion object { - private val TIME_FORMAT = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - } - val presetFilters: List get() = buildList { // Our address if available - nodeRepository.myNodeInfo.value?.myNodeNum?.let { add("!%08x".format(it)) } + nodeRepository.myNodeInfo.value?.myNodeNum?.let { add(it.toHex(8)) } // broadcast add("!ffffffff") // decoded add("decoded") // today (locale-dependent short date format) - add(DateFormat.getDateInstance(DateFormat.SHORT).format(nowInstant.toDate())) + add(DateFormatter.formatShortDate(nowInstant.toEpochMilliseconds())) // Each app name addAll(PortNum.entries.map { it.name }) } @@ -464,7 +458,7 @@ constructor( when (portnumValue) { PortNum.TEXT_MESSAGE_APP.value, PortNum.ALERT_APP.value, - -> payload.toString(Charsets.UTF_8) + -> payload.decodeToString() PortNum.POSITION_APP.value -> Position.ADAPTER.decodeOrNull(payload)?.let { Position.ADAPTER.toReadableString(it) } ?: "Failed to decode Position" @@ -495,17 +489,19 @@ constructor( } ?: "Failed to decode StoreForwardPlusPlus" PortNum.NEIGHBORINFO_APP.value -> decodeNeighborInfo(payload) PortNum.TRACEROUTE_APP.value -> decodeTraceroute(packet, payload) - else -> payload.joinToString(" ") { HEX_FORMAT.format(it) } + else -> payload.joinToString(" ") { it.toHex() } } } catch (e: Exception) { "Failed to decode payload: ${e.message}" } } + protected open fun Byte.toHex(): String = this.toString() + private fun formatNodeWithShortName(nodeNum: Int): String { val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user val shortName = user?.short_name?.takeIf { it.isNotEmpty() } ?: "" - val nodeId = "!%08x".format(nodeNum) + val nodeId = nodeNum.toHex(8) return if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId } @@ -518,8 +514,8 @@ constructor( appendLine(" node_broadcast_interval_secs: ${info.node_broadcast_interval_secs}") if (info.neighbors.isNotEmpty()) { appendLine(" neighbors:") - info.neighbors.forEach { n -> - appendLine(" - node_id: ${formatNodeWithShortName(n.node_id ?: 0)} snr: ${n.snr}") + info.neighbors.forEach { + appendLine(" - node_id: ${formatNodeWithShortName(it.node_id ?: 0)} snr: ${it.snr}") } } } @@ -529,6 +525,6 @@ constructor( val getUsername: (Int) -> String = { nodeNum -> formatNodeWithShortName(nodeNum) } return packet.getTracerouteResponse(getUsername) ?: runCatching { RouteDiscovery.ADAPTER.decode(payload).toString() }.getOrNull() - ?: payload.joinToString(" ") { HEX_FORMAT.format(it) } + ?: payload.joinToString(" ") { it.toHex() } } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt new file mode 100644 index 000000000..cc2d81ce8 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/di/FeatureSettingsModule.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.meshtastic.feature.settings") +class FeatureSettingsModule diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt similarity index 89% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt index e851b4880..ade5e6373 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt @@ -17,21 +17,14 @@ package org.meshtastic.feature.settings.filter import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter -import javax.inject.Inject -@HiltViewModel -class FilterSettingsViewModel -@Inject -constructor( - private val filterPrefs: FilterPrefs, - private val messageFilter: MessageFilter, -) : ViewModel() { +open class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val messageFilter: MessageFilter) : + ViewModel() { private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled.value) val filterEnabled: StateFlow = _filterEnabled.asStateFlow() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ConfigRoute.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/ModuleRoute.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt similarity index 96% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt index 15f1f6d05..2f1f19868 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModel.kt @@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.radio import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -31,7 +30,6 @@ import org.meshtastic.core.resources.are_you_sure import org.meshtastic.core.resources.clean_node_database_confirmation import org.meshtastic.core.resources.clean_now import org.meshtastic.core.ui.util.AlertManager -import javax.inject.Inject private const val MIN_DAYS_THRESHOLD = 7f @@ -39,10 +37,7 @@ private const val MIN_DAYS_THRESHOLD = 7f * ViewModel for [CleanNodeDatabaseScreen]. Manages the state and logic for cleaning the node database based on * specified criteria. The "older than X days" filter is always active. */ -@HiltViewModel -class CleanNodeDatabaseViewModel -@Inject -constructor( +open class CleanNodeDatabaseViewModel( private val cleanNodeDatabaseUseCase: CleanNodeDatabaseUseCase, private val alertManager: AlertManager, ) : ViewModel() { diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt similarity index 78% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 2756e8003..57c947724 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -16,34 +16,20 @@ */ package org.meshtastic.feature.settings.radio -import android.Manifest -import android.app.Application -import android.content.pm.PackageManager -import android.location.Location -import android.net.Uri -import androidx.annotation.RequiresPermission -import androidx.core.content.ContextCompat import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import co.touchlab.kermit.Logger -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okio.buffer -import okio.sink -import okio.source import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase @@ -87,8 +73,6 @@ import org.meshtastic.proto.LocalModuleConfig import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.User -import java.io.FileOutputStream -import javax.inject.Inject /** Data class that represents the current RadioConfig state. */ data class RadioConfigState( @@ -110,12 +94,8 @@ data class RadioConfigState( ) @Suppress("LongParameterList") -@HiltViewModel -class RadioConfigViewModel -@Inject -constructor( +open class RadioConfigViewModel( savedStateHandle: SavedStateHandle, - private val app: Application, private val radioConfigRepository: RadioConfigRepository, private val packetRepository: PacketRepository, private val serviceRepository: ServiceRepository, @@ -126,9 +106,9 @@ constructor( private val homoglyphEncodingPrefs: HomoglyphPrefs, private val toggleAnalyticsUseCase: ToggleAnalyticsUseCase, private val toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase, - private val importProfileUseCase: ImportProfileUseCase, - private val exportProfileUseCase: ExportProfileUseCase, - private val exportSecurityConfigUseCase: ExportSecurityConfigUseCase, + protected val importProfileUseCase: ImportProfileUseCase, + protected val exportProfileUseCase: ExportProfileUseCase, + protected val exportSecurityConfigUseCase: ExportSecurityConfigUseCase, private val installProfileUseCase: InstallProfileUseCase, private val radioConfigUseCase: RadioConfigUseCase, private val adminActionsUseCase: AdminActionsUseCase, @@ -166,15 +146,7 @@ constructor( val currentDeviceProfile get() = _currentDeviceProfile.value - @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) - suspend fun getCurrentLocation(): Location? = if ( - ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED - ) { - locationRepository.getLocations().firstOrNull() - } else { - null - } + open suspend fun getCurrentLocation(): Any? = null init { nodeRepository.nodeDBbyNum @@ -254,13 +226,6 @@ constructor( } } - private fun getOwner(destNum: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getOwner(destNum) - registerRequestId(packetId) - } - } - fun updateChannels(new: List, old: List) { val destNum = destNode.value?.num ?: return getChannelList(new, old).forEach { channel -> @@ -279,13 +244,6 @@ constructor( _radioConfigState.update { it.copy(channelList = new) } } - private fun getChannel(destNum: Int, index: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getChannel(destNum, index) - registerRequestId(packetId) - } - } - fun setConfig(config: Config) { val destNum = destNode.value?.num ?: return viewModelScope.launch { @@ -309,13 +267,6 @@ constructor( } } - private fun getConfig(destNum: Int, configType: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getConfig(destNum, configType) - registerRequestId(packetId) - } - } - @Suppress("CyclomaticComplexMethod") fun setModuleConfig(config: ModuleConfig) { val destNum = destNode.value?.num ?: return @@ -349,76 +300,18 @@ constructor( } } - private fun getModuleConfig(destNum: Int, configType: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getModuleConfig(destNum, configType) - registerRequestId(packetId) - } - } - fun setRingtone(ringtone: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(ringtone = ringtone) } viewModelScope.launch { radioConfigUseCase.setRingtone(destNum, ringtone) } } - private fun getRingtone(destNum: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getRingtone(destNum) - registerRequestId(packetId) - } - } - fun setCannedMessages(messages: String) { val destNum = destNode.value?.num ?: return _radioConfigState.update { it.copy(cannedMessageMessages = messages) } viewModelScope.launch { radioConfigUseCase.setCannedMessages(destNum, messages) } } - private fun getCannedMessages(destNum: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getCannedMessages(destNum) - registerRequestId(packetId) - } - } - - private fun getDeviceConnectionStatus(destNum: Int) { - viewModelScope.launch { - val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum) - registerRequestId(packetId) - } - } - - private fun requestShutdown(destNum: Int) { - viewModelScope.launch { - val packetId = adminActionsUseCase.shutdown(destNum) - registerRequestId(packetId) - } - } - - private fun requestReboot(destNum: Int) { - viewModelScope.launch { - val packetId = adminActionsUseCase.reboot(destNum) - registerRequestId(packetId) - } - } - - private fun requestFactoryReset(destNum: Int) { - viewModelScope.launch { - val isLocal = (destNum == myNodeNum) - val packetId = adminActionsUseCase.factoryReset(destNum, isLocal) - registerRequestId(packetId) - } - } - - private fun requestNodedbReset(destNum: Int, preserveFavorites: Boolean) { - viewModelScope.launch { - val isLocal = (destNum == myNodeNum) - val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal) - registerRequestId(packetId) - } - } - private fun sendAdminRequest(destNum: Int) { val route = radioConfigState.value.route _radioConfigState.update { it.copy(route = "") } // setter (response is PortNum.ROUTING_APP) @@ -426,18 +319,35 @@ constructor( val preserveFavorites = radioConfigState.value.nodeDbResetPreserveFavorites when (route) { - AdminRoute.REBOOT.name -> requestReboot(destNum) + AdminRoute.REBOOT.name -> + viewModelScope.launch { + val packetId = adminActionsUseCase.reboot(destNum) + registerRequestId(packetId) + } AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) { if (metadata?.canShutdown != true) { sendError(Res.string.cant_shutdown) } else { - requestShutdown(destNum) + viewModelScope.launch { + val packetId = adminActionsUseCase.shutdown(destNum) + registerRequestId(packetId) + } } } - AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum) - AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum, preserveFavorites) + AdminRoute.FACTORY_RESET.name -> + viewModelScope.launch { + val isLocal = (destNum == myNodeNum) + val packetId = adminActionsUseCase.factoryReset(destNum, isLocal) + registerRequestId(packetId) + } + AdminRoute.NODEDB_RESET.name -> + viewModelScope.launch { + val isLocal = (destNum == myNodeNum) + val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal) + registerRequestId(packetId) + } } } @@ -451,50 +361,16 @@ constructor( viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) } } - fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) { - try { - app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream -> - importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it } - } - } catch (ex: Exception) { - Logger.e { "Import DeviceProfile error: ${ex.message}" } - sendError(ex.customMessage) - } + open fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) { + // To be implemented in platform-specific subclass } - fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> - FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream -> - exportProfileUseCase(outputStream, profile) - .onSuccess { setResponseStateSuccess() } - .onFailure { throw it } - } - } - } catch (ex: Exception) { - Logger.e { "Can't write file error: ${ex.message}" } - sendError(ex.customMessage) - } - } + open fun exportProfile(uri: Any, profile: DeviceProfile) { + // To be implemented in platform-specific subclass } - fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor -> - FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream -> - exportSecurityConfigUseCase(outputStream, securityConfig) - .onSuccess { setResponseStateSuccess() } - .onFailure { throw it } - } - } - } catch (ex: Exception) { - val errorMessage = "Can't write security keys JSON error: ${ex.message}" - Logger.e { errorMessage } - sendError(ex.customMessage) - } - } + open fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) { + // To be implemented in platform-specific subclass } fun installProfile(protobuf: DeviceProfile) { @@ -513,38 +389,70 @@ constructor( _radioConfigState.update { it.copy(route = route.name, responseState = ResponseState.Loading()) } when (route) { - ConfigRoute.USER -> getOwner(destNum) + ConfigRoute.USER -> + viewModelScope.launch { + val packetId = radioConfigUseCase.getOwner(destNum) + registerRequestId(packetId) + } ConfigRoute.CHANNELS -> { - getChannel(destNum, 0) - getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value) + viewModelScope.launch { + val packetId = radioConfigUseCase.getChannel(destNum, 0) + registerRequestId(packetId) + } + viewModelScope.launch { + val packetId = radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value) + registerRequestId(packetId) + } // channel editor is synchronous, so we don't use requestIds as total setResponseStateTotal(maxChannels + 1) } is AdminRoute -> { - getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) + viewModelScope.launch { + val packetId = + radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) + registerRequestId(packetId) + } setResponseStateTotal(2) } is ConfigRoute -> { if (route == ConfigRoute.LORA) { - getChannel(destNum, 0) + viewModelScope.launch { + val packetId = radioConfigUseCase.getChannel(destNum, 0) + registerRequestId(packetId) + } } if (route == ConfigRoute.NETWORK) { - getDeviceConnectionStatus(destNum) + viewModelScope.launch { + val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum) + registerRequestId(packetId) + } + } + viewModelScope.launch { + val packetId = radioConfigUseCase.getConfig(destNum, route.type) + registerRequestId(packetId) } - getConfig(destNum, route.type) } is ModuleRoute -> { if (route == ModuleRoute.CANNED_MESSAGE) { - getCannedMessages(destNum) + viewModelScope.launch { + val packetId = radioConfigUseCase.getCannedMessages(destNum) + registerRequestId(packetId) + } } if (route == ModuleRoute.EXT_NOTIFICATION) { - getRingtone(destNum) + viewModelScope.launch { + val packetId = radioConfigUseCase.getRingtone(destNum) + registerRequestId(packetId) + } + } + viewModelScope.launch { + val packetId = radioConfigUseCase.getModuleConfig(destNum, route.type) + registerRequestId(packetId) } - getModuleConfig(destNum, route.type) } } } @@ -565,7 +473,7 @@ constructor( } } - private fun setResponseStateSuccess() { + protected fun setResponseStateSuccess() { _radioConfigState.update { state -> if (state.responseState is ResponseState.Loading) { state.copy(responseState = ResponseState.Success(true)) @@ -575,14 +483,11 @@ constructor( } } - private val Exception.customMessage: String - get() = "${javaClass.simpleName}: $message" + protected fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error)) - private fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error)) + protected fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id)) - private fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id)) - - private fun sendError(error: UiText) = setResponseStateError(error) + protected fun sendError(error: UiText) = setResponseStateError(error) private fun setResponseStateError(error: UiText) { _radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) } @@ -658,7 +563,10 @@ constructor( val index = response.index if (index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) { // Not done yet, request next channel - getChannel(destNum, index + 1) + viewModelScope.launch { + val packetId = radioConfigUseCase.getChannel(destNum, index + 1) + registerRequestId(packetId) + } } } else { // Received last channel, update total and start channel editor diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt similarity index 100% rename from feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/ResponseState.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be9d0241a..ed5394fdb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ accompanist = "0.37.3" # androidx androidxComposeMaterial3Adaptive = "1.2.0" -androidxHilt = "1.3.0" androidxTracing = "1.10.4" datastore = "1.2.0" glance = "1.2.0-rc01" @@ -16,6 +15,9 @@ navigation3 = "1.0.1" paging = "3.4.1" room = "2.8.4" savedstate = "1.4.0" +koin = "4.2.0-RC1" +koin-annotations = "2.1.0" +koin-plugin = "0.3.0" # Kotlin kotlin = "2.3.10" @@ -32,7 +34,6 @@ turbine = "1.2.1" compose-multiplatform = "1.11.0-alpha03" # Google -hilt = "2.59.2" maps-compose = "8.2.0" # ML Kit @@ -83,10 +84,6 @@ androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", versi androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" } androidx-glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" } androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } -androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHilt" } -androidx-hilt-lifecycle-viewmodel-compose = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidxHilt" } -androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidxHilt" } -androidx-hilt-common = { module = "androidx.hilt:hilt-common", version.ref = "androidxHilt" } androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" } @@ -139,11 +136,14 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" } firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.10.0" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } guava = { module = "com.google.guava:guava", version = "33.5.0-jre" } -hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } -hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } -hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } -hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } location-services = { module = "com.google.android.gms:play-services-location", version = "21.3.0" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } +koin-androidx-workmanager = { module = "io.insert-koin:koin-androidx-workmanager", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin" } maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "maps-compose" } maps-compose-widgets = { module = "com.google.maps.android:maps-compose-widgets", version.ref = "maps-compose" } @@ -243,7 +243,7 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" } google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version = "4.4.4" } -hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } +koin-gradlePlugin = { module = "io.insert-koin.compiler.plugin:io.insert-koin.compiler.plugin.gradle.plugin", version.ref = "koin-plugin" } kover-gradlePlugin = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" } ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" } secrets-gradlePlugin = {module = "com.google.android.secrets-gradle-plugin:com.google.android.secrets-gradle-plugin.gradle.plugin", version = "1.1.0"} @@ -259,6 +259,7 @@ android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform. # Jetbrains compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } +koin-compiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -269,7 +270,6 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } # Google devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" } google-services = { id = "com.google.gms.google-services", version = "4.4.4" } -hilt = { id = "com.google.dagger.hilt.android" , version.ref = "hilt" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" } # Firebase @@ -300,7 +300,7 @@ meshtastic-android-lint = { id = "meshtastic.android.lint" } meshtastic-android-room = { id = "meshtastic.android.room" } meshtastic-android-test = { id = "meshtastic.android.test" } meshtastic-detekt = { id = "meshtastic.detekt" } -meshtastic-hilt = { id = "meshtastic.hilt" } +meshtastic-koin = { id = "meshtastic.koin" } meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" } meshtastic-kmp-library = { id = "meshtastic.kmp.library" } meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" }