diff --git a/AGENTS.md b/AGENTS.md index 882c6c1f7..a7ea32e79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,140 +1,61 @@ # Meshtastic Android - Agent Guide -This file serves as a comprehensive guide for AI agents and developers working on the `Meshtastic-Android` codebase. Use this as your primary reference for understanding the architecture, conventions, and workflows. +This file serves as a comprehensive guide for AI agents and developers working on the `Meshtastic-Android` codebase. Use this as your primary reference for understanding the architecture, conventions, and strict rules of this project. -## 1. Project Overview - -- **Type:** Native Android Application (Kotlin). -- **Purpose:** Client interface for Meshtastic mesh radios. -- **Architecture:** Modern Android Development (MAD) principles. - - **UI:** Jetpack Compose (Material 3). - - **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow. - - **Dependency Injection:** Hilt. - - **Navigation:** Type-Safe Navigation (Jetpack Navigation). - - **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms). +## 1. Project Vision +We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (KMP)** architecture. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience. ## 2. Codebase Map | Directory | Description | | :--- | :--- | -| `app/` | Main application module. Contains `MainActivity`, `AppNavigation`, and Hilt entry points. Uses package `com.geeksville.mesh`. | -| `core/` | Shared library modules. Most code here uses package `org.meshtastic.core.*`. | -| `core/ble/` | **New:** Bluetooth Low Energy stack using Nordic libraries. | -| `core/strings/` | **Crucial:** Centralized string resources using Compose Multiplatform Resources. | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`). Each is a standalone Gradle module. Uses package `org.meshtastic.feature.*`. | -| `build-logic/` | Custom Gradle convention plugins. Defines build logic for the entire project. | -| `gradle/libs.versions.toml` | **Version Catalog.** All dependencies and versions are defined here. | -| `core/proto/` | Protobuf definitions for communicating with the mesh radio. | +| `app/` | Main application module. Contains `MainActivity`, Hilt 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. | +| `core:database` | Room KMP database implementation. | +| `core:datastore` | Multiplatform DataStore for preferences. | +| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). | +| `core:domain` | Pure KMP business logic and UseCases. | +| `core:data` | Core manager implementations and data orchestration. | +| `core:network` | KMP networking layer using Ktor and MQTT abstractions. | +| `core:di` | Common DI qualifiers and dispatchers. | +| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | +| `core/resources/` | Centralized string and image resources (Compose Multiplatform). | +| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`). | ## 3. Development Guidelines ### A. UI Development (Jetpack Compose) -- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate. +- **Material 3:** The app uses Material 3. - **Strings:** - - Do **not** use `app/src/main/res/values/strings.xml` for UI strings. - - Use the **Compose Multiplatform Resource** library in `core:resources`. - - **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`. - - **Usage:** - ```kotlin - import org.jetbrains.compose.resources.stringResource - import org.meshtastic.core.resources.Res - import org.meshtastic.core.resources.your_string_key + - **Rule:** MUST use the **Compose Multiplatform Resource** library in `core:resources`. + - **Location:** `core/resources/src/commonMain/composeResources/values/strings.xml`. +- **Dialogs:** Use centralized components in `core:ui`. - Text(text = stringResource(Res.string.your_string_key)) - ``` -- **Dialogs:** - - Use the centralized `MeshtasticDialog` for all alerts and confirmation boxes. - - **Specialized Overloads:** Use `MeshtasticResourceDialog` (for resource-only content) or `MeshtasticTextDialog` (for mixed resource/text content) to reduce boilerplate. - - **Location:** Defined in `core/ui/src/main/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt`. -- **Previews:** Create `@Preview` functions for your Composables to ensure they render correctly. +### B. Logic & Data Layer +- **KMP Focus:** All business logic must reside in `commonMain` of the respective `core` module. +- **I/O:** Use **Okio** (`BufferedSource`/`BufferedSink`) for stream operations. Never use `java.io` in `commonMain`. +- **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. -### B. Architecture & State -- **ViewModels:** Must be annotated with `@HiltViewModel`. -- **Injection:** Use `@Inject constructor(...)`. -- **Scopes:** Use `viewModelScope` for coroutines. Avoid `GlobalScope`. -- **Data Flow:** Expose UI state as `StateFlow` or `Flow`. +### C. Namespacing +- **Standard:** Use the `org.meshtastic.*` namespace for all code. +- **Legacy:** Maintain the `com.geeksville.mesh` Application ID and specific intent strings for backward compatibility. -### C. Navigation -- The project uses **Type-Safe Navigation** (Kotlin Serialization). -- Routes are defined in `core/navigation` (e.g., `ContactsRoutes`, `SettingsRoutes`). -- The main `NavHost` is located in `app/src/main/java/com/geeksville/mesh/ui/Main.kt`. +## 4. Execution Protocol -### D. Bluetooth (BLE) -- **Library:** Uses **Nordic Semiconductor's Kotlin BLE Library** and **Android Common Libraries**. -- **Location:** Core logic resides in `core/ble`. -- **Key Classes:** `BluetoothRepository`, `NordicBleInterface`, `BleConnection`. -- **Usage:** Use `BluetoothRepository` for scanning and bonding. Use `BleConnection` for managing connections. Avoid legacy `BluetoothAdapter` APIs directly. -- **Environment Mocking:** Use `LocalEnvironmentOwner` and `MockAndroidEnvironment` to test UI hardware reactions without a real device. +### A. Build and Verify +1. **Format:** `./gradlew spotlessApply` +2. **Lint:** `./gradlew detekt` +3. **Test:** `./gradlew testAndroid` (or `testCommonMain` for pure logic) -### E. Dependency Management -- **Never** hardcode versions in `build.gradle.kts` files. -- **Action:** Add the library and version to `gradle/libs.versions.toml`. -- **Action:** Apply plugins using the alias from the catalog (e.g., `alias(libs.plugins.meshtastic.android.library)`). -- **Alpha Libraries:** Do not be shy about using alpha libraries from Google if they provide necessary features. +### B. Expect/Actual Patterns +Use `expect`/`actual` sparingly for platform-specific types (e.g., `Location`, `NavHostController`) to keep the core logic pure and platform-agnostic. -### F. Build Variants (Flavors) -- **`google`**: Includes Google Play Services (Maps, Firebase, Crashlytics). -- **`fdroid`**: FOSS version. **Strictly segregate sensitive data** (Crashlytics, Firebase, etc.) out of this flavor. -- **Task Example:** `./gradlew assembleFdroidDebug` - -### G. Kotlin Multiplatform (KMP) & Decoupling -- **Goal:** We are actively moving logic and models from Android-specific modules to KMP modules (`core:common`, `core:model`, `core:proto`) to support future cross-platform expansion. -- **Domain Models:** Always place domain models (Data Classes, Enums) in `commonMain` of the respective module. -- **Parceling:** - - Use the platform-agnostic `CommonParcelable` and `CommonParcelize` from `core:common`. - - Avoid direct imports of `android.os.Parcelable` or `kotlinx.parcelize.Parcelize` in `commonMain`. -- **Platform Abstractions:** Use `expect`/`actual` for platform-specific logic (e.g., `DateFormatter`, `RandomUtils`, `BuildUtils`). -- **AIDL Compatibility:** AIDL parcelable declarations for models moved to `commonMain` should be relocated to `:core:api` to ensure proper export to consumer modules. - -## 4. Quality Assurance - -### A. Code Style (Spotless) -- The project uses **Spotless** to enforce formatting. -- **Command:** `./gradlew spotlessApply` -- **Rule:** You **must** run this before submitting any code. - -### B. Linting (Detekt) -- The project uses **Detekt** for static analysis. -- **Command:** `./gradlew detekt` -- **Rule:** Ensure zero regressions. - -### C. Testing -- **Unit Tests:** JUnit 4/5 in `src/test/java`. Run with `./gradlew test`. -- **Compose UI Tests (JVM):** Preferred for component testing. Use **Robolectric** in `src/test/java`. - - **Important:** Annotate with `@Config(sdk = [34])` if using Java 17 to avoid SDK 35 compatibility issues. - - **Best Practice:** Pass mocked ViewModels to Composables instead of using Hilt in Robolectric tests. -- **Instrumented Tests:** For full E2E or Hilt integration tests, use `src/androidTest/java`. Run with `./gradlew connectedAndroidTest`. -- **Feature Test:** `./gradlew feature:settings:testGoogleDebug` - -## 5. Agent Workflow - -1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment. -2. **Plan:** Identify which modules (`core` or `feature`) need modification. -3. **Implement:** - - If adding a string, modify `core:resources`. - - If adding a dependency, modify `libs.versions.toml` first. -4. **Verify:** - - Run `./gradlew spotlessApply` (Essential!). - - Run `./gradlew detekt`. - - Run relevant tests (e.g., `./gradlew :feature:settings:testDebugUnitTest`). - -## 6. Important Context - -- **Protobuf:** Communication with the device uses Protobufs. The definitions are in `core/proto`. This is a Git submodule, but the build system handles it. -- **Legacy:** Some code in `app/` uses the `com.geeksville.mesh` package. Newer code in `core/` and `feature/` uses `org.meshtastic.*`. Respect the existing package structure of the file you are editing. -- **Versioning:** Do not manually edit `versionCode` or `versionName`. These are managed by the build system and CI/CD. -- **Database Safety:** When modifying critical database logic (e.g., `NodeInfoDao`), always ensure you have explicit test coverage for security edge cases (like PKC conflicts or key wiping). Refer to `core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt` for examples. - -## 7. Troubleshooting - -- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.resources.Res` and the specific string property, and that you have run a build to generate the resources. -- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly. - ---- -*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.* - -### E. Resources and Assets -- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`. -- **Module Path:** `core/resources/src/commonMain/composeResources/` -- **Decentralization:** Feature-specific strings and assets can (and should) be housed in their respective feature module's `composeResources` directory to maintain modular boundaries and clean architectural dependency graphs. Crowdin localization handles globbing `/**/composeResources/values/strings.xml` perfectly. -- **Drawables:** Use `painterResource(Res.drawable.your_icon)` to access cross-platform drawables. Name them consistently (`ic_` for icons, `img_` for artwork). Avoid putting standard Drawables or Vectors in legacy Android `res/drawable` folders unless strictly required by a legacy library (like `OsmDroid` map markers) or the OS layer (like `app_icon.xml`). +## 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. diff --git a/README.md b/README.md index 9eed8d9ae..cab5bb9b0 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,16 @@ You can generate the documentation locally to preview your changes. ## Architecture ### Modern Android Development (MAD) -The app follows modern Android development practices: -- **UI:** Jetpack Compose (Material 3). +The app follows modern Android development practices, built on top of a shared Kotlin Multiplatform (KMP) Core: +- **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. +- **Dependency Injection:** Hilt (mapped to KMP `javax.inject` interfaces). - **Navigation:** Type-Safe Navigation (Jetpack Navigation). -- **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms). +- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms). ### Bluetooth Low Energy (BLE) -The BLE stack has been modernized to use **Nordic Semiconductor's Android Common Libraries** and **Kotlin BLE Library**. This provides a robust, Coroutine-based architecture for reliable device communication. See [core/ble/README.md](core/ble/README.md) for details. +The BLE stack uses a hybrid interface-driven architecture. Platform-agnostic interfaces live in `commonMain`, while the Android implementation utilizes **Nordic Semiconductor's Kotlin BLE Library**. This provides a robust, Coroutine-based architecture for reliable device communication while remaining KMP compatible. See [core/ble/README.md](core/ble/README.md) for details. ## Translations diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e0d08bbf7..0f427214e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -262,9 +262,11 @@ dependencies { 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.accompanist.permissions) implementation(libs.kermit) + implementation(libs.kotlinx.datetime) implementation(libs.nordic.client.android) implementation(libs.nordic.common.core) @@ -278,6 +280,9 @@ dependencies { googleImplementation(libs.location.services) googleImplementation(libs.play.services.maps) + googleImplementation(libs.maps.compose) + googleImplementation(libs.maps.compose.utils) + googleImplementation(libs.maps.compose.widgets) googleImplementation(libs.dd.sdk.android.okhttp) googleImplementation(libs.dd.sdk.android.compose) googleImplementation(libs.dd.sdk.android.logs) @@ -291,6 +296,7 @@ dependencies { fdroidImplementation(libs.osmdroid.android) fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") } + fdroidImplementation(libs.osmbonuspack) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.ext.junit) @@ -300,6 +306,7 @@ dependencies { androidTestImplementation(libs.nordic.client.android.mock) androidTestImplementation(libs.nordic.core.mock) + testImplementation(libs.androidx.work.testing) testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.kotlinx.coroutines.test) diff --git a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/MarkerClusterer.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java similarity index 98% rename from feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/MarkerClusterer.java rename to app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java index b6c5601c6..38e51da52 100644 --- a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/MarkerClusterer.java +++ b/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java @@ -15,14 +15,14 @@ * along with this program. If not, see . */ -package org.meshtastic.feature.map.cluster; +package org.meshtastic.app.map.cluster; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; import android.view.MotionEvent; -import org.meshtastic.feature.map.model.MarkerWithLabel; +import org.meshtastic.app.map.model.MarkerWithLabel; import org.osmdroid.util.BoundingBox; import org.osmdroid.views.MapView; diff --git a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/RadiusMarkerClusterer.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java similarity index 98% rename from feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/RadiusMarkerClusterer.java rename to app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java index 655e9d7b9..e2710352a 100644 --- a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/RadiusMarkerClusterer.java +++ b/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.meshtastic.feature.map.cluster; +package org.meshtastic.app.map.cluster; import android.content.Context; import android.graphics.Bitmap; @@ -27,7 +27,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.MotionEvent; -import org.meshtastic.feature.map.model.MarkerWithLabel; +import org.meshtastic.app.map.model.MarkerWithLabel; import org.osmdroid.bonuspack.R; import org.osmdroid.util.BoundingBox; diff --git a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/StaticCluster.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java similarity index 95% rename from feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/StaticCluster.java rename to app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java index b49a33f11..324a34b52 100644 --- a/feature/map/src/fdroid/java/org/meshtastic/feature/map/cluster/StaticCluster.java +++ b/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -package org.meshtastic.feature.map.cluster; +package org.meshtastic.app.map.cluster; -import org.meshtastic.feature.map.model.MarkerWithLabel; +import org.meshtastic.app.map.model.MarkerWithLabel; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; diff --git a/feature/intro/src/fdroid/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt b/app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt similarity index 91% rename from feature/intro/src/fdroid/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt index def21ab01..a9065a24a 100644 --- a/feature/intro/src/fdroid/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.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,8 +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.intro +package org.meshtastic.app.intro import androidx.compose.runtime.Composable diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt new file mode 100644 index 000000000..ba3300a99 --- /dev/null +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import org.meshtastic.core.ui.util.MapViewProvider + +class FdroidMapViewProvider : MapViewProvider { + @Composable + override fun MapView( + modifier: Modifier, + viewModel: Any, + navigateToNodeDetails: (Int) -> Unit, + focusedNodeNum: Int?, + nodeTracks: List?, + tracerouteOverlay: Any?, + tracerouteNodePositions: Map, + onTracerouteMappableCountChanged: (Int, Int) -> Unit, + ) { + val mapViewModel: MapViewModel = hiltViewModel() + org.meshtastic.app.map.MapView( + modifier = modifier, + mapViewModel = mapViewModel, + navigateToNodeDetails = navigateToNodeDetails, + focusedNodeNum = focusedNodeNum, + nodeTracks = nodeTracks as? List, + tracerouteOverlay = tracerouteOverlay as? org.meshtastic.feature.map.model.TracerouteOverlay, + tracerouteNodePositions = tracerouteNodePositions as? Map ?: emptyMap(), + onTracerouteMappableCountChanged = onTracerouteMappableCountChanged, + ) + } +} diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt new file mode 100644 index 000000000..48b1aa7fc --- /dev/null +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.map + +import org.meshtastic.core.ui.util.MapViewProvider + +fun getMapViewProvider(): MapViewProvider = FdroidMapViewProvider() diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapUtils.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt similarity index 97% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapUtils.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt index 6bad64d44..1243fdc8a 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapUtils.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.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,8 +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.map +package org.meshtastic.app.map import android.content.Context import android.util.TypedValue diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt index d43d69440..8fa664f80 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.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.map +package org.meshtastic.app.map import android.Manifest import android.graphics.Paint @@ -83,6 +83,14 @@ 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.meshtastic.app.R +import org.meshtastic.app.map.cluster.RadiusMarkerClusterer +import org.meshtastic.app.map.component.CacheLayout +import org.meshtastic.app.map.component.DownloadButton +import org.meshtastic.app.map.component.EditWaypointDialog +import org.meshtastic.app.map.component.MapButton +import org.meshtastic.app.map.model.CustomTileSource +import org.meshtastic.app.map.model.MarkerWithLabel import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.common.util.nowMillis @@ -131,14 +139,9 @@ import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.theme.TracerouteColors import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.showToast -import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer -import org.meshtastic.feature.map.component.CacheLayout -import org.meshtastic.feature.map.component.DownloadButton -import org.meshtastic.feature.map.component.EditWaypointDialog -import org.meshtastic.feature.map.component.MapButton -import org.meshtastic.feature.map.model.CustomTileSource -import org.meshtastic.feature.map.model.MarkerWithLabel +import org.meshtastic.feature.map.LastHeardFilter import org.meshtastic.feature.map.model.TracerouteOverlay +import org.meshtastic.feature.map.tracerouteNodeSelection import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits import org.meshtastic.proto.Position import org.meshtastic.proto.Waypoint diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewExtensions.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewExtensions.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt index 52ae76c25..a5f27e8e9 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewExtensions.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.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.map +package org.meshtastic.app.map import android.graphics.Color import android.graphics.DashPathEffect @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat +import org.meshtastic.app.R import org.meshtastic.proto.Position import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt similarity index 96% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt index 1f3d5c21c..36b575d6a 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.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.map +package org.meshtastic.app.map import androidx.lifecycle.SavedStateHandle import androidx.navigation.toRoute @@ -31,6 +31,7 @@ import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository 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 diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt index a32e49a0a..d6e84d19b 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewWithLifecycle.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.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,8 +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.map +package org.meshtastic.app.map import android.annotation.SuppressLint import android.content.Context diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt similarity index 99% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt index 7038177d6..112449d1f 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/SqlTileWriterExt.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.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.map +package org.meshtastic.app.map import android.database.Cursor import org.meshtastic.core.common.util.nowMillis diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/CacheLayout.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/CacheLayout.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt index ac8219d81..986918e06 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/CacheLayout.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/DownloadButton.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/DownloadButton.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt index 671626241..7b12f70b9 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/DownloadButton.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.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.map.component +package org.meshtastic.app.map.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt index 0dc57bd4c..83dc24880 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.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.map.component +package org.meshtastic.app.map.component import android.app.DatePickerDialog import android.widget.DatePicker @@ -66,10 +66,8 @@ import kotlinx.datetime.Month import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.common.util.nowInstant import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.common.util.systemTimeZone -import org.meshtastic.core.common.util.toDate import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.date @@ -121,7 +119,7 @@ fun EditWaypointDialog( if (expire != 0 && expire != Int.MAX_VALUE) { Instant.fromEpochSeconds(expire.toLong()) } else { - nowInstant + 8.hours + kotlinx.datetime.Clock.System.now() + 8.hours } } @@ -130,7 +128,7 @@ fun EditWaypointDialog( remember(currentInstant) { mutableStateOf( if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { - dateFormat.format(currentInstant.toDate()) + dateFormat.format(java.util.Date(currentInstant.toEpochMilliseconds())) } else { "" }, @@ -140,7 +138,7 @@ fun EditWaypointDialog( remember(currentInstant) { mutableStateOf( if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) { - timeFormat.format(currentInstant.toDate()) + timeFormat.format(java.util.Date(currentInstant.toEpochMilliseconds())) } else { "" }, diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/MapButton.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/MapButton.kt similarity index 98% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/MapButton.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/component/MapButton.kt index b6a368b41..5bffb830d 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/MapButton.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/component/MapButton.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt similarity index 99% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt index 6225471fb..de0f8c6c2 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.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,8 +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.map.model +package org.meshtastic.app.map.model import org.osmdroid.tileprovider.tilesource.ITileSource import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/MarkerWithLabel.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt similarity index 96% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/MarkerWithLabel.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt index 32ff692a2..da94a7725 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/MarkerWithLabel.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.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,16 +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.map.model +package org.meshtastic.app.map.model import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF import android.view.MotionEvent -import org.meshtastic.feature.map.dpToPx -import org.meshtastic.feature.map.spToPx +import org.meshtastic.app.map.dpToPx +import org.meshtastic.app.map.spToPx import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polygon diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/NOAAWmsTileSource.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt similarity index 99% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/NOAAWmsTileSource.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt index 16391721e..bab1171d8 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/NOAAWmsTileSource.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.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.map.model +package org.meshtastic.app.map.model import android.content.res.Resources import co.touchlab.kermit.Logger diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/OnlineTileSourceAuth.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt similarity index 96% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/OnlineTileSourceAuth.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt index 4ed0f43dc..3d51133bd 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/model/OnlineTileSourceAuth.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.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,8 +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.map.model +package org.meshtastic.app.map.model import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase import org.osmdroid.tileprovider.tilesource.TileSourcePolicy diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt similarity index 83% rename from feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt rename to app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt index 7455a03b6..5cdbbdcbd 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.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.map.node +package org.meshtastic.app.map.node import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -24,11 +24,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.meshtastic.feature.map.addCopyright -import org.meshtastic.feature.map.addPolyline -import org.meshtastic.feature.map.addPositionMarkers -import org.meshtastic.feature.map.addScaleBarOverlay -import org.meshtastic.feature.map.rememberMapViewWithLifecycle +import org.meshtastic.app.map.addCopyright +import org.meshtastic.app.map.addPolyline +import org.meshtastic.app.map.addPositionMarkers +import org.meshtastic.app.map.addScaleBarOverlay +import org.meshtastic.app.map.model.CustomTileSource +import org.meshtastic.app.map.rememberMapViewWithLifecycle import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint @@ -44,7 +45,7 @@ fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) rememberMapViewWithLifecycle( applicationId = nodeMapViewModel.applicationId, box = cameraView, - tileSource = nodeMapViewModel.tileSource, + tileSource = CustomTileSource.getTileSource(nodeMapViewModel.mapStyleId), ) AndroidView( diff --git a/feature/map/src/fdroid/res/drawable/ic_location_on.xml b/app/src/fdroid/res/drawable/ic_location_on.xml similarity index 100% rename from feature/map/src/fdroid/res/drawable/ic_location_on.xml rename to app/src/fdroid/res/drawable/ic_location_on.xml diff --git a/feature/map/src/fdroid/res/drawable/ic_map_location_dot.xml b/app/src/fdroid/res/drawable/ic_map_location_dot.xml similarity index 100% rename from feature/map/src/fdroid/res/drawable/ic_map_location_dot.xml rename to app/src/fdroid/res/drawable/ic_map_location_dot.xml diff --git a/feature/map/src/fdroid/res/drawable/ic_map_navigation.xml b/app/src/fdroid/res/drawable/ic_map_navigation.xml similarity index 100% rename from feature/map/src/fdroid/res/drawable/ic_map_navigation.xml rename to app/src/fdroid/res/drawable/ic_map_navigation.xml diff --git a/feature/map/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml similarity index 100% rename from feature/map/src/google/AndroidManifest.xml rename to app/src/google/AndroidManifest.xml diff --git a/feature/intro/src/google/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt b/app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt similarity index 99% rename from feature/intro/src/google/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt rename to app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt index 459ca9d82..fdad2c363 100644 --- a/feature/intro/src/google/kotlin/org/meshtastic/feature/intro/AnalyticsIntro.kt +++ b/app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.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.intro +package org.meshtastic.app.intro import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt new file mode 100644 index 000000000..8a441fa70 --- /dev/null +++ b/app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.map + +import org.meshtastic.core.ui.util.MapViewProvider + +fun getMapViewProvider(): MapViewProvider = GoogleMapViewProvider() diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt new file mode 100644 index 000000000..63a7cd8a3 --- /dev/null +++ b/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import org.meshtastic.core.ui.util.MapViewProvider + +class GoogleMapViewProvider : MapViewProvider { + @Composable + override fun MapView( + modifier: Modifier, + viewModel: Any, + navigateToNodeDetails: (Int) -> Unit, + focusedNodeNum: Int?, + nodeTracks: List?, + tracerouteOverlay: Any?, + tracerouteNodePositions: Map, + onTracerouteMappableCountChanged: (Int, Int) -> Unit, + ) { + val mapViewModel: MapViewModel = hiltViewModel() + org.meshtastic.app.map.MapView( + modifier = modifier, + mapViewModel = mapViewModel, + navigateToNodeDetails = navigateToNodeDetails, + focusedNodeNum = focusedNodeNum, + nodeTracks = nodeTracks as? List, + tracerouteOverlay = tracerouteOverlay as? org.meshtastic.feature.map.model.TracerouteOverlay, + tracerouteNodePositions = tracerouteNodePositions as? Map ?: emptyMap(), + onTracerouteMappableCountChanged = onTracerouteMappableCountChanged, + ) + } +} diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/LocationHandler.kt b/app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/LocationHandler.kt rename to app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt index ac4d632ed..1aa4a7bab 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/LocationHandler.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.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,8 +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.map +package org.meshtastic.app.map import android.Manifest import android.app.Activity diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MBTilesProvider.kt b/app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/MBTilesProvider.kt rename to app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt index 848779ccf..6ac756f6b 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MBTilesProvider.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.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.map +package org.meshtastic.app.map import android.database.sqlite.SQLiteDatabase import com.google.android.gms.maps.model.Tile diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt rename to app/src/google/kotlin/org/meshtastic/app/map/MapView.kt index 4820f5136..d9f12aac0 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -16,7 +16,7 @@ */ @file:Suppress("MagicNumber") -package org.meshtastic.feature.map +package org.meshtastic.app.map import android.Manifest import android.app.Activity @@ -95,6 +95,14 @@ import com.google.maps.android.data.kml.KmlLayer import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.json.JSONObject +import org.meshtastic.app.map.component.ClusterItemsListDialog +import org.meshtastic.app.map.component.CustomMapLayersSheet +import org.meshtastic.app.map.component.CustomTileProviderManagerSheet +import org.meshtastic.app.map.component.EditWaypointDialog +import org.meshtastic.app.map.component.MapControlsOverlay +import org.meshtastic.app.map.component.NodeClusterMarkers +import org.meshtastic.app.map.component.WaypointMarkers +import org.meshtastic.app.map.model.NodeClusterItem import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.Node @@ -116,15 +124,9 @@ import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.theme.TracerouteColors import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.formatPositionTime -import org.meshtastic.feature.map.component.ClusterItemsListDialog -import org.meshtastic.feature.map.component.CustomMapLayersSheet -import org.meshtastic.feature.map.component.CustomTileProviderManagerSheet -import org.meshtastic.feature.map.component.EditWaypointDialog -import org.meshtastic.feature.map.component.MapControlsOverlay -import org.meshtastic.feature.map.component.NodeClusterMarkers -import org.meshtastic.feature.map.component.WaypointMarkers -import org.meshtastic.feature.map.model.NodeClusterItem +import org.meshtastic.feature.map.LastHeardFilter import org.meshtastic.feature.map.model.TracerouteOverlay +import org.meshtastic.feature.map.tracerouteNodeSelection import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits import org.meshtastic.proto.Position import org.meshtastic.proto.Waypoint diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt similarity index 99% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt rename to app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt index d638a2f9d..9a501b96c 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.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.map +package org.meshtastic.app.map import android.app.Application import android.net.Uri @@ -43,6 +43,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable +import org.meshtastic.app.map.model.CustomTileProviderConfig +import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs +import org.meshtastic.app.map.repository.CustomTileProviderRepository import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.RadioController import org.meshtastic.core.navigation.MapRoutes @@ -51,9 +54,7 @@ import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed -import org.meshtastic.feature.map.model.CustomTileProviderConfig -import org.meshtastic.feature.map.prefs.map.GoogleMapsPrefs -import org.meshtastic.feature.map.repository.CustomTileProviderRepository +import org.meshtastic.feature.map.BaseMapViewModel import org.meshtastic.proto.Config import java.io.File import java.io.FileOutputStream diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt similarity index 96% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt index 6a03e663d..5c5e325ac 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/ClusterItemsListDialog.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues @@ -30,11 +30,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.model.NodeClusterItem import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.nodes_at_this_location import org.meshtastic.core.resources.okay import org.meshtastic.core.ui.component.NodeChip -import org.meshtastic.feature.map.model.NodeClusterItem @Composable fun ClusterItemsListDialog( diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt index 51c655f32..85369120c 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomMapLayersSheet.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.MapLayerItem import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add_layer import org.meshtastic.core.resources.add_network_layer @@ -65,7 +66,6 @@ import org.meshtastic.core.resources.save import org.meshtastic.core.resources.show_layer import org.meshtastic.core.resources.url import org.meshtastic.core.ui.component.MeshtasticDialog -import org.meshtastic.feature.map.MapLayerItem @Suppress("LongMethod") @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManagerSheet.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManagerSheet.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt index 8b7e2d3aa..458de9f56 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/CustomTileProviderManagerSheet.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.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.map.component +package org.meshtastic.app.map.component import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult @@ -51,6 +51,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.collectLatest import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.MapViewModel +import org.meshtastic.app.map.model.CustomTileProviderConfig import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add_custom_tile_source import org.meshtastic.core.resources.add_local_mbtiles_file @@ -70,8 +72,6 @@ import org.meshtastic.core.resources.url_template import org.meshtastic.core.resources.url_template_hint import org.meshtastic.core.ui.component.MeshtasticDialog import org.meshtastic.core.ui.util.showToast -import org.meshtastic.feature.map.MapViewModel -import org.meshtastic.feature.map.model.CustomTileProviderConfig @Suppress("LongMethod") @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt similarity index 96% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt index 8e423dea6..df808c615 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.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.map.component +package org.meshtastic.app.map.component import android.app.DatePickerDialog import android.app.TimePickerDialog @@ -68,9 +68,7 @@ import kotlinx.datetime.number import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.common.util.nowInstant import org.meshtastic.core.common.util.systemTimeZone -import org.meshtastic.core.common.util.toDate import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.date @@ -123,12 +121,12 @@ fun EditWaypointDialog( if (isExpiryEnabled) { if (expireValue != 0 && expireValue != Int.MAX_VALUE) { val instant = Instant.fromEpochSeconds(expireValue.toLong()) - val date = instant.toDate() + val date = java.util.Date(instant.toEpochMilliseconds()) selectedDateString = dateFormat.format(date) selectedTimeString = timeFormat.format(date) } else { // If enabled but not set, default to 8 hours from now - val futureInstant = nowInstant + 8.hours - val date = futureInstant.toDate() + val futureInstant = kotlinx.datetime.Clock.System.now() + 8.hours + val date = java.util.Date(futureInstant.toEpochMilliseconds()) selectedDateString = dateFormat.format(date) selectedTimeString = timeFormat.format(date) waypointInput = waypointInput.copy(expire = futureInstant.epochSeconds.toInt()) @@ -225,7 +223,7 @@ fun EditWaypointDialog( val expireValue = waypointInput.expire ?: 0 // Default to 8 hours from now if not already set if (expireValue == 0 || expireValue == Int.MAX_VALUE) { - val futureInstant = nowInstant + 8.hours + val futureInstant = kotlinx.datetime.Clock.System.now() + 8.hours waypointInput = waypointInput.copy(expire = futureInstant.epochSeconds.toInt()) } } else { @@ -241,7 +239,7 @@ fun EditWaypointDialog( if (it != 0 && it != Int.MAX_VALUE) { Instant.fromEpochSeconds(it.toLong()) } else { - nowInstant + 8.hours + kotlinx.datetime.Clock.System.now() + 8.hours } } val ldt = currentInstant.toLocalDateTime(tz) @@ -256,7 +254,7 @@ fun EditWaypointDialog( if (it != 0 && it != Int.MAX_VALUE) { Instant.fromEpochSeconds(it.toLong()) } else { - nowInstant + 8.hours + kotlinx.datetime.Clock.System.now() + 8.hours } } .toLocalDateTime(tz) @@ -291,7 +289,7 @@ fun EditWaypointDialog( if (it != 0 && it != Int.MAX_VALUE) { Instant.fromEpochSeconds(it.toLong()) } else { - nowInstant + 8.hours + kotlinx.datetime.Clock.System.now() + 8.hours } } .toLocalDateTime(tz) diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapButton.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/MapButton.kt similarity index 94% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapButton.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/MapButton.kt index 6a22fdf52..0d5a79cdb 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapButton.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/MapButton.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,8 +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.map.component +package org.meshtastic.app.map.component import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt index 042e8c58f..e2a73718f 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.MapViewModel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.manage_map_layers import org.meshtastic.core.resources.map_filter @@ -45,7 +46,6 @@ import org.meshtastic.core.resources.orient_north import org.meshtastic.core.resources.refresh import org.meshtastic.core.resources.toggle_my_position import org.meshtastic.core.ui.theme.StatusColors.StatusRed -import org.meshtastic.feature.map.MapViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt index 6314823bd..57886edda 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.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.map.component +package org.meshtastic.app.map.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -39,13 +39,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.MapViewModel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.last_heard_filter_label import org.meshtastic.core.resources.only_favorites import org.meshtastic.core.resources.show_precision_circle import org.meshtastic.core.resources.show_waypoints import org.meshtastic.feature.map.LastHeardFilter -import org.meshtastic.feature.map.MapViewModel import kotlin.math.roundToInt @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapTypeDropdown.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt similarity index 97% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapTypeDropdown.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt index e3722ac29..58c728cec 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/MapTypeDropdown.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.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.map.component +package org.meshtastic.app.map.component import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.maps.android.compose.MapType import org.jetbrains.compose.resources.stringResource +import org.meshtastic.app.map.MapViewModel import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.manage_custom_tile_sources import org.meshtastic.core.resources.map_type_hybrid @@ -35,7 +36,6 @@ import org.meshtastic.core.resources.map_type_normal import org.meshtastic.core.resources.map_type_satellite import org.meshtastic.core.resources.map_type_terrain import org.meshtastic.core.resources.selected_map_type -import org.meshtastic.feature.map.MapViewModel @Suppress("LongMethod") @Composable diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt similarity index 97% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt index 41c895c84..32e250475 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.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.map.component +package org.meshtastic.app.map.component import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,8 +32,8 @@ import com.google.maps.android.compose.Circle import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.clustering.Clustering import com.google.maps.android.compose.clustering.ClusteringMarkerProperties +import org.meshtastic.app.map.model.NodeClusterItem import org.meshtastic.feature.map.BaseMapViewModel -import org.meshtastic.feature.map.model.NodeClusterItem @OptIn(MapsComposeExperimentalApi::class) @Suppress("NestedBlockDepth") diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt index 51d276429..5403b8c11 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/PulsingNodeChip.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.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.map.component +package org.meshtastic.app.map.component import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt rename to app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt index 7072a6ae2..fdc5ee262 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/WaypointMarkers.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.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.map.component +package org.meshtastic.app.map.component import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileProviderConfig.kt b/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt similarity index 96% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileProviderConfig.kt rename to app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt index b188a5eb8..a28b3b6c1 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileProviderConfig.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.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.map.model +package org.meshtastic.app.map.model import kotlinx.serialization.Serializable import kotlin.uuid.Uuid diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt b/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt similarity index 90% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt rename to app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt index d9dcc910b..4adb7d97d 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/CustomTileSource.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.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,8 +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.map.model +package org.meshtastic.app.map.model class CustomTileSource { diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt b/app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt similarity index 97% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt rename to app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt index bea9865e2..943d2c826 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/model/NodeClusterItem.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.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.map.model +package org.meshtastic.app.map.model import com.google.android.gms.maps.model.LatLng import com.google.maps.android.clustering.ClusterItem diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt b/app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt similarity index 95% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt rename to app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt index 430e2c91d..a081a99b1 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/node/NodeMapScreen.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.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.map.node +package org.meshtastic.app.map.node import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -23,8 +23,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.meshtastic.app.map.MapView import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.map.MapView @Composable fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) { diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/di/GoogleMapsModule.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt similarity index 88% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/di/GoogleMapsModule.kt rename to app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.kt index c13b98ca0..a8d0a1192 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/di/GoogleMapsModule.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsModule.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.map.prefs.di +package org.meshtastic.app.map.prefs.di import android.content.Context import androidx.datastore.core.DataStore @@ -31,10 +31,10 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import org.meshtastic.feature.map.prefs.map.GoogleMapsPrefs -import org.meshtastic.feature.map.prefs.map.GoogleMapsPrefsImpl -import org.meshtastic.feature.map.repository.CustomTileProviderRepository -import org.meshtastic.feature.map.repository.CustomTileProviderRepositoryImpl +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 diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/map/GoogleMapsPrefs.kt b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt similarity index 98% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/map/GoogleMapsPrefs.kt rename to app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt index 0fb81a8f3..72760694a 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/prefs/map/GoogleMapsPrefs.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.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.map.prefs.map +package org.meshtastic.app.map.prefs.map import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -31,8 +31,8 @@ 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.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.feature.map.prefs.di.GoogleMapsDataStore import javax.inject.Inject import javax.inject.Singleton diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/repository/CustomTileProviderRepository.kt b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt similarity index 97% rename from feature/map/src/google/kotlin/org/meshtastic/feature/map/repository/CustomTileProviderRepository.kt rename to app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt index 1b55c2397..8d8a1d6cf 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/repository/CustomTileProviderRepository.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.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.map.repository +package org.meshtastic.app.map.repository import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow @@ -23,9 +23,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json +import org.meshtastic.app.map.model.CustomTileProviderConfig import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.MapTileProviderPrefs -import org.meshtastic.feature.map.model.CustomTileProviderConfig import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index 7de47507a..d34038548 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -40,6 +40,7 @@ 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 @@ -47,14 +48,23 @@ 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.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.ui.MainScreen +import org.meshtastic.core.barcode.rememberBarcodeScanner import org.meshtastic.core.model.util.dispatchMeshtasticUri import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI +import org.meshtastic.core.nfc.NfcScannerEffect import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.channel_invalid 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.LocalMapViewProvider +import org.meshtastic.core.ui.util.LocalNfcScannerProvider import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.intro.AppIntroductionScreen import javax.inject.Inject @@ -108,7 +118,13 @@ class MainActivity : ComponentActivity() { } @Suppress("SpreadOperator") - CompositionLocalProvider(*(LocalEnvironmentOwner provides androidEnvironment)) { + CompositionLocalProvider( + *(LocalEnvironmentOwner provides androidEnvironment), + LocalBarcodeScannerProvider provides { onResult -> rememberBarcodeScanner(onResult) }, + LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) }, + LocalAnalyticsIntroProvider provides { AnalyticsIntro() }, + LocalMapViewProvider provides getMapViewProvider(), + ) { AppTheme(dynamicColor = dynamic, darkTheme = dark) { val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle() @@ -119,7 +135,8 @@ class MainActivity : ComponentActivity() { if (appIntroCompleted) { MainScreen(uIViewModel = model) } else { - AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }) + val introViewModel = hiltViewModel() + AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel) } } } diff --git a/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt new file mode 100644 index 000000000..8e9a434fd --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt @@ -0,0 +1,75 @@ +/* + * 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/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt similarity index 97% rename from core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt rename to app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt index 38bb9feff..918da974d 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/di/ServiceModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.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.core.service.di +package org.meshtastic.app.di import dagger.Binds import dagger.Module 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 200294e16..4d009e862 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 @@ -129,7 +129,7 @@ constructor( val matchingNode = if (databaseManager.hasDatabaseFor(entry.fullAddress)) { db.values.find { node -> - val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT) + val suffix = entry.device.getMeshtasticShortName()?.lowercase(Locale.ROOT) suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix) } } else { diff --git a/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.kt new file mode 100644 index 000000000..0414e37bf --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/intro/AndroidIntroViewModel.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.app.intro + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.meshtastic.feature.intro.IntroViewModel +import javax.inject.Inject + +/** Android-specific Hilt wrapper for IntroViewModel. */ +@HiltViewModel class AndroidIntroViewModel @Inject constructor() : 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 new file mode 100644 index 000000000..24ebe7995 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/map/AndroidSharedMapViewModel.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import dagger.hilt.android.lifecycle.HiltViewModel +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( + mapPrefs: MapPrefs, + nodeRepository: NodeRepository, + packetRepository: PacketRepository, + radioController: RadioController, +) : SharedMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt similarity index 94% rename from feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt rename to app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.kt index 7619a3246..a8780be59 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/app/src/main/kotlin/org/meshtastic/app/map/node/NodeMapViewModel.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.map.node +package org.meshtastic.app.map.node import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -35,7 +35,6 @@ import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.ui.util.toPosition import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed -import org.meshtastic.feature.map.model.CustomTileSource import org.meshtastic.proto.PortNum import org.meshtastic.proto.Position import javax.inject.Inject @@ -80,6 +79,6 @@ constructor( } .stateInWhileSubscribed(initialValue = emptyList()) - val tileSource - get() = CustomTileSource.getTileSource(mapPrefs.mapStyle.value) + val mapStyleId: Int + get() = mapPrefs.mapStyle.value } diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt new file mode 100644 index 000000000..e8a23a17a --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidContactsViewModel.kt @@ -0,0 +1,35 @@ +/* + * 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.messaging + +import dagger.hilt.android.lifecycle.HiltViewModel +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( + nodeRepository: NodeRepository, + packetRepository: PacketRepository, + radioConfigRepository: RadioConfigRepository, + serviceRepository: ServiceRepository, +) : ContactsViewModel(nodeRepository, packetRepository, radioConfigRepository, serviceRepository) diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt new file mode 100644 index 000000000..ee7f4e7bb --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidMessageViewModel.kt @@ -0,0 +1,62 @@ +/* + * 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.messaging + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import org.meshtastic.core.data.repository.QuickChatActionRepository +import org.meshtastic.core.repository.CustomEmojiPrefs +import org.meshtastic.core.repository.HomoglyphPrefs +import org.meshtastic.core.repository.MeshServiceNotifications +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.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( + savedStateHandle: SavedStateHandle, + nodeRepository: NodeRepository, + radioConfigRepository: RadioConfigRepository, + quickChatActionRepository: QuickChatActionRepository, + serviceRepository: ServiceRepository, + packetRepository: PacketRepository, + uiPrefs: UiPrefs, + customEmojiPrefs: CustomEmojiPrefs, + homoglyphEncodingPrefs: HomoglyphPrefs, + meshServiceNotifications: MeshServiceNotifications, + sendMessageUseCase: SendMessageUseCase, +) : MessageViewModel( + savedStateHandle, + nodeRepository, + radioConfigRepository, + quickChatActionRepository, + serviceRepository, + packetRepository, + uiPrefs, + customEmojiPrefs, + homoglyphEncodingPrefs, + meshServiceNotifications, + sendMessageUseCase, +) diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt new file mode 100644 index 000000000..b64e5de24 --- /dev/null +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/AndroidQuickChatViewModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.app.messaging + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.meshtastic.core.data.repository.QuickChatActionRepository +import org.meshtastic.feature.messaging.QuickChatViewModel +import javax.inject.Inject + +@HiltViewModel +class AndroidQuickChatViewModel @Inject constructor(quickChatActionRepository: QuickChatActionRepository) : + QuickChatViewModel(quickChatActionRepository) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt similarity index 89% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt rename to app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt index 58e54fcf9..055f5c0cb 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/di/MessagingModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/di/MessagingModule.kt @@ -14,14 +14,14 @@ * 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 +package org.meshtastic.app.messaging.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 org.meshtastic.feature.messaging.domain.worker.WorkManagerMessageQueue @Module @InstallIn(SingletonComponent::class) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt similarity index 97% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt rename to app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt index ac4fd76a0..3b4b8f4d8 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorker.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.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.messaging.domain.worker +package org.meshtastic.app.messaging.domain.worker import android.content.Context import androidx.hilt.work.HiltWorker diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt similarity index 96% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt rename to app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt index dab1837e3..ea26e2c6c 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/domain/worker/WorkManagerMessageQueue.kt +++ b/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.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.messaging.domain.worker +package org.meshtastic.app.messaging.domain.worker import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder diff --git a/app/src/main/kotlin/org/meshtastic/app/model/DeviceListEntry.kt b/app/src/main/kotlin/org/meshtastic/app/model/DeviceListEntry.kt index 8d92cd7a8..cd175f40e 100644 --- a/app/src/main/kotlin/org/meshtastic/app/model/DeviceListEntry.kt +++ b/app/src/main/kotlin/org/meshtastic/app/model/DeviceListEntry.kt @@ -18,8 +18,7 @@ package org.meshtastic.app.model import android.hardware.usb.UsbManager import com.hoho.android.usbserial.driver.UsbSerialDriver -import no.nordicsemi.kotlin.ble.client.android.Peripheral -import no.nordicsemi.kotlin.ble.core.BondState +import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.Node @@ -50,15 +49,14 @@ sealed class DeviceListEntry( override fun toString(): String = "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize}, bonded=$bonded, hasNode=${node != null})" - @Suppress("MissingPermission") - data class Ble(val peripheral: Peripheral, override val node: Node? = null) : + data class Ble(val device: BleDevice, override val node: Node? = null) : DeviceListEntry( - name = peripheral.name ?: "unnamed-${peripheral.address}", - fullAddress = "x${peripheral.address}", - bonded = peripheral.bondState.value == BondState.BONDED, + name = device.name ?: "unnamed-${device.address}", + fullAddress = "x${device.address}", + bonded = device.isBonded, node = node, ) { - override fun copy(node: Node?): Ble = copy(peripheral = peripheral, node = node) + override fun copy(node: Node?): Ble = copy(device = device, node = node) } data class Usb( @@ -95,4 +93,4 @@ private val bleNameRegex = Regex(BLE_NAME_PATTERN) * * @return The short name (e.g., 1234) or null. */ -fun Peripheral.getMeshtasticShortName(): String? = name?.let { bleNameRegex.find(it)?.groupValues?.get(1) } +fun BleDevice.getMeshtasticShortName(): String? = name?.let { bleNameRegex.find(it)?.groupValues?.get(1) } 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 9caec2f08..130196bc1 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/ContactsNavigation.kt @@ -26,6 +26,9 @@ import androidx.navigation.navDeepLink import androidx.navigation.navigation import androidx.navigation.toRoute import kotlinx.coroutines.flow.Flow +import org.meshtastic.app.messaging.AndroidContactsViewModel +import org.meshtastic.app.messaging.AndroidMessageViewModel +import org.meshtastic.app.messaging.AndroidQuickChatViewModel import org.meshtastic.app.model.UIViewModel import org.meshtastic.core.navigation.ContactsRoutes import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI @@ -43,9 +46,13 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE val uiViewModel: UIViewModel = hiltViewModel() val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle() val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle() + val contactsViewModel = hiltViewModel() + val messageViewModel = hiltViewModel() AdaptiveContactsScreen( navController = navController, + contactsViewModel = contactsViewModel, + messageViewModel = messageViewModel, scrollToTopEvents = scrollToTopEvents, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, @@ -67,9 +74,13 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE val uiViewModel: UIViewModel = hiltViewModel() val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle() val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle() + val contactsViewModel = hiltViewModel() + val messageViewModel = hiltViewModel() AdaptiveContactsScreen( navController = navController, + contactsViewModel = contactsViewModel, + messageViewModel = messageViewModel, scrollToTopEvents = scrollToTopEvents, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, @@ -90,7 +101,9 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE ), ) { backStackEntry -> val message = backStackEntry.toRoute().message + val viewModel = hiltViewModel() ShareScreen( + viewModel = viewModel, onConfirm = { navController.navigate(ContactsRoutes.Messages(it, message)) { popUpTo { inclusive = true } @@ -102,6 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE composable( deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/quick_chat")), ) { - QuickChatScreen(onNavigateUp = navController::navigateUp) + val viewModel = hiltViewModel() + QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp) } } 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 da766bd06..71adb01cc 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/MapNavigation.kt @@ -16,10 +16,12 @@ */ 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.meshtastic.app.map.AndroidSharedMapViewModel import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI import org.meshtastic.core.navigation.MapRoutes import org.meshtastic.core.navigation.NodesRoutes @@ -27,7 +29,9 @@ import org.meshtastic.feature.map.MapScreen fun NavGraphBuilder.mapGraph(navController: NavHostController) { composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) { + val viewModel = hiltViewModel() MapScreen( + viewModel = viewModel, onClickNodeChip = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) { launchSingleTop = true 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 8d628a96c..56d44b6f4 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt @@ -40,6 +40,8 @@ import androidx.navigation.navDeepLink import androidx.navigation.toRoute import kotlinx.coroutines.flow.Flow import org.jetbrains.compose.resources.StringResource +import org.meshtastic.app.map.node.NodeMapScreen +import org.meshtastic.app.map.node.NodeMapViewModel import org.meshtastic.app.ui.node.AdaptiveNodeListScreen import org.meshtastic.core.navigation.ContactsRoutes import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI @@ -57,8 +59,6 @@ import org.meshtastic.core.resources.power import org.meshtastic.core.resources.signal import org.meshtastic.core.resources.traceroute import org.meshtastic.core.ui.component.ScrollToTopEvent -import org.meshtastic.feature.map.node.NodeMapScreen -import org.meshtastic.feature.map.node.NodeMapViewModel import org.meshtastic.feature.node.metrics.DeviceMetricsScreen import org.meshtastic.feature.node.metrics.EnvironmentMetricsScreen import org.meshtastic.feature.node.metrics.HostMetricsLogScreen 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 155eaec8a..fd0371af8 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 @@ -33,12 +33,15 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import no.nordicsemi.kotlin.ble.client.android.CentralManager -import no.nordicsemi.kotlin.ble.client.android.Peripheral -import no.nordicsemi.kotlin.ble.core.ConnectionState -import no.nordicsemi.kotlin.ble.core.WriteType +import org.meshtastic.core.ble.AndroidBleDevice +import org.meshtastic.core.ble.AndroidBleService import org.meshtastic.core.ble.BleConnection +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleConnectionState +import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.ble.BleWriteType +import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID import org.meshtastic.core.ble.retryBleOperation import org.meshtastic.core.common.util.nowMillis @@ -62,7 +65,9 @@ private val SCAN_TIMEOUT = 5.seconds * - Routing raw byte packets between the radio and [RadioInterfaceService]. * * @param serviceScope The coroutine scope to use for launching coroutines. - * @param centralManager The central manager provided by Nordic BLE Library. + * @param scanner The BLE scanner. + * @param bluetoothRepository The Bluetooth repository. + * @param connectionFactory The BLE connection factory. * @param service The [RadioInterfaceService] to use for handling radio events. * @param address The BLE address of the device to connect to. */ @@ -71,7 +76,9 @@ class NordicBleInterface @AssistedInject constructor( private val serviceScope: CoroutineScope, - private val centralManager: CentralManager, + private val scanner: BleScanner, + private val bluetoothRepository: BluetoothRepository, + private val connectionFactory: BleConnectionFactory, private val service: RadioInterfaceService, @Assisted val address: String, ) : IRadioInterface { @@ -91,7 +98,7 @@ constructor( private val connectionScope: CoroutineScope = CoroutineScope(serviceScope.coroutineContext + SupervisorJob() + exceptionHandler) - private val bleConnection: BleConnection = BleConnection(centralManager, connectionScope, address) + private val bleConnection: BleConnection = connectionFactory.create(connectionScope, address) private val writeMutex: Mutex = Mutex() private var connectionStartTime: Long = 0 @@ -106,21 +113,19 @@ constructor( // --- Connection & Discovery Logic --- - /** Robustly finds the peripheral. First checks bonded devices, then performs a short scan if not found. */ - private suspend fun findPeripheral(): Peripheral { - centralManager - .getBondedPeripherals() + /** Robustly finds the device. First checks bonded devices, then performs a short scan if not found. */ + private suspend fun findDevice(): BleDevice { + bluetoothRepository.state.value.bondedDevices .firstOrNull { it.address == address } ?.let { return it } Logger.i { "[$address] Device not found in bonded list, scanning..." } - val scanner = BleScanner(centralManager) repeat(SCAN_RETRY_COUNT) { attempt -> - val p = scanner.scan(SCAN_TIMEOUT).firstOrNull { it.address == address } - if (p != null) return p + val d = scanner.scan(SCAN_TIMEOUT).firstOrNull { it.address == address } + if (d != null) return d if (attempt < SCAN_RETRY_COUNT - 1) { delay(SCAN_RETRY_DELAY_MS) @@ -138,7 +143,7 @@ constructor( bleConnection.connectionState .onEach { state -> - if (state is ConnectionState.Disconnected) { + if (state is BleConnectionState.Disconnected) { onDisconnected(state) } } @@ -148,9 +153,9 @@ constructor( } .launchIn(connectionScope) - val p = findPeripheral() - val state = bleConnection.connectAndAwait(p, CONNECTION_TIMEOUT_MS) - if (state !is ConnectionState.Connected) { + val device = findDevice() + val state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS) + if (state !is BleConnectionState.Connected) { throw RadioNotConnectedException("Failed to connect to device at address $address") } @@ -158,7 +163,7 @@ constructor( discoverServicesAndSetupCharacteristics() } catch (e: Exception) { val failureTime = nowMillis - connectionStartTime - Logger.w(e) { "[$address] Failed to connect to peripheral after ${failureTime}ms" } + Logger.w(e) { "[$address] Failed to connect to device after ${failureTime}ms" } handleFailure(e) } } @@ -166,8 +171,9 @@ constructor( private suspend fun onConnected() { try { - bleConnection.peripheralFlow.first()?.let { p -> - val rssi = retryBleOperation(tag = address) { p.readRssi() } + bleConnection.deviceFlow.first()?.let { device -> + val androidDevice = device as AndroidBleDevice + val rssi = retryBleOperation(tag = address) { androidDevice.peripheral.readRssi() } Logger.d { "[$address] Connection confirmed. Initial RSSI: $rssi dBm" } } } catch (e: Exception) { @@ -175,7 +181,7 @@ constructor( } } - private fun onDisconnected(state: ConnectionState.Disconnected) { + private fun onDisconnected(@Suppress("UNUSED_PARAMETER") state: BleConnectionState.Disconnected) { radioService = null val uptime = @@ -185,26 +191,22 @@ constructor( 0 } Logger.w { - "[$address] BLE disconnected - Reason: ${state.reason}, " + + "[$address] BLE disconnected, " + "Uptime: ${uptime}ms, " + "Packets RX: $packetsReceived ($bytesReceived bytes), " + "Packets TX: $packetsSent ($bytesSent bytes)" } - val (isPermanent, msg) = - when (val reason = state.reason) { - is ConnectionState.Disconnected.Reason.InsufficientAuthentication -> - Pair(true, "Insufficient authentication: please unpair and repair the device") - is ConnectionState.Disconnected.Reason.RequiredServiceNotFound -> - Pair(false, "Required characteristic missing") - else -> Pair(false, reason.toString()) - } - service.onDisconnect(isPermanent, errorMessage = msg) + + // Note: Disconnected state in commonMain doesn't currently carry a reason. + // We might want to add that later if needed. + service.onDisconnect(false, errorMessage = "Disconnected") } private suspend fun discoverServicesAndSetupCharacteristics() { try { bleConnection.profile(serviceUuid = SERVICE_UUID) { service -> - val radioService = MeshtasticRadioServiceImpl(service) + val androidService = (service as AndroidBleService).service + val radioService = MeshtasticRadioServiceImpl(androidService) // Wire up notifications radioService.fromRadio @@ -235,7 +237,7 @@ constructor( Logger.i { "[$address] Profile service active and characteristics subscribed" } // Log negotiated MTU for diagnostics - val maxLen = bleConnection.maximumWriteValueLength(WriteType.WITHOUT_RESPONSE) + val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE) Logger.i { "[$address] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): $maxLen bytes" } this@NordicBleInterface.service.onConnect() 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 56038c94e..570996691 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt +++ b/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt @@ -20,8 +20,8 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf +import org.meshtastic.app.messaging.domain.worker.SendMessageWorker import org.meshtastic.core.repository.MeshWorkerManager -import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker import javax.inject.Inject import javax.inject.Singleton 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 0ea247a12..cb03f8446 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 @@ -117,15 +117,15 @@ constructor( /** Initiates the bonding process and connects to the device upon success. */ private fun requestBonding(entry: DeviceListEntry.Ble) { - Logger.i { "Starting bonding for ${entry.peripheral.address.anonymize}" } + Logger.i { "Starting bonding for ${entry.device.address.anonymize}" } viewModelScope.launch { @Suppress("TooGenericExceptionCaught") try { - bluetoothRepository.bond(entry.peripheral) - Logger.i { "Bonding complete for ${entry.peripheral.address.anonymize}, selecting device..." } + bluetoothRepository.bond(entry.device) + Logger.i { "Bonding complete for ${entry.device.address.anonymize}, selecting device..." } changeDeviceAddress(entry.fullAddress) } catch (ex: SecurityException) { - Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize} Permissions not granted" } + Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize} Permissions not granted" } serviceRepository.setErrorMessage( text = "Bonding failed: ${ex.message} Permissions not granted", severity = Severity.Warn, @@ -135,9 +135,9 @@ constructor( val message = ex.message ?: "" if (message.contains("Received bond state changed 11")) { // This is a known issue where bonding is still in progress, ignore as error - Logger.d { "Bonding still in progress for ${entry.peripheral.address.anonymize}" } + Logger.d { "Bonding still in progress for ${entry.device.address.anonymize}" } } else { - Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize}" } + Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize}" } serviceRepository.setErrorMessage(text = "Bonding failed: ${ex.message}", severity = Severity.Warn) } } 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 960fcda6b..959c4ff3f 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 @@ -35,6 +35,7 @@ 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 @@ -73,12 +74,14 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod ScannerView( state = filterState, - onScanResultSelected = { result -> scanModel.onSelected(DeviceListEntry.Ble(result.peripheral)) }, + 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(result.peripheral) + ?: DeviceListEntry.Ble(AndroidBleDevice(result.peripheral)) } Card( modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/CurrentlyConnectedInfo.kt b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/CurrentlyConnectedInfo.kt index 16f7af6ec..c8e80b91f 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/CurrentlyConnectedInfo.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/connections/components/CurrentlyConnectedInfo.kt @@ -43,8 +43,6 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.delay import kotlinx.coroutines.withTimeout import no.nordicsemi.android.common.ui.view.RssiIcon -import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException -import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException import org.jetbrains.compose.resources.stringResource import org.meshtastic.app.model.DeviceListEntry import org.meshtastic.core.model.Node @@ -75,23 +73,14 @@ fun CurrentlyConnectedInfo( var rssi by remember { mutableIntStateOf(0) } LaunchedEffect(bleDevice) { if (bleDevice != null) { - while (bleDevice.peripheral.isConnected) { + while (bleDevice.device.isConnected) { try { - rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.peripheral.readRssi() } + rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.device.readRssi() } delay(RSSI_DELAY.seconds) - } catch (e: PeripheralNotConnectedException) { - Logger.w(e) { "Failed to read RSSI ${e.message}" } - break - } catch (e: OperationFailedException) { + } catch (e: Exception) { // RSSI reading failures are common when disconnecting; log as warning to avoid Crashlytics noise Logger.w(e) { "Failed to read RSSI ${e.message}" } break - } catch (e: SecurityException) { - Logger.w(e) { "Failed to read RSSI ${e.message}" } - break - } catch (e: Exception) { - Logger.w(e) { "Unexpected error reading RSSI: ${e.message}" } - break } } } diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt b/app/src/test/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorkerTest.kt similarity index 99% rename from feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt rename to app/src/test/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorkerTest.kt index 537bc1d63..3f0f10068 100644 --- a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/domain/worker/SendMessageWorkerTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorkerTest.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.messaging.domain.worker +package org.meshtastic.app.messaging.domain.worker import android.content.Context import androidx.test.core.app.ApplicationProvider diff --git a/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceRetryTest.kt b/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceRetryTest.kt index c9abcdf5e..90840450f 100644 --- a/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceRetryTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceRetryTest.kt @@ -141,10 +141,25 @@ class NordicBleInterfaceRetryTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) @@ -246,10 +261,25 @@ class NordicBleInterfaceRetryTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = uniqueAddress, ) diff --git a/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceTest.kt b/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceTest.kt index d737e7671..faf62d3d4 100644 --- a/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/repository/radio/NordicBleInterfaceTest.kt @@ -151,10 +151,25 @@ class NordicBleInterfaceTest { // Create the interface println("Creating NordicBleInterface") + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) @@ -284,10 +299,25 @@ class NordicBleInterfaceTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) @@ -377,10 +407,25 @@ class NordicBleInterfaceTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) @@ -467,10 +512,25 @@ class NordicBleInterfaceTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) @@ -553,10 +613,25 @@ class NordicBleInterfaceTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = uniqueAddress, ) @@ -644,10 +719,25 @@ class NordicBleInterfaceTest { centralManager.simulatePeripherals(listOf(peripheralSpec)) advanceUntilIdle() + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val bluetoothRepository: org.meshtastic.core.ble.BluetoothRepository = mockk { + io.mockk.every { state } returns + kotlinx.coroutines.flow.MutableStateFlow( + org.meshtastic.core.ble.BluetoothState( + hasPermissions = true, + enabled = true, + bondedDevices = emptyList(), + ), + ) + } + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val nordicInterface = NordicBleInterface( serviceScope = this, - centralManager = centralManager, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, service = service, address = address, ) diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties index 415377946..ede665cdc 100644 --- a/build-logic/gradle.properties +++ b/build-logic/gradle.properties @@ -19,13 +19,13 @@ # These need to be set separately because properties are not passed to included builds. # https://github.com/gradle/gradle/issues/2534 -org.gradle.jvmargs=-Xmx2g -XX:+UseG1GC -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC -Dfile.encoding=UTF-8 # Parallelism & Caching org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true -org.gradle.isolated-projects=false +org.gradle.isolated-projects=true org.gradle.vfs.watch=true org.gradle.configureondemand=false diff --git a/core/barcode/build.gradle.kts b/core/barcode/build.gradle.kts index f5978105c..91f319b07 100644 --- a/core/barcode/build.gradle.kts +++ b/core/barcode/build.gradle.kts @@ -30,6 +30,7 @@ configure { dependencies { implementation(project(":core:resources")) + implementation(projects.core.ui) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) diff --git a/core/barcode/src/fdroid/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt b/core/barcode/src/fdroid/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt index 5d12b6f13..9f68d3791 100644 --- a/core/barcode/src/fdroid/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt +++ b/core/barcode/src/fdroid/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt @@ -66,6 +66,7 @@ import com.google.zxing.common.HybridBinarizer import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.close +import org.meshtastic.core.ui.util.BarcodeScanner import java.nio.ByteBuffer import java.util.concurrent.Executors diff --git a/core/barcode/src/google/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt b/core/barcode/src/google/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt index c9ff070bd..df06400d8 100644 --- a/core/barcode/src/google/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt +++ b/core/barcode/src/google/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt @@ -66,6 +66,7 @@ import com.google.mlkit.vision.common.InputImage import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.close +import org.meshtastic.core.ui.util.BarcodeScanner import java.util.concurrent.Executors @Composable diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts index 1cb622d54..191a335be 100644 --- a/core/ble/build.gradle.kts +++ b/core/ble/build.gradle.kts @@ -15,37 +15,55 @@ * 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.hilt) + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.devtools.ksp) } -configure { namespace = "org.meshtastic.core.ble" } +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.core.ble" + androidResources.enable = false + } -dependencies { - implementation(projects.core.common) - implementation(projects.core.di) - implementation(projects.core.model) + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.di) + implementation(projects.core.model) - api(libs.nordic.client.android) - api(libs.nordic.ble.env.android) - api(libs.nordic.ble.env.android.compose) - api(libs.nordic.common.scanner.ble) - api(libs.nordic.common.core) + implementation(libs.kermit) + implementation(libs.kotlinx.coroutines.core) + api(libs.javax.inject) + } - implementation(libs.androidx.lifecycle.process) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.javax.inject) - implementation(libs.kermit) - implementation(libs.kotlinx.coroutines.core) + androidMain.dependencies { + implementation(libs.hilt.android) + api(libs.nordic.client.android) + api(libs.nordic.ble.env.android) + api(libs.nordic.ble.env.android.compose) + api(libs.nordic.common.scanner.ble) + api(libs.nordic.common.core) - testImplementation(libs.junit) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.mockk) - testImplementation(libs.nordic.client.android.mock) - testImplementation(libs.nordic.client.core.mock) - testImplementation(libs.nordic.core.mock) - testImplementation(libs.androidx.lifecycle.testing) + implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.lifecycle.runtime.ktx) + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.mockk) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.nordic.client.android.mock) + implementation(libs.nordic.client.core.mock) + implementation(libs.nordic.core.mock) + implementation(libs.androidx.lifecycle.testing) + } + } } + +dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleConnection.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnection.kt similarity index 57% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/BleConnection.kt rename to core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnection.kt index 5472eb704..36895f66e 100644 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleConnection.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnection.kt @@ -34,92 +34,85 @@ import kotlinx.coroutines.withTimeout import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.kotlin.ble.client.android.CentralManager import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority -import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.core.ConnectionState import no.nordicsemi.kotlin.ble.core.WriteType -import kotlin.time.Duration.Companion.seconds import kotlin.uuid.Uuid /** - * Encapsulates a BLE connection to a [Peripheral]. Handles connection lifecycle, state monitoring, and service - * discovery. + * An Android implementation of [BleConnection] using Nordic's [CentralManager]. * * @param centralManager The Nordic [CentralManager] to use for connection. * @param scope The [CoroutineScope] in which to monitor connection state. * @param tag A tag for logging. */ -class BleConnection( +class AndroidBleConnection( private val centralManager: CentralManager, private val scope: CoroutineScope, private val tag: String = "BLE", -) { - /** The currently connected [Peripheral], or null if not connected. */ - var peripheral: Peripheral? = null - private set +) : BleConnection { - private val _peripheral = MutableSharedFlow(replay = 1) + private var _device: AndroidBleDevice? = null + override val device: BleDevice? + get() = _device - /** A flow of the current peripheral. */ - val peripheralFlow = _peripheral.asSharedFlow() + private val _deviceFlow = MutableSharedFlow(replay = 1) + override val deviceFlow: SharedFlow = _deviceFlow.asSharedFlow() - private val _connectionState = simpleSharedFlow() - - /** A flow of [ConnectionState] changes for the current [peripheral]. */ - val connectionState: SharedFlow = _connectionState.asSharedFlow() + private val _connectionState = simpleSharedFlow() + override val connectionState: SharedFlow = _connectionState.asSharedFlow() private var stateJob: Job? = null private var profileJob: Job? = null - /** - * Connects to the given [Peripheral]. Note that this method returns as soon as the connection attempt is initiated. - * Use [connectAndAwait] if you need to wait for the connection to be established. - * - * @param p The peripheral to connect to. - */ - suspend fun connect(p: Peripheral) = withContext(NonCancellable) { + override suspend fun connect(device: BleDevice) = withContext(NonCancellable) { + val androidDevice = device as AndroidBleDevice stateJob?.cancel() - peripheral = p - _peripheral.emit(p) + _device = androidDevice + _deviceFlow.emit(androidDevice) centralManager.connect( - peripheral = p, + peripheral = androidDevice.peripheral, options = CentralManager.ConnectionOptions.AutoConnect(automaticallyRequestHighestValueLength = true), ) stateJob = - p.state + androidDevice.peripheral.state .onEach { state -> Logger.d { "[$tag] Connection state changed to $state" } + val commonState = + when (state) { + is ConnectionState.Connecting -> BleConnectionState.Connecting + is ConnectionState.Connected -> BleConnectionState.Connected + is ConnectionState.Disconnecting -> BleConnectionState.Disconnecting + is ConnectionState.Disconnected -> BleConnectionState.Disconnected + } if (state is ConnectionState.Connected) { - p.requestConnectionPriority(ConnectionPriority.HIGH) - observePeripheralDetails(p) + androidDevice.peripheral.requestConnectionPriority(ConnectionPriority.HIGH) + observePeripheralDetails(androidDevice) } - _connectionState.emit(state) + androidDevice.updateState(state) + _connectionState.emit(commonState) } .launchIn(scope) } - /** - * Connects to the given [Peripheral] and waits for a terminal state (Connected or Disconnected). - * - * @param p The peripheral to connect to. - * @param timeoutMs The maximum time to wait for a connection in milliseconds. - * @param onRegister Optional block to run before connecting, allowing for profile registration. - * @return The final [ConnectionState]. - * @throws kotlinx.coroutines.TimeoutCancellationException if the timeout is reached. - */ - suspend fun connectAndAwait(p: Peripheral, timeoutMs: Long, onRegister: suspend () -> Unit = {}): ConnectionState { + override suspend fun connectAndAwait( + device: BleDevice, + timeoutMs: Long, + onRegister: suspend () -> Unit, + ): BleConnectionState { onRegister() - connect(p) + connect(device) return withTimeout(timeoutMs) { - connectionState.first { it is ConnectionState.Connected || it is ConnectionState.Disconnected } + connectionState.first { it is BleConnectionState.Connected || it is BleConnectionState.Disconnected } } } @Suppress("TooGenericExceptionCaught") - private fun observePeripheralDetails(p: Peripheral) { + private fun observePeripheralDetails(androidDevice: AndroidBleDevice) { + val p = androidDevice.peripheral p.phy.onEach { phy -> Logger.i { "[$tag] BLE PHY changed to $phy" } }.launchIn(scope) p.connectionParameters @@ -135,32 +128,24 @@ class BleConnection( .launchIn(scope) } - /** Disconnects from the current peripheral. */ - suspend fun disconnect() = withContext(NonCancellable) { + override suspend fun disconnect() = withContext(NonCancellable) { stateJob?.cancel() stateJob = null profileJob?.cancel() profileJob = null - peripheral?.disconnect() - peripheral = null - _peripheral.emit(null) + _device?.peripheral?.disconnect() + _device = null + _deviceFlow.emit(null) } - /** - * Executes a block within a discovered profile. Handles peripheral readiness, discovery with a timeout, and cleans - * up the profile job if discovery fails. - * - * @param serviceUuid The UUID of the service to discover. - * @param timeout The duration to wait for discovery. - * @param block The block to execute with the discovered service. - */ @Suppress("TooGenericExceptionCaught") - suspend fun profile( + override suspend fun profile( serviceUuid: Uuid, - timeout: kotlin.time.Duration = 30.seconds, - setup: suspend CoroutineScope.(no.nordicsemi.kotlin.ble.client.RemoteService) -> T, + timeout: kotlin.time.Duration, + setup: suspend CoroutineScope.(BleService) -> T, ): T { - val p = peripheralFlow.first { it != null }!! + val androidDevice = deviceFlow.first { it != null } as AndroidBleDevice + val p = androidDevice.peripheral val serviceReady = CompletableDeferred() profileJob?.cancel() @@ -170,9 +155,8 @@ class BleConnection( val profileScope = this p.profile(serviceUuid = serviceUuid, required = true, scope = profileScope) { service -> try { - val result = setup(service) + val result = setup(AndroidBleService(service)) serviceReady.complete(result) - // Keep the profile active until this launch scope (profileJob) is cancelled awaitCancellation() } catch (e: Throwable) { if (!serviceReady.isCompleted) serviceReady.completeExceptionally(e) @@ -193,11 +177,17 @@ class BleConnection( } } - /** Returns the maximum write value length for the given write type. */ - fun maximumWriteValueLength(writeType: WriteType): Int? = peripheral?.maximumWriteValueLength(writeType) + override fun maximumWriteValueLength(writeType: BleWriteType): Int? { + val nordicWriteType = + when (writeType) { + BleWriteType.WITH_RESPONSE -> WriteType.WITH_RESPONSE + BleWriteType.WITHOUT_RESPONSE -> WriteType.WITHOUT_RESPONSE + } + return _device?.peripheral?.maximumWriteValueLength(nordicWriteType) + } /** Requests a new connection priority for the current peripheral. */ suspend fun requestConnectionPriority(priority: ConnectionPriority) { - peripheral?.requestConnectionPriority(priority) + _device?.peripheral?.requestConnectionPriority(priority) } } diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothState.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt similarity index 50% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothState.kt rename to core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt index c0123ef20..6166287ef 100644 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothState.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleConnectionFactory.kt @@ -16,20 +16,15 @@ */ package org.meshtastic.core.ble -import no.nordicsemi.kotlin.ble.client.android.Peripheral -import org.meshtastic.core.model.util.anonymize +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.kotlin.ble.client.android.CentralManager +import javax.inject.Inject +import javax.inject.Singleton -/** A snapshot in time of the state of the bluetooth subsystem. */ -data class BluetoothState( - /** Whether we have adequate permissions to query bluetooth state */ - val hasPermissions: Boolean = false, - /** If we have adequate permissions and bluetooth is enabled */ - val enabled: Boolean = false, - /** If enabled, a list of the currently bonded devices */ - val bondedDevices: List = emptyList(), -) { - override fun toString(): String = - "BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { - it.anonymize - }})" +/** An Android implementation of [BleConnectionFactory]. */ +@Singleton +class AndroidBleConnectionFactory @Inject constructor(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/AndroidBleDevice.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleDevice.kt new file mode 100644 index 000000000..54fa3231c --- /dev/null +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleDevice.kt @@ -0,0 +1,63 @@ +/* + * 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.core.ble + +import android.annotation.SuppressLint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.core.BondState +import no.nordicsemi.kotlin.ble.core.ConnectionState + +/** An Android implementation of [BleDevice] that wraps a Nordic [Peripheral]. */ +class AndroidBleDevice(val peripheral: Peripheral) : BleDevice { + override val name: String? + get() = peripheral.name + + override val address: String + get() = peripheral.address + + private val _state = MutableStateFlow(BleConnectionState.Disconnected) + override val state: StateFlow = _state.asStateFlow() + + @Suppress("MissingPermission") + override val isBonded: Boolean + get() = peripheral.bondState.value == BondState.BONDED + + override val isConnected: Boolean + get() = peripheral.isConnected + + @SuppressLint("MissingPermission") + override suspend fun readRssi(): Int = peripheral.readRssi() + + @SuppressLint("MissingPermission") + override suspend fun bond() { + peripheral.createBond() + } + + /** Updates the connection state based on Nordic's [ConnectionState]. */ + fun updateState(nordicState: ConnectionState) { + _state.value = + when (nordicState) { + is ConnectionState.Connecting -> BleConnectionState.Connecting + is ConnectionState.Connected -> BleConnectionState.Connected + is ConnectionState.Disconnecting -> BleConnectionState.Disconnecting + is ConnectionState.Disconnected -> BleConnectionState.Disconnected + } + } +} diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleScanner.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt similarity index 53% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/BleScanner.kt rename to core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt index 690ce766a..828ed6d10 100644 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleScanner.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleScanner.kt @@ -19,33 +19,17 @@ package org.meshtastic.core.ble import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import no.nordicsemi.kotlin.ble.client.android.CentralManager -import no.nordicsemi.kotlin.ble.client.android.ConjunctionFilterScope -import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.client.distinctByPeripheral import javax.inject.Inject import kotlin.time.Duration /** - * A wrapper around [CentralManager]'s scanning capabilities to provide a consistent and easy-to-use API for BLE - * scanning across the application. + * An Android implementation of [BleScanner] using Nordic's [CentralManager]. * * @param centralManager The Nordic [CentralManager] to use for scanning. */ -class BleScanner @Inject constructor(private val centralManager: CentralManager) { +class AndroidBleScanner @Inject constructor(private val centralManager: CentralManager) : BleScanner { - /** - * Scans for BLE devices. - * - * @param timeout The duration of the scan. - * @param filterBlock Optional filter configuration block. - * @return A [Flow] of discovered [Peripheral]s. - */ - fun scan(timeout: Duration, filterBlock: (ConjunctionFilterScope.() -> Unit)? = null): Flow = - if (filterBlock != null) { - centralManager.scan(timeout, filterBlock) - } else { - centralManager.scan(timeout) - } - .distinctByPeripheral() - .map { it.peripheral } + 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/AndroidBleService.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleService.kt new file mode 100644 index 000000000..46b0d6cd2 --- /dev/null +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBleService.kt @@ -0,0 +1,22 @@ +/* + * 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.core.ble + +import no.nordicsemi.kotlin.ble.client.RemoteService + +/** An Android implementation of [BleService] that wraps a Nordic [RemoteService]. */ +class AndroidBleService(val service: RemoteService) : BleService diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt similarity index 72% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt rename to core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt index dbf68f811..24137e8a2 100644 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepository.kt @@ -36,26 +36,18 @@ import org.meshtastic.core.di.ProcessLifecycle import javax.inject.Inject import javax.inject.Singleton -/** Repository responsible for maintaining and updating the state of Bluetooth availability. */ +/** Android implementation of [BluetoothRepository]. */ @Singleton -class BluetoothRepository +class AndroidBluetoothRepository @Inject constructor( private val dispatchers: CoroutineDispatchers, @ProcessLifecycle private val processLifecycle: Lifecycle, private val centralManager: CentralManager, private val androidEnvironment: AndroidEnvironment, -) { - private val _state = - MutableStateFlow( - BluetoothState( - // Assume we have permission until we get our initial state update to prevent premature - // notifications to the user. - hasPermissions = true, - ), - ) - val state: StateFlow - get() = _state.asStateFlow() +) : BluetoothRepository { + private val _state = MutableStateFlow(BluetoothState(hasPermissions = true)) + override val state: StateFlow = _state.asStateFlow() init { processLifecycle.coroutineScope.launch(dispatchers.default) { @@ -63,25 +55,16 @@ constructor( } } - fun refreshState() { + override fun refreshState() { processLifecycle.coroutineScope.launch(dispatchers.default) { updateBluetoothState() } } - /** @return true for a valid Bluetooth address, false otherwise */ - fun isValid(bleAddress: String): Boolean = BluetoothAdapter.checkBluetoothAddress(bleAddress) + override fun isValid(bleAddress: String): Boolean = BluetoothAdapter.checkBluetoothAddress(bleAddress) - /** - * Initiates bonding with the given peripheral. This is a suspending function that completes when the bonding - * process is finished. After successful bonding, the repository's state is refreshed to include the new bonded - * device. - * - * @param peripheral The peripheral to bond with. - * @throws SecurityException if required Bluetooth permissions are not granted. - * @throws Exception if the bonding process fails. - */ @SuppressLint("MissingPermission") - suspend fun bond(peripheral: Peripheral) { - peripheral.createBond() + override suspend fun bond(device: BleDevice) { + val androidDevice = device as AndroidBleDevice + androidDevice.peripheral.createBond() updateBluetoothState() } @@ -100,16 +83,15 @@ constructor( } @SuppressLint("MissingPermission") - private fun getBondedAppPeripherals(enabled: Boolean, hasPerms: Boolean): List = + private fun getBondedAppPeripherals(enabled: Boolean, hasPerms: Boolean): List = if (enabled && hasPerms) { - centralManager.getBondedPeripherals().filter(::isMatchingPeripheral) + centralManager.getBondedPeripherals().filter(::isMatchingPeripheral).map { AndroidBleDevice(it) } } else { emptyList() } - /** @return true if the given address is currently bonded to the system. */ @SuppressLint("MissingPermission") - fun isBonded(address: String): Boolean { + override fun isBonded(address: String): Boolean { val enabled = androidEnvironment.isBluetoothEnabled val hasPerms = hasRequiredPermissions() return if (enabled && hasPerms) { @@ -126,7 +108,6 @@ constructor( androidEnvironment.isLocationPermissionGranted } - /** Checks if a peripheral is one of ours, either by its advertised name or by the services it provides. */ private fun isMatchingPeripheral(peripheral: Peripheral): Boolean { val nameMatches = peripheral.name?.matches(Regex(BLE_NAME_PATTERN)) ?: false val hasRequiredService = diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt new file mode 100644 index 000000000..3855eff05 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt @@ -0,0 +1,69 @@ +/* + * 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.core.ble + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharedFlow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.uuid.Uuid + +/** Represents the type of write operation. */ +enum class BleWriteType { + WITH_RESPONSE, + WITHOUT_RESPONSE, +} + +/** Encapsulates a BLE connection to a [BleDevice]. */ +interface BleConnection { + /** The currently connected [BleDevice], or null if not connected. */ + val device: BleDevice? + + /** A flow of the current device. */ + val deviceFlow: SharedFlow + + /** A flow of [BleConnectionState] changes. */ + val connectionState: SharedFlow + + /** Connects to the given [BleDevice]. */ + suspend fun connect(device: BleDevice) + + /** Connects to the given [BleDevice] and waits for a terminal state. */ + suspend fun connectAndAwait( + device: BleDevice, + timeoutMs: Long, + onRegister: suspend () -> Unit = {}, + ): BleConnectionState + + /** Disconnects from the current device. */ + suspend fun disconnect() + + /** Executes a block within a discovered profile. */ + suspend fun profile( + serviceUuid: Uuid, + timeout: Duration = 30.seconds, + setup: suspend CoroutineScope.(BleService) -> T, + ): T + + /** Returns the maximum write value length for the given write type. */ + fun maximumWriteValueLength(writeType: BleWriteType): Int? +} + +/** Represents a BLE service for commonMain. */ +interface BleService { + // This will be expanded as needed, but for now we just need a common type to pass around. +} diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionFactory.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionFactory.kt new file mode 100644 index 000000000..efa7fe3cb --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.core.ble + +import kotlinx.coroutines.CoroutineScope + +/** A factory for creating [BleConnection] instances. */ +interface BleConnectionFactory { + /** + * Creates a new [BleConnection] instance. + * + * @param scope The [CoroutineScope] in which to monitor connection state. + * @param tag A tag for logging. + * @return A new [BleConnection] instance. + */ + fun create(scope: CoroutineScope, tag: String): BleConnection +} diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionState.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionState.kt new file mode 100644 index 000000000..a9f82c5f9 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnectionState.kt @@ -0,0 +1,32 @@ +/* + * 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.core.ble + +/** Represents the state of a BLE connection. */ +sealed class BleConnectionState { + /** The peripheral is disconnected. */ + object Disconnected : BleConnectionState() + + /** The peripheral is connecting. */ + object Connecting : BleConnectionState() + + /** The peripheral is connected. */ + object Connected : BleConnectionState() + + /** The peripheral is disconnecting. */ + object Disconnecting : BleConnectionState() +} diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleDevice.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleDevice.kt new file mode 100644 index 000000000..8c3278b26 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleDevice.kt @@ -0,0 +1,43 @@ +/* + * 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.core.ble + +import kotlinx.coroutines.flow.StateFlow + +/** Represents a BLE device. */ +interface BleDevice { + /** The device's name. */ + val name: String? + + /** The device's address. */ + val address: String + + /** The current connection state of the device. */ + val state: StateFlow + + /** Whether the device is bonded. */ + val isBonded: Boolean + + /** Whether the device is currently connected. */ + val isConnected: Boolean + + /** Reads the current RSSI value. */ + suspend fun readRssi(): Int + + /** Bond the device. */ + suspend fun bond() +} diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleRetry.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleRetry.kt similarity index 100% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/BleRetry.kt rename to core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleRetry.kt diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanner.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanner.kt new file mode 100644 index 000000000..d0b4b3ac2 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanner.kt @@ -0,0 +1,31 @@ +/* + * 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.core.ble + +import kotlinx.coroutines.flow.Flow +import kotlin.time.Duration + +/** A scanner for BLE devices. */ +interface BleScanner { + /** + * Scans for BLE devices. + * + * @param timeout The duration of the scan. + * @return A [Flow] of discovered [BleDevice]s. + */ + fun scan(timeout: Duration): Flow +} diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt new file mode 100644 index 000000000..d25e11618 --- /dev/null +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BluetoothRepository.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.core.ble + +import kotlinx.coroutines.flow.StateFlow + +/** Repository responsible for Bluetooth availability and bonding. */ +interface BluetoothRepository { + /** The current state of Bluetooth on the device. */ + val state: StateFlow + + /** Refreshes the Bluetooth state. */ + fun refreshState() + + /** Returns true if the given address is valid. */ + fun isValid(bleAddress: String): Boolean + + /** Returns true if the given address is bonded. */ + fun isBonded(address: String): Boolean + + /** Initiates bonding with the given device. */ + suspend fun bond(device: BleDevice) +} + +/** Represents the state of Bluetooth on the device. */ +data class BluetoothState( + /** True if the application has the required Bluetooth permissions. */ + val hasPermissions: Boolean = false, + + /** True if Bluetooth is enabled on the device. */ + val enabled: Boolean = false, + + /** A list of bonded devices. */ + val bondedDevices: List = emptyList(), +) diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/MeshtasticBleConstants.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleConstants.kt similarity index 100% rename from core/ble/src/main/kotlin/org/meshtastic/core/ble/MeshtasticBleConstants.kt rename to core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleConstants.kt diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleModule.kt b/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleModule.kt deleted file mode 100644 index 4970cfa89..000000000 --- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BleModule.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.core.ble - -import android.content.Context -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.di.CoroutineDispatchers -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object BleModule { - - @Provides - @Singleton - fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment = - NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true) - - @Provides - @Singleton - fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager = - CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope) - - @Provides - @Singleton - fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope = - CoroutineScope(SupervisorJob() + dispatchers.default) -} 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 a3f31f448..1e6d37f67 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 @@ -152,7 +152,6 @@ constructor( if (queueJob?.isActive == true) return queueJob = scope.handledLaunch { - Logger.d { "packet queueJob started" } try { while (serviceRepository.connectionState.value == ConnectionState.Connected) { val packet = queueMutex.withLock { queuedPackets.removeFirstOrNull() } ?: break diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts index 8245b887e..93f251c88 100644 --- a/core/service/build.gradle.kts +++ b/core/service/build.gradle.kts @@ -14,34 +14,44 @@ * 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.hilt) + alias(libs.plugins.meshtastic.kmp.library) + alias(libs.plugins.devtools.ksp) } -configure { - buildFeatures { aidl = true } - namespace = "org.meshtastic.core.service" +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.core.service" + androidResources.enable = false + } - testOptions { unitTests.isReturnDefaultValues = true } + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.data) + implementation(projects.core.database) + 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) + } + + commonTest.dependencies { + implementation(libs.junit) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.mockk) + implementation(libs.turbine) + } + } } -dependencies { - api(projects.core.api) - implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.model) - implementation(projects.core.prefs) - implementation(projects.core.proto) - implementation(libs.javax.inject) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kermit) - - testImplementation(libs.junit) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.mockk) - testImplementation(libs.turbine) -} +dependencies { add("kspAndroid", libs.hilt.compiler) } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt similarity index 100% rename from core/service/src/main/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt similarity index 100% rename from core/service/src/main/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidServiceRepository.kt diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceClient.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ServiceClient.kt similarity index 100% rename from core/service/src/main/kotlin/org/meshtastic/core/service/ServiceClient.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/ServiceClient.kt diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt similarity index 100% rename from core/service/src/main/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 1dfdc27b8..a25a6b8bb 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -19,7 +19,6 @@ 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) } @@ -27,8 +26,6 @@ configure { namespace = "org.meshtastic.core.ui" } dependencies { implementation(projects.core.common) - implementation(projects.core.barcode) - implementation(projects.core.nfc) implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.model) @@ -37,7 +34,6 @@ dependencies { implementation(projects.core.service) implementation(projects.core.resources) - implementation(libs.accompanist.permissions) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.compose.material3) diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ImportFab.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ImportFab.kt index e4852111c..e237a08d6 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ImportFab.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ImportFab.kt @@ -39,8 +39,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.barcode.rememberBarcodeScanner -import org.meshtastic.core.nfc.NfcScannerEffect import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.import_label @@ -60,6 +58,8 @@ import org.meshtastic.core.resources.url import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.QrCode2 import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider +import org.meshtastic.core.ui.util.LocalNfcScannerProvider import org.meshtastic.core.ui.util.openNfcSettings import org.meshtastic.proto.SharedContact @@ -98,17 +98,18 @@ fun MeshtasticImportFAB( var showNfcDisabledDialog by remember { mutableStateOf(false) } val context = LocalContext.current - val barcodeScanner = rememberBarcodeScanner(onResult = { contents -> contents?.toUri()?.let { onImport(it) } }) + val barcodeScanner = LocalBarcodeScannerProvider.current { contents -> contents?.toUri()?.let { onImport(it) } } + val nfcScanner = LocalNfcScannerProvider.current if (isNfcScanning) { - NfcScannerEffect( - onResult = { contents -> + nfcScanner( + { contents -> contents?.toUri()?.let { onImport(it) isNfcScanning = false } }, - onNfcDisabled = { + { isNfcScanning = false showNfcDisabledDialog = true }, diff --git a/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScanner.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/BarcodeScanner.kt similarity index 90% rename from core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScanner.kt rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/util/BarcodeScanner.kt index 6a16fc8d4..399917df0 100644 --- a/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScanner.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/BarcodeScanner.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.core.barcode +package org.meshtastic.core.ui.util interface BarcodeScanner { fun startScan() diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalAnalyticsIntroProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalAnalyticsIntroProvider.kt new file mode 100644 index 000000000..ae80c13a2 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalAnalyticsIntroProvider.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.core.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf + +val LocalAnalyticsIntroProvider = compositionLocalOf<@Composable () -> Unit> { {} } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalBarcodeScannerProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalBarcodeScannerProvider.kt new file mode 100644 index 000000000..79b02e2da --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalBarcodeScannerProvider.kt @@ -0,0 +1,31 @@ +/* + * 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 + +val LocalBarcodeScannerProvider = + compositionLocalOf<@Composable (onResult: (String?) -> Unit) -> BarcodeScanner> { + { + object : BarcodeScanner { + override fun startScan() { + // Default NO-OP + } + } + } + } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt new file mode 100644 index 000000000..837289bbe --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.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.core.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf + +val LocalNfcScannerProvider = + compositionLocalOf<@Composable (onResult: (String?) -> Unit, onNfcDisabled: () -> Unit) -> Unit> { { _, _ -> } } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt new file mode 100644 index 000000000..319755d42 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt @@ -0,0 +1,43 @@ +/* + * 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 + +/** + * Interface for providing a flavored MapView. This allows the map feature to be decoupled from specific map + * implementations (Google Maps vs osmdroid). + */ +interface MapViewProvider { + @Composable + fun MapView( + modifier: Modifier, + // We use Any here to avoid circular dependency with feature:map + viewModel: Any, + navigateToNodeDetails: (Int) -> Unit, + focusedNodeNum: Int? = null, + // Using List to avoid dependency on proto.Position if needed + nodeTracks: List? = null, + tracerouteOverlay: Any? = null, + tracerouteNodePositions: Map = emptyMap(), + onTracerouteMappableCountChanged: (Int, Int) -> Unit = { _, _ -> }, + ) +} + +val LocalMapViewProvider = compositionLocalOf { null } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt index af6df6cba..06a66baed 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransport.kt @@ -31,77 +31,63 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withTimeout import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic -import no.nordicsemi.kotlin.ble.client.android.CentralManager -import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority -import no.nordicsemi.kotlin.ble.client.android.Peripheral -import no.nordicsemi.kotlin.ble.core.ConnectionState -import no.nordicsemi.kotlin.ble.core.WriteType -import org.meshtastic.core.ble.BleConnection +import org.meshtastic.core.ble.AndroidBleService +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleConnectionState +import org.meshtastic.core.ble.BleDevice import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.ble.BleWriteType import org.meshtastic.core.ble.MeshtasticBleConstants.OTA_NOTIFY_CHARACTERISTIC import org.meshtastic.core.ble.MeshtasticBleConstants.OTA_SERVICE_UUID import org.meshtastic.core.ble.MeshtasticBleConstants.OTA_WRITE_CHARACTERISTIC import kotlin.time.Duration.Companion.seconds /** - * BLE transport implementation for ESP32 Unified OTA protocol. Uses Nordic Kotlin-BLE-Library for modern coroutine - * support. + * BLE transport implementation for ESP32 Unified OTA protocol. * * Service UUID: 4FAFC201-1FB5-459E-8FCC-C5C9C331914B * - OTA Characteristic (Write): 62ec0272-3ec5-11eb-b378-0242ac130005 * - TX Characteristic (Notify): 62ec0272-3ec5-11eb-b378-0242ac130003 */ class BleOtaTransport( - private val centralManager: CentralManager, + private val scanner: BleScanner, + connectionFactory: BleConnectionFactory, private val address: String, dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : UnifiedOtaProtocol { private val transportScope = CoroutineScope(SupervisorJob() + dispatcher) - private val bleConnection = BleConnection(centralManager, transportScope, "BLE OTA") + private val bleConnection = connectionFactory.create(transportScope, "BLE OTA") private var otaCharacteristic: RemoteCharacteristic? = null private val responseChannel = Channel(Channel.UNLIMITED) private var isConnected = false - /** - * Scan for the device by MAC address with retries. After reboot, the device needs time to come up in OTA mode. - * - * Note: We scan by address rather than service UUID because some ESP32 OTA bootloaders don't include the service - * UUID in their advertisement data - the service is only discoverable after connecting. We verify the OTA service - * exists after connection. - * - * ESP32 bootloaders may use the original MAC address OR increment the last byte by 1 for OTA mode, so we check both - * addresses. - */ - private suspend fun scanForOtaDevice(): Peripheral? { + /** Scan for the device by MAC address with retries. After reboot, the device needs time to come up in OTA mode. */ + private suspend fun scanForOtaDevice(): BleDevice? { // ESP32 OTA bootloader may use MAC address with last byte incremented by 1 val otaAddress = calculateOtaAddress(macAddress = address) val targetAddresses = setOf(address, otaAddress) Logger.i { "BLE OTA: Will match addresses: $targetAddresses" } - val scanner = BleScanner(centralManager) - repeat(SCAN_RETRY_COUNT) { attempt -> Logger.i { "BLE OTA: Scanning for device (attempt ${attempt + 1}/$SCAN_RETRY_COUNT)..." } - // Scan without service UUID filter - ESP32 OTA bootloader may not advertise the UUID - // Log all devices found during scan for debugging val foundDevices = mutableSetOf() - val peripheral = + val device = scanner .scan(SCAN_TIMEOUT) - .onEach { p -> - if (foundDevices.add(p.address)) { - Logger.d { "BLE OTA: Scan found device: ${p.address} (name=${p.name})" } + .onEach { d -> + if (foundDevices.add(d.address)) { + Logger.d { "BLE OTA: Scan found device: ${d.address} (name=${d.name})" } } } .firstOrNull { it.address in targetAddresses } - if (peripheral != null) { - Logger.i { "BLE OTA: Found target device at ${peripheral.address}" } - return peripheral + if (device != null) { + Logger.i { "BLE OTA: Found target device at ${device.address}" } + return device } Logger.w { "BLE OTA: Target addresses $targetAddresses not in ${foundDevices.size} devices found" } @@ -136,8 +122,7 @@ class BleOtaTransport( Logger.i { "BLE OTA: Connecting to $address using Nordic BLE Library..." } - // Scan for device by address - device must have rebooted into OTA mode - val p = + val device = scanForOtaDevice() ?: throw OtaProtocolException.ConnectionFailed( "Device not found at address $address. " + @@ -147,41 +132,39 @@ class BleOtaTransport( bleConnection.connectionState .onEach { state -> Logger.d { "BLE OTA: Connection state changed to $state" } - isConnected = state is ConnectionState.Connected + isConnected = state is BleConnectionState.Connected } .launchIn(transportScope) try { - val finalState = bleConnection.connectAndAwait(p, CONNECTION_TIMEOUT_MS) - if (finalState is ConnectionState.Disconnected) { - Logger.w { "BLE OTA: Failed to connect to ${p.address} (state=$finalState)" } - throw OtaProtocolException.ConnectionFailed("Failed to connect to device at address ${p.address}") + val finalState = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS) + if (finalState is BleConnectionState.Disconnected) { + Logger.w { "BLE OTA: Failed to connect to ${device.address} (state=$finalState)" } + throw OtaProtocolException.ConnectionFailed("Failed to connect to device at address ${device.address}") } } catch (@Suppress("SwallowedException") e: kotlinx.coroutines.TimeoutCancellationException) { - Logger.w { "BLE OTA: Timed out waiting to connect to ${p.address}. Error: ${e.message}" } - throw OtaProtocolException.Timeout("Timed out connecting to device at address ${p.address}") + Logger.w { "BLE OTA: Timed out waiting to connect to ${device.address}. Error: ${e.message}" } + throw OtaProtocolException.Timeout("Timed out connecting to device at address ${device.address}") } - Logger.i { "BLE OTA: Connected to ${p.address}, discovering services..." } - - // Increase connection priority for OTA - bleConnection.requestConnectionPriority(ConnectionPriority.HIGH) + Logger.i { "BLE OTA: Connected to ${device.address}, discovering services..." } // Discover services using our unified profile helper bleConnection.profile(OTA_SERVICE_UUID) { service -> + val androidService = (service as AndroidBleService).service val ota = - requireNotNull(service.characteristics.firstOrNull { it.uuid == OTA_WRITE_CHARACTERISTIC }) { + requireNotNull(androidService.characteristics.firstOrNull { it.uuid == OTA_WRITE_CHARACTERISTIC }) { "OTA characteristic not found" } val txChar = - requireNotNull(service.characteristics.firstOrNull { it.uuid == OTA_NOTIFY_CHARACTERISTIC }) { + requireNotNull(androidService.characteristics.firstOrNull { it.uuid == OTA_NOTIFY_CHARACTERISTIC }) { "TX characteristic not found" } otaCharacteristic = ota // Log negotiated MTU for diagnostics - val maxLen = bleConnection.maximumWriteValueLength(WriteType.WITHOUT_RESPONSE) + val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE) Logger.i { "BLE OTA: Service ready. Max write value length: $maxLen bytes" } // Enable notifications and collect responses @@ -211,13 +194,7 @@ class BleOtaTransport( } } - /** - * Initiates the OTA update by sending the size and hash. - * - * Note: If the start command is fragmented into multiple BLE packets, the protocol may send multiple responses - * (usually one ACK per packet followed by a final OK/ERASING). - */ - @Suppress("CyclomaticComplexMethod") + /** Initiates the OTA update by sending the size and hash. */ override suspend fun startOta( sizeBytes: Long, sha256Hash: String, @@ -233,7 +210,6 @@ class BleOtaTransport( responsesReceived++ when (val parsed = OtaResponse.parse(response)) { is OtaResponse.Ok -> { - // Only consider handshake complete after consuming all potential fragmented responses if (responsesReceived >= packetsSent) { handshakeComplete = true } @@ -258,14 +234,7 @@ class BleOtaTransport( } } - /** - * Streams the firmware data in chunks. - * - * Each chunk is potentially fragmented into multiple BLE packets based on the negotiated MTU. The transport ensures - * that every fragmented packet is acknowledged by the device before proceeding, preventing buffer overflows on the - * radio. - */ - @Suppress("CyclomaticComplexMethod") + /** Streams the firmware data in chunks. */ override suspend fun streamFirmware( data: ByteArray, chunkSize: Int, @@ -283,10 +252,10 @@ class BleOtaTransport( val currentChunkSize = minOf(chunkSize, remainingBytes) val chunk = data.copyOfRange(sentBytes, sentBytes + currentChunkSize) - // Write chunk (potentially fragmented into multiple BLE packets) - val packetsSentForChunk = writeData(chunk, WriteType.WITHOUT_RESPONSE) + // Write chunk + val packetsSentForChunk = writeData(chunk, BleWriteType.WITHOUT_RESPONSE) - // Wait for responses (The protocol expects one response per GATT write) + // Wait for responses val nextSentBytes = sentBytes + currentChunkSize repeat(packetsSentForChunk) { i -> val response = waitForResponse(ACK_TIMEOUT_MS) @@ -298,15 +267,10 @@ class BleOtaTransport( } is OtaResponse.Ok -> { - // OK indicates completion (usually on last packet of last chunk) if (nextSentBytes >= totalBytes && isLastPacketOfChunk) { sentBytes = nextSentBytes onProgress(1.0f) return@runCatching Unit - } else if (!isLastPacketOfChunk) { - // Intermediate OK might happen if the device treats packets as chunks - } else { - throw OtaProtocolException.TransferFailed("Premature OK received at offset $nextSentBytes") } } @@ -325,7 +289,6 @@ class BleOtaTransport( onProgress(sentBytes.toFloat() / totalBytes) } - // If we finished the loop without receiving OK, wait for it now (verification stage) val finalResponse = waitForResponse(VERIFICATION_TIMEOUT_MS) when (val parsed = OtaResponse.parse(finalResponse)) { is OtaResponse.Ok -> Unit @@ -348,16 +311,10 @@ class BleOtaTransport( private suspend fun sendCommand(command: OtaCommand): Int { val data = command.toString().toByteArray() - return writeData(data, WriteType.WITH_RESPONSE) + return writeData(data, BleWriteType.WITH_RESPONSE) } - /** - * Writes data to the OTA characteristic, fragmenting the data into multiple BLE packets if it exceeds the - * negotiated MTU (maximum write length). - * - * @return The number of packets sent. - */ - private suspend fun writeData(data: ByteArray, writeType: WriteType): Int { + private suspend fun writeData(data: ByteArray, writeType: BleWriteType): Int { val characteristic = otaCharacteristic ?: throw OtaProtocolException.ConnectionFailed("OTA characteristic not available") @@ -369,7 +326,14 @@ class BleOtaTransport( while (offset < data.size) { val chunkSize = minOf(data.size - offset, maxLen) val packet = data.copyOfRange(offset, offset + chunkSize) - characteristic.write(packet, writeType = writeType) + + val nordicWriteType = + when (writeType) { + BleWriteType.WITH_RESPONSE -> no.nordicsemi.kotlin.ble.core.WriteType.WITH_RESPONSE + BleWriteType.WITHOUT_RESPONSE -> no.nordicsemi.kotlin.ble.core.WriteType.WITHOUT_RESPONSE + } + + characteristic.write(packet, writeType = nordicWriteType) offset += chunkSize packetsSent++ } @@ -389,17 +353,14 @@ class BleOtaTransport( // Timeouts and retries private val SCAN_TIMEOUT = 10.seconds private const val CONNECTION_TIMEOUT_MS = 15_000L - private const val ERASING_TIMEOUT_MS = 60_000L // Flash erase can take a while + private const val ERASING_TIMEOUT_MS = 60_000L private const val ACK_TIMEOUT_MS = 10_000L private const val VERIFICATION_TIMEOUT_MS = 10_000L - // Reboot and scan retry configuration - // Device needs time to reboot into OTA mode after receiving the reboot command private const val REBOOT_DELAY_MS = 5_000L private const val SCAN_RETRY_COUNT = 3 private const val SCAN_RETRY_DELAY_MS = 2_000L - // Recommended chunk size for BLE const val RECOMMENDED_CHUNK_SIZE = 512 } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt index 20c4d4403..890c23a3e 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandler.kt @@ -26,8 +26,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import no.nordicsemi.kotlin.ble.client.android.CentralManager import org.jetbrains.compose.resources.getString +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleScanner import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware @@ -73,7 +74,8 @@ constructor( private val firmwareRetriever: FirmwareRetriever, private val radioController: RadioController, private val nodeRepository: NodeRepository, - private val centralManager: CentralManager, + private val bleScanner: BleScanner, + private val bleConnectionFactory: BleConnectionFactory, @ApplicationContext private val context: Context, ) : FirmwareUpdateHandler { @@ -101,7 +103,7 @@ constructor( hardware = hardware, updateState = updateState, firmwareUri = firmwareUri, - transportFactory = { BleOtaTransport(centralManager, address) }, + transportFactory = { BleOtaTransport(bleScanner, bleConnectionFactory, address) }, rebootMode = 1, connectionAttempts = 5, ) diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt index 8e33e18a4..a2c27579e 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportErrorTest.kt @@ -100,7 +100,9 @@ class BleOtaTransportErrorTest { } centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) try { transport.connect().getOrThrow() @@ -162,7 +164,9 @@ class BleOtaTransportErrorTest { } centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) try { transport.connect().getOrThrow() @@ -243,7 +247,9 @@ class BleOtaTransportErrorTest { } centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) try { transport.connect().getOrThrow() diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt index 2d80cea30..6dd37803b 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportMtuTest.kt @@ -80,7 +80,9 @@ class BleOtaTransportMtuTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) transport.connect().getOrThrow() diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt index d44678d98..407a2b4a7 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportNordicMockTest.kt @@ -144,7 +144,9 @@ class BleOtaTransportNordicMockTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) // 1. Connect val connectResult = transport.connect() diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt index 3b33ed5b6..1e71db220 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportServiceDiscoveryTest.kt @@ -96,7 +96,9 @@ class BleOtaTransportServiceDiscoveryTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) val result = transport.connect() assertTrue("Connect should fail when OTA service is missing", result.isFailure) @@ -135,7 +137,9 @@ class BleOtaTransportServiceDiscoveryTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) val result = transport.connect() assertTrue("Connect should fail when TX characteristic is missing", result.isFailure) @@ -148,7 +152,9 @@ class BleOtaTransportServiceDiscoveryTest { val centralManager = CentralManager.mock(mockEnvironment, scope = backgroundScope) // Don't simulate any peripherals — scan will find nothing - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) val result = transport.connect() assertTrue("Connect should fail when device is not found", result.isFailure) @@ -200,7 +206,9 @@ class BleOtaTransportServiceDiscoveryTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) val result = transport.connect() assertTrue("Connect should succeed: ${result.exceptionOrNull()}", result.isSuccess) diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt index cf581f77d..8d7e4a87f 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt @@ -103,7 +103,9 @@ class BleOtaTransportTest { centralManager.simulatePeripherals(listOf(otaPeripheral)) - val transport = BleOtaTransport(centralManager, address, testDispatcher) + val scanner = org.meshtastic.core.ble.AndroidBleScanner(centralManager) + val connectionFactory = org.meshtastic.core.ble.AndroidBleConnectionFactory(centralManager) + val transport = BleOtaTransport(scanner, connectionFactory, address, testDispatcher) // 1. Connect transport.connect().getOrThrow() diff --git a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index 62f586a53..23fb682da 100644 --- a/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/test/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -26,7 +26,6 @@ import io.mockk.mockkStatic import io.mockk.unmockkStatic import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import no.nordicsemi.kotlin.ble.client.android.CentralManager import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before @@ -45,12 +44,20 @@ class Esp32OtaUpdateHandlerTest { private val firmwareRetriever: FirmwareRetriever = mockk() private val radioController: RadioController = mockk() private val nodeRepository: NodeRepository = mockk() - private val centralManager: CentralManager = mockk() + private val bleScanner: org.meshtastic.core.ble.BleScanner = mockk() + private val bleConnectionFactory: org.meshtastic.core.ble.BleConnectionFactory = mockk() private val context: Context = mockk() private val contentResolver: ContentResolver = mockk() private val handler = - Esp32OtaUpdateHandler(firmwareRetriever, radioController, nodeRepository, centralManager, context) + Esp32OtaUpdateHandler( + firmwareRetriever, + radioController, + nodeRepository, + bleScanner, + bleConnectionFactory, + context, + ) @Before fun setUp() { diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index 026918527..bf7667a61 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -14,37 +14,61 @@ * 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.devtools.ksp) } -configure { - namespace = "org.meshtastic.feature.intro" - testOptions { unitTests { isIncludeAndroidResources = true } } +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.feature.intro" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } + + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.model) + implementation(projects.core.repository) + implementation(projects.core.ui) + implementation(projects.core.resources) + + implementation(libs.androidx.lifecycle.viewmodel.compose) + 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) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.navigation3.ui) + implementation(libs.hilt.android) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.mockk) + implementation(libs.robolectric) + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.androidx.test.core) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.compose.ui.test.junit4) + } + } } dependencies { - implementation(projects.core.resources) - implementation(projects.core.ui) - - implementation(libs.accompanist.permissions) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.navigation3.runtime) - implementation(libs.androidx.navigation3.ui) - - testImplementation(libs.junit) - testImplementation(libs.mockk) - testImplementation(libs.robolectric) - testImplementation(platform(libs.androidx.compose.bom)) - testImplementation(libs.androidx.test.core) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.androidx.compose.ui.test.junit4) + add("kspAndroid", libs.androidx.hilt.compiler) + add("kspAndroid", libs.hilt.compiler) } diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt similarity index 97% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt index 7a4215220..943818301 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt @@ -19,7 +19,6 @@ package org.meshtastic.feature.intro import android.Manifest import android.os.Build import androidx.compose.runtime.Composable -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay @@ -36,7 +35,7 @@ import com.google.accompanist.permissions.rememberPermissionState */ @OptIn(ExperimentalPermissionsApi::class) @Composable -fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel = hiltViewModel()) { +fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel) { val notificationPermissionState: PermissionState? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/BluetoothScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/BluetoothScreen.kt similarity index 100% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/BluetoothScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/BluetoothScreen.kt diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/CriticalAlertsScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/CriticalAlertsScreen.kt similarity index 100% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/CriticalAlertsScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/CriticalAlertsScreen.kt diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt similarity index 96% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt index 49f29ba6b..38500b7e6 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/FeatureUIData.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.intro import androidx.compose.ui.graphics.vector.ImageVector diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt similarity index 98% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt index ecdb44ae3..01e25a3bf 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroBottomBar.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.intro import androidx.compose.foundation.layout.PaddingValues diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt similarity index 93% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt index 05c82bdd0..38956e1c7 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt @@ -27,17 +27,6 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.MultiplePermissionsState import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted -import kotlinx.serialization.Serializable - -@Serializable data object Welcome : NavKey - -@Serializable data object Bluetooth : NavKey - -@Serializable data object Location : NavKey - -@Serializable data object Notifications : NavKey - -@Serializable data object CriticalAlerts : NavKey /** * Provides the navigation graph for the application introduction flow. The flow follows the hierarchy of necessity: diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt similarity index 98% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt index 7d7c3600c..2fd5aca68 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.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.intro import android.content.Context diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/LocationScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/LocationScreen.kt similarity index 100% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/LocationScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/LocationScreen.kt diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/NotificationsScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/NotificationsScreen.kt similarity index 100% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/NotificationsScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/NotificationsScreen.kt diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/PermissionScreenLayout.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/PermissionScreenLayout.kt similarity index 100% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/PermissionScreenLayout.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/PermissionScreenLayout.kt diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt similarity index 97% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt rename to feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt index 8a4843c87..b9943974f 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt +++ b/feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt @@ -49,6 +49,7 @@ import org.meshtastic.core.resources.meshtastic import org.meshtastic.core.resources.share_your_location_in_real_time import org.meshtastic.core.resources.stay_connected_anywhere import org.meshtastic.core.resources.track_and_share_locations +import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider /** * The initial welcome screen for the app introduction flow. It displays a brief overview of the app's key features. @@ -57,6 +58,7 @@ import org.meshtastic.core.resources.track_and_share_locations */ @Composable internal fun WelcomeScreen(onGetStarted: () -> Unit) { + val analyticsIntro = LocalAnalyticsIntroProvider.current val features = remember { listOf( FeatureUIData( @@ -109,7 +111,7 @@ internal fun WelcomeScreen(onGetStarted: () -> Unit) { Spacer(modifier = Modifier.height(16.dp)) } Spacer(modifier = Modifier.weight(1f)) - AnalyticsIntro() + analyticsIntro() } } } diff --git a/feature/intro/src/test/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt b/feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt similarity index 100% rename from feature/intro/src/test/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt rename to feature/intro/src/androidUnitTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavKeys.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavKeys.kt new file mode 100644 index 000000000..455c51317 --- /dev/null +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavKeys.kt @@ -0,0 +1,30 @@ +/* + * 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 + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable data object Welcome : NavKey + +@Serializable data object Bluetooth : NavKey + +@Serializable data object Location : NavKey + +@Serializable data object Notifications : NavKey + +@Serializable data object CriticalAlerts : NavKey diff --git a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt similarity index 90% rename from feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt rename to feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt index e76c007ed..96a6b933f 100644 --- a/feature/intro/src/main/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt +++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt @@ -18,12 +18,9 @@ package org.meshtastic.feature.intro import androidx.lifecycle.ViewModel import androidx.navigation3.runtime.NavKey -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject /** ViewModel for the app introduction flow. */ -@HiltViewModel -class IntroViewModel @Inject constructor() : ViewModel() { +open class IntroViewModel : ViewModel() { /** * Determines the next navigation key based on the current key and the state of permissions. The flow hierarchy is: diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index f8b445a04..d701a243b 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -14,61 +14,75 @@ * 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.devtools.ksp) } -configure { namespace = "org.meshtastic.feature.map" } +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.feature.map" + 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.model) + implementation(projects.core.navigation) + implementation(projects.core.prefs) + implementation(projects.core.proto) + implementation(projects.core.service) + implementation(projects.core.resources) + implementation(projects.core.ui) + implementation(projects.core.di) + + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.javax.inject) + } + + 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) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.annotation) + 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.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.navigation.common) + implementation(libs.androidx.savedstate.compose) + implementation(libs.androidx.savedstate.ktx) + implementation(libs.material) + implementation(libs.kermit) + implementation(libs.hilt.android) + } + + androidUnitTest.dependencies { + implementation(libs.junit) + implementation(libs.mockk) + implementation(libs.robolectric) + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.test.core) + } + } +} dependencies { - 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.prefs) - implementation(projects.core.proto) - implementation(projects.core.service) - implementation(projects.core.resources) - implementation(projects.core.ui) - implementation(projects.core.di) - - implementation(libs.androidx.datastore) - implementation(libs.androidx.datastore.preferences) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.annotation) - 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.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.navigation.common) - implementation(libs.androidx.savedstate.compose) - implementation(libs.androidx.savedstate.ktx) - implementation(libs.material) - implementation(libs.kermit) - - fdroidImplementation(libs.osmbonuspack) - fdroidImplementation(libs.osmdroid.android) - - googleImplementation(libs.location.services) - googleImplementation(libs.maps.compose) - googleImplementation(libs.maps.compose.utils) - googleImplementation(libs.maps.compose.widgets) - - testImplementation(libs.junit) - testImplementation(libs.mockk) - testImplementation(libs.robolectric) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.androidx.test.core) + add("kspAndroid", libs.androidx.hilt.compiler) + add("kspAndroid", libs.hilt.compiler) } diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapScreen.kt similarity index 86% rename from feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt rename to feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapScreen.kt index 2dcfcfdab..666ae7438 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/MapScreen.kt +++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapScreen.kt @@ -22,22 +22,22 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.map import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.core.ui.util.LocalMapViewProvider @Composable fun MapScreen( onClickNodeChip: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit, modifier: Modifier = Modifier, - mapViewModel: MapViewModel = hiltViewModel(), + viewModel: SharedMapViewModel, ) { - val ourNodeInfo by mapViewModel.ourNodeInfo.collectAsStateWithLifecycle() - val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle() + val ourNodeInfo by viewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by viewModel.isConnected.collectAsStateWithLifecycle() @Suppress("ViewModelForwarding") Scaffold( @@ -54,9 +54,9 @@ fun MapScreen( ) }, ) { paddingValues -> - MapView( + LocalMapViewProvider.current?.MapView( modifier = Modifier.fillMaxSize().padding(paddingValues), - mapViewModel = mapViewModel, + viewModel = viewModel, navigateToNodeDetails = navigateToNodeDetails, ) } diff --git a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MBTilesProviderTest.kt b/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MBTilesProviderTest.kt similarity index 100% rename from feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MBTilesProviderTest.kt rename to feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MBTilesProviderTest.kt diff --git a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt b/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt similarity index 100% rename from feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt rename to feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt similarity index 98% rename from feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt rename to feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 06037e880..a7caf78a9 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -45,7 +45,7 @@ import org.meshtastic.proto.Position import org.meshtastic.proto.Waypoint @Suppress("TooManyFunctions") -abstract class BaseMapViewModel( +open class BaseMapViewModel( protected val mapPrefs: MapPrefs, protected open val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, @@ -134,7 +134,8 @@ abstract class BaseMapViewModel( mapPrefs.setLastHeardTrackFilter(filter.seconds) } - abstract fun getUser(userId: String?): org.meshtastic.proto.User + open fun getUser(userId: String?) = + nodeRepository.getUser(userId ?: org.meshtastic.core.model.DataPacket.ID_BROADCAST) fun getNodeOrFallback(nodeNum: Int): Node = nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum) 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 new file mode 100644 index 000000000..df3787a31 --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt @@ -0,0 +1,32 @@ +/* + * 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 + +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( + mapPrefs: MapPrefs, + nodeRepository: NodeRepository, + packetRepository: PacketRepository, + radioController: RadioController, +) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapLayer.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapLayer.kt new file mode 100644 index 000000000..82572ef8d --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapLayer.kt @@ -0,0 +1,34 @@ +/* + * 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.model + +import kotlin.uuid.Uuid + +enum class LayerType { + KML, + GEOJSON, +} + +data class MapLayerItem( + val id: String = Uuid.random().toString(), + val name: String, + val uriString: String? = null, + val isVisible: Boolean = true, + val layerType: LayerType, + val isNetwork: Boolean = false, + val isRefreshing: Boolean = false, +) diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.kt similarity index 96% rename from feature/map/src/main/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.kt rename to feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.kt index 3e803c641..7a9bb6627 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/TracerouteOverlay.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.map.model data class TracerouteOverlay( diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 81104e76f..481737827 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -14,57 +14,76 @@ * 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.meshtastic.kmp.library) + alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.devtools.ksp) } -configure { namespace = "org.meshtastic.feature.messaging" } +kotlin { + @Suppress("UnstableApiUsage") + android { + namespace = "org.meshtastic.feature.messaging" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } + + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.domain) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(projects.core.prefs) + implementation(projects.core.proto) + implementation(projects.core.resources) + implementation(projects.core.service) + implementation(projects.core.ui) + + implementation(libs.androidx.lifecycle.viewmodel.compose) + 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) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.ui.text) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.lifecycle.runtime.compose) + 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 { + implementation(libs.junit) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.turbine) + } + + androidUnitTest.dependencies { + implementation(libs.mockk) + implementation(libs.androidx.work.testing) + implementation(libs.androidx.test.core) + implementation(libs.robolectric) + } + } +} dependencies { - implementation(projects.core.common) - implementation(projects.core.data) - implementation(projects.core.database) - implementation(projects.core.domain) - implementation(projects.core.model) - implementation(projects.core.navigation) - 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.activity.compose) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material3.adaptive) - implementation(libs.androidx.compose.material3.adaptive.layout) - implementation(libs.androidx.compose.material3.adaptive.navigation) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.paging.compose) - implementation(libs.kermit) - implementation(libs.androidx.work.runtime.ktx) - implementation(libs.androidx.hilt.work) - ksp(libs.androidx.hilt.compiler) - - debugImplementation(libs.androidx.compose.ui.test.manifest) - - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - androidTestImplementation(libs.androidx.test.ext.junit) - - testImplementation(libs.junit) - testImplementation(libs.mockk) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.turbine) - testImplementation(libs.androidx.work.testing) - testImplementation(libs.androidx.test.core) - testImplementation(libs.robolectric) + add("kspAndroid", libs.androidx.hilt.compiler) + add("kspAndroid", libs.hilt.compiler) } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/Message.kt similarity index 99% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/Message.kt index 4154e43df..74879870a 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -95,7 +95,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow 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 androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.CoroutineScope @@ -161,7 +160,7 @@ private const val ROUNDED_CORNER_PERCENT = 100 fun MessageScreen( contactKey: String, message: String, - viewModel: MessageViewModel = hiltViewModel(), + viewModel: MessageViewModel, navigateToNodeDetails: (Int) -> Unit, navigateToQuickChatOptions: () -> Unit, onNavigateBack: () -> Unit, diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt similarity index 98% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt index f63a8a101..9ddcb3ad6 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChat.kt +++ b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt @@ -61,7 +61,6 @@ import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.platform.LocalHapticFeedback 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.database.entity.QuickChatAction @@ -85,11 +84,7 @@ import org.meshtastic.core.ui.component.rememberDragDropState import org.meshtastic.core.ui.theme.AppTheme @Composable -fun QuickChatScreen( - modifier: Modifier = Modifier, - viewModel: QuickChatViewModel = hiltViewModel(), - onNavigateUp: () -> Unit, -) { +fun QuickChatScreen(modifier: Modifier = Modifier, viewModel: QuickChatViewModel, onNavigateUp: () -> Unit) { val actions by viewModel.quickChatActions.collectAsStateWithLifecycle() var showActionDialog by remember { mutableStateOf(null) } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/UnreadUiDefaults.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/UnreadUiDefaults.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/UnreadUiDefaults.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/UnreadUiDefaults.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageBubble.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageBubble.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageBubble.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageBubble.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt similarity index 96% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt index d7acde4dd..4181039f0 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt +++ b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt @@ -64,6 +64,8 @@ import org.meshtastic.proto.SharedContact @Composable fun AdaptiveContactsScreen( navController: NavHostController, + contactsViewModel: org.meshtastic.feature.messaging.ui.contact.ContactsViewModel, + messageViewModel: org.meshtastic.feature.messaging.MessageViewModel, scrollToTopEvents: Flow, sharedContactRequested: SharedContact?, requestChannelSet: ChannelSet?, @@ -138,6 +140,7 @@ fun AdaptiveContactsScreen( onHandleScannedUri = onHandleScannedUri, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, + viewModel = contactsViewModel, onClickNodeChip = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) { launchSingleTop = true @@ -160,6 +163,7 @@ fun AdaptiveContactsScreen( MessageScreen( contactKey = contactKey, message = if (contactKey == initialContactKey) initialMessage else "", + viewModel = messageViewModel, navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, navigateToQuickChatOptions = { navController.navigate(ContactsRoutes.QuickChat) }, onNavigateBack = handleBack, diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt similarity index 100% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt similarity index 99% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt index 82348cc07..3e77dc763 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt +++ b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems @@ -121,7 +120,7 @@ fun ContactsScreen( onHandleScannedUri: (Uri, onInvalid: () -> Unit) -> Unit, onClearSharedContactRequested: () -> Unit, onClearRequestChannelUrl: () -> Unit, - viewModel: ContactsViewModel = hiltViewModel(), + viewModel: ContactsViewModel, onClickNodeChip: (Int) -> Unit = {}, onNavigateToMessages: (String) -> Unit = {}, onNavigateToNodeDetails: (Int) -> Unit = {}, diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt similarity index 96% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt rename to feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt index 6e351ebed..33186e0cd 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt +++ b/feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.model.Contact @@ -52,7 +51,7 @@ import org.meshtastic.feature.messaging.ui.contact.ContactItem import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel @Composable -fun ShareScreen(viewModel: ContactsViewModel = hiltViewModel(), onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) { +fun ShareScreen(viewModel: ContactsViewModel, onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) { val contactList by viewModel.contactList.collectAsStateWithLifecycle() ShareScreen(contacts = contactList, onConfirm = onConfirm, onNavigateUp = onNavigateUp) diff --git a/feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt b/feature/messaging/src/androidUnitTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt similarity index 100% rename from feature/messaging/src/test/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt rename to feature/messaging/src/androidUnitTest/kotlin/org/meshtastic/feature/messaging/HomoglyphCharacterTransformTest.kt diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt similarity index 98% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt rename to feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index a991d1061..ed4b332f3 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -49,13 +48,9 @@ import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.repository.usecase.SendMessageUseCase import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class MessageViewModel -@Inject -constructor( +open class MessageViewModel( savedStateHandle: SavedStateHandle, private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt similarity index 86% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt rename to feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt index e6f0762c4..0c850fe86 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.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,22 +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.messaging import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.QuickChatActionRepository import org.meshtastic.core.database.entity.QuickChatAction import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed -import javax.inject.Inject -@HiltViewModel -class QuickChatViewModel @Inject constructor(private val quickChatActionRepository: QuickChatActionRepository) : - ViewModel() { +open class QuickChatViewModel(private val quickChatActionRepository: QuickChatActionRepository) : ViewModel() { val quickChatActions get() = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList()) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt similarity index 98% rename from feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt rename to feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index 595e4a1e4..961ff5566 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -39,13 +38,9 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ChannelSet -import javax.inject.Inject import kotlin.collections.map as collectionsMap -@HiltViewModel -class ContactsViewModel -@Inject -constructor( +open class ContactsViewModel( private val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, radioConfigRepository: RadioConfigRepository, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt index c42d57035..162af7350 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt @@ -52,7 +52,7 @@ import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Route import org.meshtastic.core.ui.theme.TracerouteColors -import org.meshtastic.feature.map.MapView +import org.meshtastic.core.ui.util.LocalMapViewProvider import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.proto.Position @@ -116,11 +116,13 @@ private fun TracerouteMapScaffold( }, ) { paddingValues -> Box(modifier = modifier.fillMaxSize().padding(paddingValues)) { - MapView( + LocalMapViewProvider.current?.MapView( + modifier = Modifier, + viewModel = Unit, navigateToNodeDetails = {}, tracerouteOverlay = overlay, tracerouteNodePositions = snapshotPositions, - onTracerouteMappableCountChanged = { shown, total -> + onTracerouteMappableCountChanged = { shown: Int, total: Int -> tracerouteNodesShown = shown tracerouteNodesTotal = total },