mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Migrate project to Kotlin Multiplatform (KMP) architecture (#4738)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
182ad933f4
commit
0ce322a0f5
163 changed files with 1837 additions and 877 deletions
163
AGENTS.md
163
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<UiState>` or `Flow<UiState>`.
|
||||
### 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.
|
||||
|
|
|
|||
11
README.md
11
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.intro
|
||||
package org.meshtastic.app.intro
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Any>?,
|
||||
tracerouteOverlay: Any?,
|
||||
tracerouteNodePositions: Map<Int, Any>,
|
||||
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<org.meshtastic.proto.Position>,
|
||||
tracerouteOverlay = tracerouteOverlay as? org.meshtastic.feature.map.model.TracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions as? Map<Int, org.meshtastic.proto.Position> ?: emptyMap(),
|
||||
onTracerouteMappableCountChanged = onTracerouteMappableCountChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
fun getMapViewProvider(): MapViewProvider = FdroidMapViewProvider()
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.database.Cursor
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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 {
|
||||
""
|
||||
},
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import co.touchlab.kermit.Logger
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourcePolicy
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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(
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.intro
|
||||
package org.meshtastic.app.intro
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
fun getMapViewProvider(): MapViewProvider = GoogleMapViewProvider()
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Any>?,
|
||||
tracerouteOverlay: Any?,
|
||||
tracerouteNodePositions: Map<Int, Any>,
|
||||
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<org.meshtastic.proto.Position>,
|
||||
tracerouteOverlay = tracerouteOverlay as? org.meshtastic.feature.map.model.TracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions as? Map<Int, org.meshtastic.proto.Position> ?: emptyMap(),
|
||||
onTracerouteMappableCountChanged = onTracerouteMappableCountChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.google.android.gms.maps.model.Tile
|
||||
|
|
@ -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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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(
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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)
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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")
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.uuid.Uuid
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
class CustomTileSource {
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.maps.android.clustering.ClusterItem
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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) {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
|
||||
|
|
@ -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<AndroidIntroViewModel>()
|
||||
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
Normal file
75
app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.native
|
||||
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
|
||||
import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
|
||||
import org.meshtastic.core.ble.AndroidBleConnectionFactory
|
||||
import org.meshtastic.core.ble.AndroidBleScanner
|
||||
import org.meshtastic.core.ble.AndroidBluetoothRepository
|
||||
import org.meshtastic.core.ble.BleConnection
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class BleModule {
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBleScanner(impl: AndroidBleScanner): BleScanner
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBluetoothRepository(impl: AndroidBluetoothRepository): BluetoothRepository
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBleConnectionFactory(impl: AndroidBleConnectionFactory): BleConnectionFactory
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment =
|
||||
NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope =
|
||||
CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager =
|
||||
CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope)
|
||||
|
||||
@Provides
|
||||
fun provideBleConnection(factory: BleConnectionFactory, coroutineScope: CoroutineScope): BleConnection =
|
||||
factory.create(coroutineScope, "BLE")
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.service.di
|
||||
package org.meshtastic.app.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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()
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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,
|
||||
)
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
|
|
@ -14,14 +14,14 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.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)
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
package org.meshtastic.app.messaging.domain.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
package org.meshtastic.app.messaging.domain.worker
|
||||
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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<AndroidContactsViewModel>()
|
||||
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
|
||||
|
||||
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<AndroidContactsViewModel>()
|
||||
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
|
||||
|
||||
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<ContactsRoutes.Share>().message
|
||||
val viewModel = hiltViewModel<AndroidContactsViewModel>()
|
||||
ShareScreen(
|
||||
viewModel = viewModel,
|
||||
onConfirm = {
|
||||
navController.navigate(ContactsRoutes.Messages(it, message)) {
|
||||
popUpTo<ContactsRoutes.Share> { inclusive = true }
|
||||
|
|
@ -102,6 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
|
|||
composable<ContactsRoutes.QuickChat>(
|
||||
deepLinks = listOf(navDeepLink<ContactsRoutes.QuickChat>(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
|
||||
) {
|
||||
QuickChatScreen(onNavigateUp = navController::navigateUp)
|
||||
val viewModel = hiltViewModel<AndroidQuickChatViewModel>()
|
||||
QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
|
||||
val viewModel = hiltViewModel<AndroidSharedMapViewModel>()
|
||||
MapScreen(
|
||||
viewModel = viewModel,
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
package org.meshtastic.app.messaging.domain.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ configure<LibraryExtension> {
|
|||
|
||||
dependencies {
|
||||
implementation(project(":core:resources"))
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,37 +15,55 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LibraryExtension> { 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) }
|
||||
|
|
|
|||
|
|
@ -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<Peripheral?>(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<BleDevice?>(replay = 1)
|
||||
override val deviceFlow: SharedFlow<BleDevice?> = _deviceFlow.asSharedFlow()
|
||||
|
||||
private val _connectionState = simpleSharedFlow<ConnectionState>()
|
||||
|
||||
/** A flow of [ConnectionState] changes for the current [peripheral]. */
|
||||
val connectionState: SharedFlow<ConnectionState> = _connectionState.asSharedFlow()
|
||||
private val _connectionState = simpleSharedFlow<BleConnectionState>()
|
||||
override val connectionState: SharedFlow<BleConnectionState> = _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 <T> profile(
|
||||
override suspend fun <T> 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<T>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Peripheral> = 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)
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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>(BleConnectionState.Disconnected)
|
||||
override val state: StateFlow<BleConnectionState> = _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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Peripheral> =
|
||||
if (filterBlock != null) {
|
||||
centralManager.scan(timeout, filterBlock)
|
||||
} else {
|
||||
centralManager.scan(timeout)
|
||||
}
|
||||
.distinctByPeripheral()
|
||||
.map { it.peripheral }
|
||||
override fun scan(timeout: Duration): Flow<BleDevice> =
|
||||
centralManager.scan(timeout).distinctByPeripheral().map { AndroidBleDevice(it.peripheral) }
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
|
|
@ -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<BluetoothState>
|
||||
get() = _state.asStateFlow()
|
||||
) : BluetoothRepository {
|
||||
private val _state = MutableStateFlow(BluetoothState(hasPermissions = true))
|
||||
override val state: StateFlow<BluetoothState> = _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<Peripheral> =
|
||||
private fun getBondedAppPeripherals(enabled: Boolean, hasPerms: Boolean): List<BleDevice> =
|
||||
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 =
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<BleDevice?>
|
||||
|
||||
/** A flow of [BleConnectionState] changes. */
|
||||
val connectionState: SharedFlow<BleConnectionState>
|
||||
|
||||
/** 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 <T> 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.
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<BleConnectionState>
|
||||
|
||||
/** 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()
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<BleDevice>
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.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<BluetoothState>
|
||||
|
||||
/** 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<BleDevice> = emptyList(),
|
||||
)
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,34 +14,44 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<LibraryExtension> {
|
||||
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) }
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue