From b3b38acc0ba8dc87b1d47cc3c0189d915de85439 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:48:10 -0500 Subject: [PATCH] feat: Migrate networking to Ktor and enhance multiplatform support (#4890) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/copilot-instructions.md | 2 + AGENTS.md | 2 + app/build.gradle.kts | 9 +- .../googleReleaseRuntimeClasspath.txt | 415 ------------------ app/proguard-rules.pro | 3 +- .../meshtastic/app/di/FDroidNetworkModule.kt | 15 - .../meshtastic/app/di/GoogleNetworkModule.kt | 30 -- .../kotlin/org/meshtastic/app/MainActivity.kt | 7 + .../org/meshtastic/app/di/NetworkModule.kt | 50 ++- .../meshtastic/app/di/KoinVerificationTest.kt | 2 - build-logic/convention/build.gradle.kts | 27 +- .../main/kotlin/AnalyticsConventionPlugin.kt | 22 +- ...droidApplicationComposeConventionPlugin.kt | 15 +- ...droidApplicationFlavorsConventionPlugin.kt | 17 +- .../AndroidLibraryComposeConventionPlugin.kt | 15 +- .../AndroidLibraryFlavorsConventionPlugin.kt | 17 +- .../kotlin/AndroidRoomConventionPlugin.kt | 15 +- .../main/kotlin/KmpFeatureConventionPlugin.kt | 16 +- .../kotlin/KmpJvmAndroidConventionPlugin.kt | 12 +- .../main/kotlin/KmpLibraryConventionPlugin.kt | 9 +- .../src/main/kotlin/KoinConventionPlugin.kt | 1 - .../meshtastic/buildlogic/AndroidCompose.kt | 18 +- .../kotlin/org/meshtastic/buildlogic/Dokka.kt | 30 +- .../meshtastic/buildlogic/FlavorResolution.kt | 66 ++- .../kotlin/org/meshtastic/buildlogic/Graph.kt | 145 +++--- .../meshtastic/buildlogic/KotlinAndroid.kt | 66 ++- .../kotlin/org/meshtastic/buildlogic/Kover.kt | 29 +- .../buildlogic/ProjectExtensions.kt | 31 +- build.gradle.kts | 14 - compose_compiler_config.conf | 3 - core/ble/build.gradle.kts | 2 - core/common/build.gradle.kts | 2 - core/data/build.gradle.kts | 22 +- core/navigation/build.gradle.kts | 2 +- core/network/build.gradle.kts | 4 - .../network/di/CoreNetworkAndroidModule.kt | 11 +- desktop/build.gradle.kts | 6 +- .../kotlin/org/meshtastic/desktop/Main.kt | 123 +++++- docs/roadmap.md | 6 +- feature/connections/build.gradle.kts | 2 +- feature/firmware/build.gradle.kts | 8 +- feature/intro/build.gradle.kts | 4 +- feature/map/build.gradle.kts | 3 +- feature/messaging/build.gradle.kts | 2 +- feature/node/build.gradle.kts | 6 +- feature/settings/build.gradle.kts | 6 - feature/widget/build.gradle.kts | 1 + gradle.properties | 6 - gradle/libs.versions.toml | 13 +- 49 files changed, 435 insertions(+), 897 deletions(-) delete mode 100644 app/dependencies/googleReleaseRuntimeClasspath.txt diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aae64c1a2..b3547a58c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -79,6 +79,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`. - **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. - **BLE:** All Bluetooth communication must route through `core:ble` using Kable. +- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`. +- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules. - **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available. - **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`. - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. diff --git a/AGENTS.md b/AGENTS.md index aae64c1a2..b3547a58c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`. - **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. - **BLE:** All Bluetooth communication must route through `core:ble` using Kable. +- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`. +- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules. - **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available. - **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`. - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8018c588..1d574b1b9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -251,17 +251,17 @@ dependencies { implementation(libs.androidx.lifecycle.process) implementation(libs.jetbrains.lifecycle.viewmodel.compose) implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.jetbrains.navigation3.runtime) implementation(libs.jetbrains.navigation3.ui) implementation(libs.androidx.paging.compose) - implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.android) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.coil.network.okhttp) + implementation(libs.ktor.client.logging) + implementation(libs.coil) + implementation(libs.coil.network.ktor3) implementation(libs.coil.svg) implementation(libs.androidx.core.splashscreen) implementation(libs.kotlinx.serialization.json) - implementation(libs.okhttp3.logging.interceptor) implementation(libs.usb.serial.android) implementation(libs.androidx.work.runtime.ktx) implementation(libs.koin.android) @@ -281,7 +281,6 @@ dependencies { 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) googleImplementation(libs.dd.sdk.android.rum) diff --git a/app/dependencies/googleReleaseRuntimeClasspath.txt b/app/dependencies/googleReleaseRuntimeClasspath.txt deleted file mode 100644 index c6b9b1427..000000000 --- a/app/dependencies/googleReleaseRuntimeClasspath.txt +++ /dev/null @@ -1,415 +0,0 @@ -androidx.activity:activity-compose:1.12.3 -androidx.activity:activity-ktx:1.12.3 -androidx.activity:activity:1.12.3 -androidx.annotation:annotation-experimental:1.5.1 -androidx.annotation:annotation-jvm:1.9.1 -androidx.annotation:annotation:1.9.1 -androidx.appcompat:appcompat-resources:1.7.1 -androidx.appcompat:appcompat:1.7.1 -androidx.arch.core:core-common:2.2.0 -androidx.arch.core:core-runtime:2.2.0 -androidx.autofill:autofill:1.0.0 -androidx.cardview:cardview:1.0.0 -androidx.collection:collection-jvm:1.5.0 -androidx.collection:collection-ktx:1.5.0 -androidx.collection:collection:1.5.0 -androidx.compose.animation:animation-android:1.11.0-alpha04 -androidx.compose.animation:animation-core-android:1.11.0-alpha04 -androidx.compose.animation:animation-core:1.11.0-alpha04 -androidx.compose.animation:animation:1.11.0-alpha04 -androidx.compose.foundation:foundation-android:1.11.0-alpha04 -androidx.compose.foundation:foundation-layout-android:1.11.0-alpha04 -androidx.compose.foundation:foundation-layout:1.11.0-alpha04 -androidx.compose.foundation:foundation:1.11.0-alpha04 -androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha07 -androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha07 -androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha07 -androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha07 -androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha07 -androidx.compose.material3.adaptive:adaptive:1.3.0-alpha07 -androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha13 -androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha13 -androidx.compose.material3:material3-android:1.5.0-alpha13 -androidx.compose.material3:material3:1.5.0-alpha13 -androidx.compose.material:material-android:1.11.0-alpha04 -androidx.compose.material:material-icons-core-android:1.7.8 -androidx.compose.material:material-icons-core:1.7.8 -androidx.compose.material:material-icons-extended-android:1.7.8 -androidx.compose.material:material-icons-extended:1.7.8 -androidx.compose.material:material-ripple-android:1.11.0-alpha04 -androidx.compose.material:material-ripple:1.11.0-alpha04 -androidx.compose.material:material:1.11.0-alpha04 -androidx.compose.runtime:runtime-android:1.11.0-alpha04 -androidx.compose.runtime:runtime-annotation-android:1.11.0-alpha04 -androidx.compose.runtime:runtime-annotation:1.11.0-alpha04 -androidx.compose.runtime:runtime-livedata:1.11.0-alpha04 -androidx.compose.runtime:runtime-retain-android:1.11.0-alpha04 -androidx.compose.runtime:runtime-retain:1.11.0-alpha04 -androidx.compose.runtime:runtime-saveable-android:1.11.0-alpha04 -androidx.compose.runtime:runtime-saveable:1.11.0-alpha04 -androidx.compose.runtime:runtime-tracing:1.11.0-alpha04 -androidx.compose.runtime:runtime:1.11.0-alpha04 -androidx.compose.ui:ui-android:1.11.0-alpha04 -androidx.compose.ui:ui-geometry-android:1.11.0-alpha04 -androidx.compose.ui:ui-geometry:1.11.0-alpha04 -androidx.compose.ui:ui-graphics-android:1.11.0-alpha04 -androidx.compose.ui:ui-graphics:1.11.0-alpha04 -androidx.compose.ui:ui-text-android:1.11.0-alpha04 -androidx.compose.ui:ui-text:1.11.0-alpha04 -androidx.compose.ui:ui-tooling-android:1.11.0-alpha04 -androidx.compose.ui:ui-tooling-data-android:1.11.0-alpha04 -androidx.compose.ui:ui-tooling-data:1.11.0-alpha04 -androidx.compose.ui:ui-tooling-preview-android:1.11.0-alpha04 -androidx.compose.ui:ui-tooling-preview:1.11.0-alpha04 -androidx.compose.ui:ui-tooling:1.11.0-alpha04 -androidx.compose.ui:ui-unit-android:1.11.0-alpha04 -androidx.compose.ui:ui-unit:1.11.0-alpha04 -androidx.compose.ui:ui-util-android:1.11.0-alpha04 -androidx.compose.ui:ui-util:1.11.0-alpha04 -androidx.compose.ui:ui:1.11.0-alpha04 -androidx.compose:compose-bom-alpha:2026.01.01 -androidx.compose:compose-bom:2026.01.00 -androidx.concurrent:concurrent-futures-ktx:1.1.0 -androidx.concurrent:concurrent-futures:1.1.0 -androidx.constraintlayout:constraintlayout-core:1.0.0 -androidx.constraintlayout:constraintlayout:2.1.0 -androidx.coordinatorlayout:coordinatorlayout:1.1.0 -androidx.core:core-ktx:1.17.0 -androidx.core:core-location-altitude-external-protobuf:1.0.0-beta01 -androidx.core:core-location-altitude-proto:1.0.0-beta01 -androidx.core:core-location-altitude:1.0.0-beta01 -androidx.core:core-splashscreen:1.2.0 -androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.17.0 -androidx.cursoradapter:cursoradapter:1.0.0 -androidx.customview:customview-poolingcontainer:1.0.0 -androidx.customview:customview:1.1.0 -androidx.databinding:viewbinding:8.13.2 -androidx.datastore:datastore-android:1.2.0 -androidx.datastore:datastore-core-android:1.2.0 -androidx.datastore:datastore-core-okio-jvm:1.2.0 -androidx.datastore:datastore-core-okio:1.2.0 -androidx.datastore:datastore-core:1.2.0 -androidx.datastore:datastore-preferences-android:1.2.0 -androidx.datastore:datastore-preferences-core-android:1.2.0 -androidx.datastore:datastore-preferences-core:1.2.0 -androidx.datastore:datastore-preferences-external-protobuf:1.2.0 -androidx.datastore:datastore-preferences-proto:1.2.0 -androidx.datastore:datastore-preferences:1.2.0 -androidx.datastore:datastore:1.2.0 -androidx.documentfile:documentfile:1.0.0 -androidx.drawerlayout:drawerlayout:1.1.1 -androidx.dynamicanimation:dynamicanimation:1.1.0 -androidx.emoji2:emoji2-emojipicker:1.6.0 -androidx.emoji2:emoji2-views-helper:1.6.0 -androidx.emoji2:emoji2:1.6.0 -androidx.exifinterface:exifinterface:1.4.1 -androidx.fragment:fragment-ktx:1.6.2 -androidx.fragment:fragment:1.6.2 -androidx.graphics:graphics-path:1.0.1 -androidx.graphics:graphics-shapes-android:1.0.1 -androidx.graphics:graphics-shapes:1.0.1 -androidx.hilt:hilt-common:1.3.0 -androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0 -androidx.hilt:hilt-lifecycle-viewmodel:1.3.0 -androidx.hilt:hilt-work:1.3.0 -androidx.interpolator:interpolator:1.0.0 -androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.10.0 -androidx.lifecycle:lifecycle-common-jvm:2.10.0 -androidx.lifecycle:lifecycle-common:2.10.0 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0 -androidx.lifecycle:lifecycle-livedata-core:2.10.0 -androidx.lifecycle:lifecycle-livedata-ktx:2.10.0 -androidx.lifecycle:lifecycle-livedata:2.10.0 -androidx.lifecycle:lifecycle-process:2.10.0 -androidx.lifecycle:lifecycle-runtime-android:2.10.0 -androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0 -androidx.lifecycle:lifecycle-runtime-compose:2.10.0 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0 -androidx.lifecycle:lifecycle-runtime-ktx:2.10.0 -androidx.lifecycle:lifecycle-runtime:2.10.0 -androidx.lifecycle:lifecycle-service:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-android:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0 -androidx.lifecycle:lifecycle-viewmodel:2.10.0 -androidx.loader:loader:1.0.0 -androidx.localbroadcastmanager:localbroadcastmanager:1.1.0 -androidx.metrics:metrics-performance:1.0.0-beta03 -androidx.navigation3:navigation3-runtime-android:1.0.0 -androidx.navigation3:navigation3-runtime:1.0.0 -androidx.navigation3:navigation3-ui-android:1.0.0 -androidx.navigation3:navigation3-ui:1.0.0 -androidx.navigation:navigation-common-android:2.9.7 -androidx.navigation:navigation-common:2.9.7 -androidx.navigation:navigation-compose-android:2.9.7 -androidx.navigation:navigation-compose:2.9.7 -androidx.navigation:navigation-fragment:2.9.7 -androidx.navigation:navigation-runtime-android:2.9.7 -androidx.navigation:navigation-runtime:2.9.7 -androidx.navigationevent:navigationevent-android:1.0.2 -androidx.navigationevent:navigationevent-compose-android:1.0.2 -androidx.navigationevent:navigationevent-compose:1.0.2 -androidx.navigationevent:navigationevent:1.0.2 -androidx.paging:paging-common-android:3.4.0 -androidx.paging:paging-common:3.4.0 -androidx.paging:paging-compose-android:3.4.0 -androidx.paging:paging-compose:3.4.0 -androidx.print:print:1.0.0 -androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta11 -androidx.privacysandbox.ads:ads-adservices:1.1.0-beta11 -androidx.profileinstaller:profileinstaller:1.4.1 -androidx.recyclerview:recyclerview:1.3.2 -androidx.resourceinspection:resourceinspection-annotation:1.0.1 -androidx.room:room-common-jvm:2.8.4 -androidx.room:room-common:2.8.4 -androidx.room:room-paging-android:2.8.4 -androidx.room:room-paging:2.8.4 -androidx.room:room-runtime-android:2.8.4 -androidx.room:room-runtime:2.8.4 -androidx.savedstate:savedstate-android:1.4.0 -androidx.savedstate:savedstate-compose-android:1.4.0 -androidx.savedstate:savedstate-compose:1.4.0 -androidx.savedstate:savedstate-ktx:1.4.0 -androidx.savedstate:savedstate:1.4.0 -androidx.slidingpanelayout:slidingpanelayout:1.2.0 -androidx.sqlite:sqlite-android:2.6.2 -androidx.sqlite:sqlite-framework-android:2.6.2 -androidx.sqlite:sqlite-framework:2.6.2 -androidx.sqlite:sqlite:2.6.2 -androidx.startup:startup-runtime:1.2.0 -androidx.tracing:tracing-ktx:1.2.0 -androidx.tracing:tracing-perfetto:1.0.1 -androidx.tracing:tracing:1.2.0 -androidx.transition:transition:1.6.0 -androidx.vectordrawable:vectordrawable-animated:1.1.0 -androidx.vectordrawable:vectordrawable:1.1.0 -androidx.versionedparcelable:versionedparcelable:1.1.1 -androidx.viewpager2:viewpager2:1.1.0-beta02 -androidx.viewpager:viewpager:1.0.0 -androidx.window:window-core-android:1.5.0 -androidx.window:window-core:1.5.0 -androidx.window:window:1.5.0 -androidx.work:work-runtime-ktx:2.11.1 -androidx.work:work-runtime:2.11.1 -co.touchlab:kermit-android:2.0.8 -co.touchlab:kermit-core-android:2.0.8 -co.touchlab:kermit-core:2.0.8 -co.touchlab:kermit:2.0.8 -com.caverock:androidsvg-aar:1.4 -com.datadoghq:dd-sdk-android-compose:3.6.0 -com.datadoghq:dd-sdk-android-core:3.6.0 -com.datadoghq:dd-sdk-android-internal:3.6.0 -com.datadoghq:dd-sdk-android-logs:3.6.0 -com.datadoghq:dd-sdk-android-okhttp:3.6.0 -com.datadoghq:dd-sdk-android-rum:3.6.0 -com.datadoghq:dd-sdk-android-session-replay-compose:3.6.0 -com.datadoghq:dd-sdk-android-session-replay:3.6.0 -com.datadoghq:dd-sdk-android-timber:3.6.0 -com.datadoghq:dd-sdk-android-trace-api:3.6.0 -com.datadoghq:dd-sdk-android-trace-internal:3.6.0 -com.datadoghq:dd-sdk-android-trace-otel:3.6.0 -com.datadoghq:dd-sdk-android-trace:3.6.0 -com.github.mik3y:usb-serial-for-android:3.10.0 -com.google.accompanist:accompanist-drawablepainter:0.37.3 -com.google.accompanist:accompanist-permissions:0.37.3 -com.google.android.datatransport:transport-api:3.2.0 -com.google.android.datatransport:transport-backend-cct:3.3.0 -com.google.android.datatransport:transport-runtime:3.3.0 -com.google.android.gms:play-services-ads-identifier:18.0.0 -com.google.android.gms:play-services-base:18.5.0 -com.google.android.gms:play-services-basement:18.9.0 -com.google.android.gms:play-services-location:21.3.0 -com.google.android.gms:play-services-maps:20.0.0 -com.google.android.gms:play-services-measurement-api:23.0.0 -com.google.android.gms:play-services-measurement-base:23.0.0 -com.google.android.gms:play-services-measurement-impl:23.0.0 -com.google.android.gms:play-services-measurement-sdk-api:23.0.0 -com.google.android.gms:play-services-measurement-sdk:23.0.0 -com.google.android.gms:play-services-measurement:23.0.0 -com.google.android.gms:play-services-stats:17.0.2 -com.google.android.gms:play-services-tasks:18.4.0 -com.google.android.material:material:1.13.0 -com.google.auto.value:auto-value-annotations:1.6.3 -com.google.code.findbugs:jsr305:3.0.2 -com.google.code.gson:gson:2.13.2 -com.google.dagger:dagger-lint-aar:2.59 -com.google.dagger:dagger:2.59 -com.google.dagger:hilt-android:2.59 -com.google.dagger:hilt-core:2.59 -com.google.errorprone:error_prone_annotations:2.41.0 -com.google.firebase:firebase-analytics:23.0.0 -com.google.firebase:firebase-annotations:17.0.0 -com.google.firebase:firebase-bom:34.8.0 -com.google.firebase:firebase-common:22.0.1 -com.google.firebase:firebase-components:19.0.0 -com.google.firebase:firebase-config-interop:16.0.1 -com.google.firebase:firebase-crashlytics:20.0.4 -com.google.firebase:firebase-datatransport:19.0.0 -com.google.firebase:firebase-encoders-json:18.0.1 -com.google.firebase:firebase-encoders-proto:16.0.0 -com.google.firebase:firebase-encoders:17.0.0 -com.google.firebase:firebase-installations-interop:17.2.0 -com.google.firebase:firebase-installations:19.0.1 -com.google.firebase:firebase-measurement-connector:20.0.1 -com.google.firebase:firebase-sessions:3.0.4 -com.google.guava:failureaccess:1.0.3 -com.google.guava:guava:33.5.0-android -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava -com.google.j2objc:j2objc-annotations:3.1 -com.google.maps.android:android-maps-utils:4.0.0 -com.google.maps.android:maps-compose-utils:8.0.0 -com.google.maps.android:maps-compose-widgets:8.0.0 -com.google.maps.android:maps-compose:8.0.0 -com.google.maps.android:maps-ktx:6.0.0 -com.google.maps.android:maps-utils-ktx:6.0.0 -com.google.re2j:re2j:1.7 -com.google.zxing:core:3.5.4 -com.jakewharton.timber:timber:5.0.1 -com.journeyapps:zxing-android-embedded:4.3.0 -com.lyft.kronos:kronos-android:0.0.1-alpha11 -com.lyft.kronos:kronos-java:0.0.1-alpha11 -com.mikepenz:aboutlibraries-compose-core-android:13.2.1 -com.mikepenz:aboutlibraries-compose-core:13.2.1 -com.mikepenz:aboutlibraries-compose-m3-android:13.2.1 -com.mikepenz:aboutlibraries-compose-m3:13.2.1 -com.mikepenz:aboutlibraries-core-android:13.2.1 -com.mikepenz:aboutlibraries-core:13.2.1 -com.mikepenz:multiplatform-markdown-renderer-android:0.39.2 -com.mikepenz:multiplatform-markdown-renderer-m3-android:0.39.2 -com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2 -com.mikepenz:multiplatform-markdown-renderer:0.39.2 -com.patrykandpatrick.vico:compose-android:3.0.0-beta.3 -com.patrykandpatrick.vico:compose-m2-android:3.0.0-beta.3 -com.patrykandpatrick.vico:compose-m2:3.0.0-beta.3 -com.patrykandpatrick.vico:compose-m3-android:3.0.0-beta.3 -com.patrykandpatrick.vico:compose-m3:3.0.0-beta.3 -com.patrykandpatrick.vico:compose:3.0.0-beta.3 -com.squareup.okhttp3:logging-interceptor:5.3.2 -com.squareup.okhttp3:okhttp-android:5.3.2 -com.squareup.okhttp3:okhttp:5.3.2 -com.squareup.okio:okio-jvm:3.16.4 -com.squareup.okio:okio:3.16.4 -com.squareup.wire:wire-runtime-jvm:5.2.1 -com.squareup.wire:wire-runtime:5.2.1 -io.coil-kt.coil3:coil-android:3.3.0 -io.coil-kt.coil3:coil-compose-android:3.3.0 -io.coil-kt.coil3:coil-compose-core-android:3.3.0 -io.coil-kt.coil3:coil-compose-core:3.3.0 -io.coil-kt.coil3:coil-compose:3.3.0 -io.coil-kt.coil3:coil-core-android:3.3.0 -io.coil-kt.coil3:coil-core:3.3.0 -io.coil-kt.coil3:coil-network-core-android:3.3.0 -io.coil-kt.coil3:coil-network-core:3.3.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 -io.coil-kt.coil3:coil-network-okhttp:3.3.0 -io.coil-kt.coil3:coil-svg-android:3.3.0 -io.coil-kt.coil3:coil-svg:3.3.0 -io.coil-kt.coil3:coil:3.3.0 -io.ktor:ktor-client-content-negotiation-jvm:3.4.0 -io.ktor:ktor-client-content-negotiation:3.4.0 -io.ktor:ktor-client-core-jvm:3.4.0 -io.ktor:ktor-client-core:3.4.0 -io.ktor:ktor-client-okhttp-jvm:3.4.0 -io.ktor:ktor-client-okhttp:3.4.0 -io.ktor:ktor-events-jvm:3.4.0 -io.ktor:ktor-events:3.4.0 -io.ktor:ktor-http-cio-jvm:3.4.0 -io.ktor:ktor-http-cio:3.4.0 -io.ktor:ktor-http-jvm:3.4.0 -io.ktor:ktor-http:3.4.0 -io.ktor:ktor-io-jvm:3.4.0 -io.ktor:ktor-io:3.4.0 -io.ktor:ktor-network-jvm:3.4.0 -io.ktor:ktor-network:3.4.0 -io.ktor:ktor-serialization-jvm:3.4.0 -io.ktor:ktor-serialization-kotlinx-json-jvm:3.4.0 -io.ktor:ktor-serialization-kotlinx-json:3.4.0 -io.ktor:ktor-serialization-kotlinx-jvm:3.4.0 -io.ktor:ktor-serialization-kotlinx:3.4.0 -io.ktor:ktor-serialization:3.4.0 -io.ktor:ktor-sse-jvm:3.4.0 -io.ktor:ktor-sse:3.4.0 -io.ktor:ktor-utils-jvm:3.4.0 -io.ktor:ktor-utils:3.4.0 -io.ktor:ktor-websocket-serialization-jvm:3.4.0 -io.ktor:ktor-websocket-serialization:3.4.0 -io.ktor:ktor-websockets-jvm:3.4.0 -io.ktor:ktor-websockets:3.4.0 -io.opentelemetry:opentelemetry-api:1.40.0 -io.opentelemetry:opentelemetry-context:1.40.0 -jakarta.inject:jakarta.inject-api:2.0.1 -javax.inject:javax.inject:1 -no.nordicsemi.android:dfu:2.10.1 -no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha12 -no.nordicsemi.kotlin.ble:client-core-android:2.0.0-alpha12 -no.nordicsemi.kotlin.ble:client-core:2.0.0-alpha12 -no.nordicsemi.kotlin.ble:core:2.0.0-alpha12 -org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5 -org.jctools:jctools-core:3.3.0 -org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6 -org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6 -org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6 -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6 -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6 -org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6 -org.jetbrains.androidx.savedstate:savedstate:1.3.6 -org.jetbrains.compose.animation:animation-core:1.10.0 -org.jetbrains.compose.animation:animation:1.10.0 -org.jetbrains.compose.annotation-internal:annotation:1.10.0 -org.jetbrains.compose.collection-internal:collection:1.10.0 -org.jetbrains.compose.components:components-resources-android:1.10.0 -org.jetbrains.compose.components:components-resources:1.10.0 -org.jetbrains.compose.foundation:foundation-layout:1.10.0 -org.jetbrains.compose.foundation:foundation:1.10.0 -org.jetbrains.compose.material3:material3:1.9.0 -org.jetbrains.compose.material:material-ripple:1.10.0 -org.jetbrains.compose.material:material:1.10.0 -org.jetbrains.compose.runtime:runtime-saveable:1.10.0 -org.jetbrains.compose.runtime:runtime:1.10.0 -org.jetbrains.compose.ui:ui-backhandler-android:1.9.1 -org.jetbrains.compose.ui:ui-backhandler:1.9.1 -org.jetbrains.compose.ui:ui-geometry:1.10.0 -org.jetbrains.compose.ui:ui-graphics:1.10.0 -org.jetbrains.compose.ui:ui-text:1.10.0 -org.jetbrains.compose.ui:ui-tooling-preview:1.10.0-rc02 -org.jetbrains.compose.ui:ui-unit:1.10.0 -org.jetbrains.compose.ui:ui-util:1.10.0 -org.jetbrains.compose.ui:ui:1.10.0 -org.jetbrains.kotlin:kotlin-bom:1.8.22 -org.jetbrains.kotlin:kotlin-parcelize-runtime:2.3.0 -org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.3.0 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21 -org.jetbrains.kotlin:kotlin-stdlib:2.3.0 -org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0 -org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2 -org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2 -org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1-0.6.x-compat -org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat -org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2 -org.jetbrains.kotlinx:kotlinx-io-core:0.8.2 -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-core:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0 -org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0 -org.jetbrains:annotations:23.0.0 -org.jetbrains:markdown-jvm:0.7.3 -org.jetbrains:markdown:0.7.3 -org.jspecify:jspecify:1.0.0 -org.slf4j:slf4j-api:2.0.17 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 732f4ae2d..4d6c3924e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -27,8 +27,7 @@ -keep class com.google.protobuf.** { *; } -keep class org.meshtastic.proto.** { *; } -# OkHttp --dontwarn okhttp3.internal.platform.** +# Networking -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt index 42f1f9a88..fba7a417f 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt @@ -16,11 +16,8 @@ */ package org.meshtastic.app.di -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import org.koin.core.annotation.Module import org.koin.core.annotation.Single -import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.model.NetworkDeviceHardware import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.network.service.ApiService @@ -28,18 +25,6 @@ import org.meshtastic.core.network.service.ApiService @Module class FDroidNetworkModule { - @Single - fun provideOkHttpClient(buildConfigProvider: BuildConfigProvider): OkHttpClient = OkHttpClient.Builder() - .addInterceptor( - interceptor = - HttpLoggingInterceptor().apply { - if (buildConfigProvider.isDebug) { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - }, - ) - .build() - @Single fun provideApiService(): ApiService = object : ApiService { override suspend fun getDeviceHardware(): List = diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt index 0e88cb0fe..eede9d6e3 100644 --- a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt +++ b/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt @@ -16,43 +16,13 @@ */ package org.meshtastic.app.di -import android.content.Context -import com.datadog.android.okhttp.DatadogEventListener -import com.datadog.android.okhttp.DatadogInterceptor -import okhttp3.Cache -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import org.koin.core.annotation.Module import org.koin.core.annotation.Single -import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.network.service.ApiService import org.meshtastic.core.network.service.ApiServiceImpl -import java.io.File @Module class GoogleNetworkModule { @Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl - - @Single - fun provideOkHttpClient(context: Context, buildConfigProvider: BuildConfigProvider): OkHttpClient = - OkHttpClient.Builder() - .cache( - cache = - Cache( - directory = File(context.applicationContext.cacheDir, "http_cache"), - maxSize = 50L * 1024L * 1024L, // 50 MiB - ), - ) - .addInterceptor( - interceptor = - HttpLoggingInterceptor().apply { - if (buildConfigProvider.isDebug) { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - }, - ) - .addInterceptor(interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build()) - .eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory()) - .build() } diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index c590d11a4..c191c576e 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -42,7 +42,10 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger +import coil3.ImageLoader +import coil3.compose.setSingletonImageLoaderFactory import kotlinx.coroutines.launch +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel @@ -106,6 +109,10 @@ class MainActivity : ComponentActivity() { } setContent { + // Bridge Koin-provided ImageLoader (with flavor-specific HttpClient, SVG, debug logger) + // to Coil's singleton so all AsyncImage composables use the custom configuration. + setSingletonImageLoaderFactory { get() } + val theme by model.theme.collectAsStateWithLifecycle() val dynamic = theme == MODE_DYNAMIC val dark = diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt index 7178f7426..cf32517a5 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt @@ -21,19 +21,21 @@ import android.content.Context import android.net.ConnectivityManager import android.net.nsd.NsdManager import coil3.ImageLoader +import coil3.annotation.ExperimentalCoilApi import coil3.disk.DiskCache import coil3.memory.MemoryCache -import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.network.ktor3.KtorNetworkFetcherFactory import coil3.request.crossfade import coil3.svg.SvgDecoder import coil3.util.DebugLogger import coil3.util.Logger import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.engine.android.Android import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import okhttp3.OkHttpClient import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.core.common.BuildConfigProvider @@ -52,26 +54,24 @@ class NetworkModule { fun provideNsdManager(application: Application): NsdManager = application.getSystemService(Context.NSD_SERVICE) as NsdManager + @OptIn(ExperimentalCoilApi::class) @Single fun provideImageLoader( - okHttpClient: OkHttpClient, + httpClient: HttpClient, application: Context, buildConfigProvider: BuildConfigProvider, - ): ImageLoader { - val sharedOkHttp = okHttpClient.newBuilder().build() - return ImageLoader.Builder(context = application) - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp })) - add(SvgDecoder.Factory(scaleToDensity = true)) - } - .memoryCache { - MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build() - } - .diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() } - .logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null) - .crossfade(enable = true) - .build() - } + ): ImageLoader = ImageLoader.Builder(context = application) + .components { + add(KtorNetworkFetcherFactory(httpClient = httpClient)) + add(SvgDecoder.Factory(scaleToDensity = true)) + } + .memoryCache { + MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build() + } + .diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() } + .logger(logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null) + .crossfade(enable = true) + .build() @Single fun provideJson(): Json = Json { @@ -80,9 +80,11 @@ class NetworkModule { } @Single - fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) { - engine { preconfigured = okHttpClient } - - install(plugin = ContentNegotiation) { json(json) } - } + fun provideHttpClient(json: Json, buildConfigProvider: BuildConfigProvider): HttpClient = + HttpClient(engineFactory = Android) { + install(plugin = ContentNegotiation) { json(json) } + if (buildConfigProvider.isDebug) { + install(plugin = Logging) { level = LogLevel.BODY } + } + } } diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt index d71c7dd9c..b5b183e0a 100644 --- a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt @@ -25,7 +25,6 @@ import androidx.work.WorkerParameters import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher -import okhttp3.OkHttpClient import org.junit.Test import org.koin.test.verify.definition import org.koin.test.verify.injectedParameters @@ -53,7 +52,6 @@ class KoinVerificationTest { NodeIdLookup::class, HttpClient::class, HttpClientEngine::class, - OkHttpClient::class, ), injections = injectedParameters( diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index f3ecc5591..858b0a708 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -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 @@ -32,11 +32,7 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - } -} +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } } dependencies { // This allows the use of the 'libs' type-safe accessor in the Kotlin source of the plugins @@ -78,17 +74,16 @@ spotless { target("src/*/kotlin/**/*.kt", "src/*/java/**/*.kt") targetExclude("**/build/**/*.kt") ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } - ktlint(libs.versions.ktlint.get()).setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path) + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path) licenseHeaderFile(rootProject.file("../config/spotless/copyright.kt")) } kotlinGradle { target("**/*.gradle.kts") ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } - ktlint(libs.versions.ktlint.get()).setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path) - licenseHeaderFile( - rootProject.file("../config/spotless/copyright.kts"), - "(^(?![\\/ ]\\*).*$)" - ) + ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath(rootProject.file("../config/spotless/.editorconfig").path) + licenseHeaderFile(rootProject.file("../config/spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)") } } @@ -98,12 +93,7 @@ detekt { buildUponDefaultConfig = true allRules = false baseline = file("detekt-baseline.xml") - source.setFrom( - files( - "src/main/java", - "src/main/kotlin", - ) - ) + source.setFrom(files("src/main/java", "src/main/kotlin")) } gradlePlugin { @@ -196,6 +186,5 @@ gradlePlugin { id = "meshtastic.root" implementationClass = "RootConventionPlugin" } - } } diff --git a/build-logic/convention/src/main/kotlin/AnalyticsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AnalyticsConventionPlugin.kt index 9b07a200c..edf2d794a 100644 --- a/build-logic/convention/src/main/kotlin/AnalyticsConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AnalyticsConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.datadog.gradle.plugin.DdExtension @@ -29,8 +28,8 @@ import org.meshtastic.buildlogic.libs import org.meshtastic.buildlogic.plugin /** - * Convention plugin for analytics (Google Services, Crashlytics, Datadog). - * Segregates these plugins to only affect the "google" flavor and disables their tasks for "fdroid". + * Convention plugin for analytics (Google Services, Crashlytics, Datadog). Segregates these plugins to only affect the + * "google" flavor and disables their tasks for "fdroid". */ class AnalyticsConventionPlugin : Plugin { override fun apply(target: Project) { @@ -50,7 +49,9 @@ class AnalyticsConventionPlugin : Plugin { // This avoids iterating all tasks with a generic filter and improves configuration performance. plugins.withId("com.google.gms.google-services") { tasks.configureEach { - if (name.contains("GoogleServices", ignoreCase = true) && name.contains("fdroid", ignoreCase = true)) { + if ( + name.contains("GoogleServices", ignoreCase = true) && name.contains("fdroid", ignoreCase = true) + ) { enabled = false } } @@ -66,10 +67,13 @@ class AnalyticsConventionPlugin : Plugin { plugins.withId("com.datadoghq.dd-sdk-android-gradle-plugin") { tasks.configureEach { - if ((name.contains("datadog", ignoreCase = true) || - name.contains("uploadMapping", ignoreCase = true) || - name.contains("buildId", ignoreCase = true)) && - name.contains("fdroid", ignoreCase = true)) { + if ( + ( + name.contains("datadog", ignoreCase = true) || + name.contains("uploadMapping", ignoreCase = true) || + name.contains("buildId", ignoreCase = true) + ) && name.contains("fdroid", ignoreCase = true) + ) { enabled = false } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 260b7a154..2ab9bef23 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import com.android.build.api.dsl.ApplicationExtension import org.gradle.api.Plugin import org.gradle.api.Project @@ -26,19 +25,17 @@ import org.meshtastic.buildlogic.plugin /** * Compose configuration for Android applications. - * - * Note: This has identical implementation to AndroidLibraryComposeConventionPlugin. - * Both use the same configureAndroidCompose() function which works with CommonExtension. - * Kept separate to maintain explicit intent in build.gradle.kts configuration despite duplication. + * + * Note: This has identical implementation to AndroidLibraryComposeConventionPlugin. Both use the same + * configureAndroidCompose() function which works with CommonExtension. Kept separate to maintain explicit intent in + * build.gradle.kts configuration despite duplication. */ class AndroidApplicationComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = libs.plugin("compose-compiler").get().pluginId) apply(plugin = libs.plugin("compose-multiplatform").get().pluginId) - extensions.configure { - configureAndroidCompose(this) - } + extensions.configure { configureAndroidCompose(this) } } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt index 9b8477b02..8b9e026c9 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import com.android.build.api.dsl.ApplicationExtension import org.gradle.api.Plugin import org.gradle.api.Project @@ -24,17 +23,13 @@ import org.meshtastic.buildlogic.configureFlavors /** * Flavor configuration for Android applications. * - * Optimization note: This is nearly identical to AndroidLibraryFlavorsConventionPlugin. - * The underlying configureFlavors() function already handles both ApplicationExtension and LibraryExtension. - * Could be consolidated into a single plugin accepting CommonExtension, but kept separate for now - * to maintain explicit intent in build.gradle.kts declarations. + * Optimization note: This is nearly identical to AndroidLibraryFlavorsConventionPlugin. The underlying + * configureFlavors() function already handles both ApplicationExtension and LibraryExtension. Could be consolidated + * into a single plugin accepting CommonExtension, but kept separate for now to maintain explicit intent in + * build.gradle.kts declarations. */ class AndroidApplicationFlavorsConventionPlugin : Plugin { override fun apply(target: Project) { - with(target) { - extensions.configure { - configureFlavors(this) - } - } + with(target) { extensions.configure { configureFlavors(this) } } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index df12e2bdf..7177b92ed 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import com.android.build.api.dsl.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project @@ -26,19 +25,17 @@ import org.meshtastic.buildlogic.plugin /** * Compose configuration for Android libraries. - * - * Note: This has identical implementation to AndroidApplicationComposeConventionPlugin. - * Both use the same configureAndroidCompose() function which works with CommonExtension. - * Kept separate to maintain explicit intent in build.gradle.kts configuration despite duplication. + * + * Note: This has identical implementation to AndroidApplicationComposeConventionPlugin. Both use the same + * configureAndroidCompose() function which works with CommonExtension. Kept separate to maintain explicit intent in + * build.gradle.kts configuration despite duplication. */ class AndroidLibraryComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = libs.plugin("compose-compiler").get().pluginId) apply(plugin = libs.plugin("compose-multiplatform").get().pluginId) - extensions.configure { - configureAndroidCompose(this) - } + extensions.configure { configureAndroidCompose(this) } } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryFlavorsConventionPlugin.kt index efcee3a6a..7dc9b5c5e 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryFlavorsConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryFlavorsConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import com.android.build.api.dsl.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project @@ -24,17 +23,13 @@ import org.meshtastic.buildlogic.configureFlavors /** * Flavor configuration for Android libraries. * - * Optimization note: This is nearly identical to AndroidApplicationFlavorsConventionPlugin. - * The underlying configureFlavors() function already handles both ApplicationExtension and LibraryExtension. - * Could be consolidated into a single plugin accepting CommonExtension, but kept separate for now - * to maintain explicit intent in build.gradle.kts declarations. + * Optimization note: This is nearly identical to AndroidApplicationFlavorsConventionPlugin. The underlying + * configureFlavors() function already handles both ApplicationExtension and LibraryExtension. Could be consolidated + * into a single plugin accepting CommonExtension, but kept separate for now to maintain explicit intent in + * build.gradle.kts declarations. */ class AndroidLibraryFlavorsConventionPlugin : Plugin { override fun apply(target: Project) { - with(target) { - extensions.configure { - configureFlavors(this) - } - } + with(target) { extensions.configure { configureFlavors(this) } } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt index 5f96b8175..7331390e2 100644 --- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import androidx.room3.gradle.RoomExtension import com.google.devtools.ksp.gradle.KspExtension import org.gradle.api.Plugin @@ -33,9 +32,7 @@ class AndroidRoomConventionPlugin : Plugin { apply(plugin = "androidx.room3") apply(plugin = "com.google.devtools.ksp") - extensions.configure { - arg("room.generateKotlin", "true") - } + extensions.configure { arg("room.generateKotlin", "true") } extensions.configure { // The schemas directory contains a schema file for each version of the Room database. @@ -50,13 +47,9 @@ class AndroidRoomConventionPlugin : Plugin { pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { extensions.configure { - sourceSets.getByName("commonMain").dependencies { - implementation(roomRuntime) - } - } - dependencies { - add("kspAndroid", roomCompiler) + sourceSets.getByName("commonMain").dependencies { implementation(roomRuntime) } } + dependencies { add("kspAndroid", roomCompiler) } } pluginManager.withPlugin("org.jetbrains.kotlin.android") { diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt index b2ee6bcd3..4bc4cb927 100644 --- a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply @@ -26,10 +25,9 @@ import org.meshtastic.buildlogic.libs /** * Convention plugin for KMP feature modules. * - * Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and - * [KoinConventionPlugin] and wires the common Compose / Lifecycle / Koin dependencies - * that every feature module needs. Feature `build.gradle.kts` files only declare - * their module-specific deps. + * Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and [KoinConventionPlugin] and wires the + * common Compose / Lifecycle / Koin dependencies that every feature module needs. Feature `build.gradle.kts` files only + * declare their module-specific deps. * * Modelled after the `AndroidFeatureImplConventionPlugin` pattern from * [Now in Android](https://github.com/android/nowinandroid). @@ -71,12 +69,8 @@ class KmpFeatureConventionPlugin : Plugin { implementation(libs.library("androidx-compose-ui-tooling-preview")) } - sourceSets.getByName("commonTest").dependencies { - implementation(project(":core:testing")) - } + sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) } } } } } - - diff --git a/build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt index 7255df416..ea905de6e 100644 --- a/build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpJvmAndroidConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,20 +14,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import org.gradle.api.Plugin import org.gradle.api.Project import org.meshtastic.buildlogic.configureJvmAndroidMainHierarchy /** - * Opt-in convention for KMP modules that intentionally share a `jvmAndroidMain` source set - * between the desktop JVM target and the Android target. + * Opt-in convention for KMP modules that intentionally share a `jvmAndroidMain` source set between the desktop JVM + * target and the Android target. */ class KmpJvmAndroidConventionPlugin : Plugin { override fun apply(target: Project) { - with(target) { - configureJvmAndroidMainHierarchy() - } + with(target) { configureJvmAndroidMainHierarchy() } } } - diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt index c0f055f7e..a8a77bcdf 100644 --- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import dev.mokkery.gradle.MokkeryGradleExtension import org.gradle.api.Plugin import org.gradle.api.Project @@ -23,6 +22,7 @@ import org.gradle.kotlin.dsl.configure import org.meshtastic.buildlogic.configureAndroidMarketplaceFallback import org.meshtastic.buildlogic.configureKmpTestDependencies import org.meshtastic.buildlogic.configureKotlinMultiplatform +import org.meshtastic.buildlogic.configureTestOptions import org.meshtastic.buildlogic.libs import org.meshtastic.buildlogic.plugin @@ -38,12 +38,11 @@ class KmpLibraryConventionPlugin : Plugin { apply(plugin = "meshtastic.kover") apply(plugin = libs.plugin("mokkery").get().pluginId) - extensions.configure { - stubs.allowConcreteClassInstantiation.set(true) - } + extensions.configure { stubs.allowConcreteClassInstantiation.set(true) } configureKotlinMultiplatform() configureKmpTestDependencies() + configureTestOptions() configureAndroidMarketplaceFallback() } } diff --git a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt index ea96ad569..9b832ce16 100644 --- a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/AndroidCompose.kt index a23ca91ab..40cbe83fa 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/AndroidCompose.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,22 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies -/** - * Configure Compose-specific options - */ -internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension, -) { - commonExtension.apply { - buildFeatures.compose = true - } +/** Configure Compose-specific options */ +internal fun Project.configureAndroidCompose(commonExtension: CommonExtension) { + commonExtension.apply { buildFeatures.compose = true } val hasAndroidTest = project.projectDir.resolve("src/androidTest").exists() dependencies { @@ -38,10 +31,9 @@ internal fun Project.configureAndroidCompose( if (hasAndroidTest) { "androidTestImplementation"(platform(bom)) } - "implementation"(libs.library("androidx-compose-ui-tooling")) + "debugImplementation"(libs.library("androidx-compose-ui-tooling")) "implementation"(libs.library("androidx-compose-runtime")) "runtimeOnly"(libs.library("androidx-compose-runtime-tracing")) - "debugImplementation"(libs.library("androidx-compose-ui-tooling")) "implementation"(libs.library("compose-multiplatform-runtime")) "implementation"(libs.library("compose-multiplatform-resources")) diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt index 2455c7ce1..12b80e956 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Dokka.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import org.gradle.api.Project @@ -38,16 +37,8 @@ fun Project.configureDokka() { } // Dokka 2.x requires each source file to belong to exactly one source set. - val baseSourceSets = listOf( - "main", - "commonMain", - "androidMain", - "jvmMain", - "jvmAndroidMain", - "fdroid", - "google", - "release" - ) + val baseSourceSets = + listOf("main", "commonMain", "androidMain", "jvmMain", "jvmAndroidMain", "fdroid", "google", "release") val isCoreSourceSet = name in baseSourceSets suppress.set(!isCoreSourceSet) @@ -59,8 +50,7 @@ fun Project.configureDokka() { // Standardized repo-root based source links localDirectory.set(project.projectDir) - val relativePath = - project.projectDir.relativeTo(rootProject.projectDir).path.replace("\\", "/") + val relativePath = project.projectDir.relativeTo(rootProject.projectDir).path.replace("\\", "/") remoteUrl.set(URI("https://github.com/meshtastic/Meshtastic-Android/blob/main/$relativePath")) remoteLineSuffix.set("#L") } @@ -68,20 +58,14 @@ fun Project.configureDokka() { } } -/** - * Configure Dokka aggregation in a way that is compatible with Gradle Isolated Projects. - */ +/** Configure Dokka aggregation in a way that is compatible with Gradle Isolated Projects. */ fun Project.configureDokkaAggregation() { extensions.configure { moduleName.set("Meshtastic App") - dokkaPublications.configureEach { - suppressInheritedMembers.set(true) - } + dokkaPublications.configureEach { suppressInheritedMembers.set(true) } } subprojects.forEach { subproject -> - subproject.pluginManager.withPlugin("org.jetbrains.dokka") { - dependencies.add("dokka", subproject) - } + subproject.pluginManager.withPlugin("org.jetbrains.dokka") { dependencies.add("dokka", subproject) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt index 620d0c830..c4c52cb9a 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,15 +14,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import com.android.build.api.attributes.ProductFlavorAttr import org.gradle.api.Project import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.AttributeDisambiguationRule +import org.gradle.api.attributes.MultipleCandidatesDetails +import javax.inject.Inject private const val LEGACY_MARKETPLACE_ATTRIBUTE_NAME = "marketplace" +/** + * Registers [AttributeDisambiguationRule]s so Gradle can pick a default product flavor when a consumer configuration + * (e.g. `androidHostTestRuntimeClasspath` from a KMP module) does not carry the marketplace flavor attribute, but the + * producer (e.g. `core:barcode`) publishes multiple flavor variants. + * + * This replaces the previous `afterEvaluate { configurations.configureEach { … } }` approach that stamped attributes on + * every resolvable Android configuration. Disambiguation rules fire during dependency resolution — not configuration + * time — so they are immune to KGP's lazy configuration creation order and fully compatible with Configuration Cache, + * Isolated Projects, and future Gradle/KGP changes. + * + * The default flavor is configurable via the `meshtastic.defaultMarketplace` Gradle property (defaults to the + * [MeshtasticFlavor] entry marked `default = true`, which is `google`). + */ internal fun Project.configureAndroidMarketplaceFallback() { val defaultMarketplace = providers @@ -30,27 +45,34 @@ internal fun Project.configureAndroidMarketplaceFallback() { .orElse(MeshtasticFlavor.entries.first { it.default }.name) .get() + // AGP publishes the typed ProductFlavorAttr on flavored variant configurations. val marketplaceAttr = ProductFlavorAttr.of(MeshtasticFlavor.fdroid.dimension.name) + dependencies.attributesSchema.attribute(marketplaceAttr) { + disambiguationRules.add(ProductFlavorDisambiguationRule::class.java) { params(defaultMarketplace) } + } + + // Some AGP versions also publish a plain String "marketplace" attribute. val legacyMarketplaceAttr = Attribute.of(LEGACY_MARKETPLACE_ATTRIBUTE_NAME, String::class.java) - - afterEvaluate { - configurations.configureEach { - if (!isCanBeResolved || isCanBeConsumed) return@configureEach - if (!name.contains("android", ignoreCase = true)) return@configureEach - if (attributes.getAttribute(marketplaceAttr) != null && attributes.getAttribute(legacyMarketplaceAttr) != null) { - return@configureEach - } - - // Prefer explicit flavor from configuration name; otherwise use configurable default. - val inferredMarketplace = - when { - name.contains(MeshtasticFlavor.fdroid.name, ignoreCase = true) -> MeshtasticFlavor.fdroid.name - name.contains(MeshtasticFlavor.google.name, ignoreCase = true) -> MeshtasticFlavor.google.name - else -> defaultMarketplace - } - - attributes.attribute(marketplaceAttr, objects.named(ProductFlavorAttr::class.java, inferredMarketplace)) - attributes.attribute(legacyMarketplaceAttr, inferredMarketplace) - } + dependencies.attributesSchema.attribute(legacyMarketplaceAttr) { + disambiguationRules.add(StringDisambiguationRule::class.java) { params(defaultMarketplace) } + } +} + +/** + * Selects the default marketplace flavor when Gradle encounters ambiguous [ProductFlavorAttr] candidates during + * variant-aware dependency resolution. + */ +internal abstract class ProductFlavorDisambiguationRule @Inject constructor(private val defaultFlavor: String) : + AttributeDisambiguationRule { + override fun execute(details: MultipleCandidatesDetails) { + details.candidateValues.find { it.name == defaultFlavor }?.let { details.closestMatch(it) } + } +} + +/** Selects the default marketplace for the legacy plain-String "marketplace" attribute. */ +internal abstract class StringDisambiguationRule @Inject constructor(private val defaultFlavor: String) : + AttributeDisambiguationRule { + override fun execute(details: MultipleCandidatesDetails) { + details.candidateValues.find { it == defaultFlavor }?.let { details.closestMatch(it) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt index 9279c9419..6e93053f3 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import org.gradle.api.DefaultTask @@ -35,9 +34,7 @@ import org.gradle.kotlin.dsl.withType import org.meshtastic.buildlogic.PluginType.Unknown import kotlin.text.RegexOption.DOT_MATCHES_ALL -/** - * Declaration order is important, as only the first match will be retained. - */ +/** Declaration order is important, as only the first match will be retained. */ internal enum class PluginType(val id: String, val ref: String, val style: String) { AndroidApplication( id = "meshtastic.android.application", @@ -89,56 +86,62 @@ internal enum class PluginType(val id: String, val ref: String, val style: Strin ref = "kmp-library", style = "fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000", ), - Unknown( - id = "?", - ref = "unknown", - style = "fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000", - ), + Unknown(id = "?", ref = "unknown", style = "fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000"), } -/** - * Optimized and Isolated Projects compatible graph configuration. - */ +/** Optimized and Isolated Projects compatible graph configuration. */ internal fun Project.configureGraphTasks() { if (!buildFile.exists()) return - val supportedConfigurations = providers.gradleProperty("graph.supportedConfigurations") - .map { it.split(",").toSet() } - .orElse(setOf("api", "implementation", "baselineProfile", "testedApks")) + val supportedConfigurations = + providers + .gradleProperty("graph.supportedConfigurations") + .map { it.split(",").toSet() } + .orElse(setOf("api", "implementation", "baselineProfile", "testedApks")) val targetProjectPath = path - val dumpTask = tasks.register("graphDump") { - projectPath.set(targetProjectPath) - - dependenciesData.set(providers.provider { - val deps = mutableMapOf>>() - val projectDeps = mutableSetOf>() - configurations.filter { it.name in supportedConfigurations.get() }.forEach { config -> - config.dependencies.withType().forEach { dep -> - projectDeps.add(config.name to dep.path) - } - } - deps[targetProjectPath] = projectDeps - deps - }) + val dumpTask = + tasks.register("graphDump") { + projectPath.set(targetProjectPath) - pluginsData.set(providers.provider { - val projectPlugins = mutableMapOf() - val type = when { - pluginManager.hasPlugin("meshtastic.android.application") || pluginManager.hasPlugin("meshtastic.android.application.compose") -> PluginType.AndroidApplication - targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication - pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature - targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature - else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown - } - projectPlugins[targetProjectPath] = type - projectPlugins - }) + dependenciesData.set( + providers.provider { + val deps = mutableMapOf>>() + val projectDeps = mutableSetOf>() + configurations + .filter { it.name in supportedConfigurations.get() } + .forEach { config -> + config.dependencies.withType().forEach { dep -> + projectDeps.add(config.name to dep.path) + } + } + deps[targetProjectPath] = projectDeps + deps + }, + ) - output.set(layout.buildDirectory.file("mermaid/graph.txt")) - legend.set(layout.buildDirectory.file("mermaid/legend.txt")) - } + pluginsData.set( + providers.provider { + val projectPlugins = mutableMapOf() + val type = + when { + pluginManager.hasPlugin("meshtastic.android.application") || + pluginManager.hasPlugin("meshtastic.android.application.compose") -> + PluginType.AndroidApplication + targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication + pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature + targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature + else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown + } + projectPlugins[targetProjectPath] = type + projectPlugins + }, + ) + + output.set(layout.buildDirectory.file("mermaid/graph.txt")) + legend.set(layout.buildDirectory.file("mermaid/legend.txt")) + } tasks.register("graphUpdate") { projectPath.set(targetProjectPath) @@ -151,20 +154,15 @@ internal fun Project.configureGraphTasks() { @CacheableTask private abstract class GraphDumpTask : DefaultTask() { - @get:Input - abstract val projectPath: Property + @get:Input abstract val projectPath: Property - @get:Input - abstract val dependenciesData: MapProperty>> + @get:Input abstract val dependenciesData: MapProperty>> - @get:Input - abstract val pluginsData: MapProperty + @get:Input abstract val pluginsData: MapProperty - @get:OutputFile - abstract val output: RegularFileProperty + @get:OutputFile abstract val output: RegularFileProperty - @get:OutputFile - abstract val legend: RegularFileProperty + @get:OutputFile abstract val legend: RegularFileProperty @TaskAction operator fun invoke() { @@ -177,17 +175,20 @@ private abstract class GraphDumpTask : DefaultTask() { val currentProject = projectPath.get() val projectPlugins = pluginsData.get() val projectDeps = dependenciesData.get()[currentProject] ?: emptySet() - - appendLine(" $currentProject[${currentProject.substringAfterLast(":")}]:::${projectPlugins[currentProject]?.ref}") - + + appendLine( + " $currentProject[${currentProject.substringAfterLast(":")}]:::${projectPlugins[currentProject]?.ref}", + ) + projectDeps.forEach { (config, depPath) -> - val link = when (config) { - "api" -> "-->" - else -> "-.->" - } + val link = + when (config) { + "api" -> "-->" + else -> "-.->" + } appendLine(" $currentProject $link $depPath") } - + appendLine() PluginType.entries.forEach { appendLine("classDef ${it.ref} ${it.style};") } } @@ -206,16 +207,17 @@ private abstract class GraphDumpTask : DefaultTask() { @CacheableTask private abstract class GraphUpdateTask : DefaultTask() { - @get:Input - abstract val projectPath: Property + @get:Input abstract val projectPath: Property + @get:InputFile @get:PathSensitive(NONE) abstract val input: RegularFileProperty + @get:InputFile @get:PathSensitive(NONE) abstract val legend: RegularFileProperty - @get:OutputFile - abstract val output: RegularFileProperty + + @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun update() { @@ -223,10 +225,11 @@ private abstract class GraphUpdateTask : DefaultTask() { if (!readme.exists()) return val mermaid = input.get().asFile.readText() val currentContent = readme.readText() - val newContent = currentContent.replace( - Regex(".*?", DOT_MATCHES_ALL), - "\n```mermaid\n$mermaid\n```\n" - ) + val newContent = + currentContent.replace( + Regex(".*?", DOT_MATCHES_ALL), + "\n```mermaid\n$mermaid\n```\n", + ) if (currentContent != newContent) { readme.writeText(newContent) } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt index a3e216b84..ff0eebe4c 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import com.android.build.api.dsl.ApplicationExtension @@ -35,12 +34,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -/** - * Configure base Kotlin with Android options - */ -internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension, -) { +/** Configure base Kotlin with Android options */ +internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) { val compileSdkVersion = configProperties.getProperty("COMPILE_SDK").toInt() val minSdkVersion = configProperties.getProperty("MIN_SDK").toInt() val targetSdkVersion = configProperties.getProperty("TARGET_SDK").toInt() @@ -49,7 +44,7 @@ internal fun Project.configureKotlinAndroid( compileSdk = compileSdkVersion defaultConfig.minSdk = minSdkVersion - + if (this is ApplicationExtension) { defaultConfig.targetSdk = targetSdkVersion } @@ -62,9 +57,7 @@ internal fun Project.configureKotlinAndroid( configureKotlin() } -/** - * Configure Kotlin Multiplatform options - */ +/** Configure Kotlin Multiplatform options */ internal fun Project.configureKotlinMultiplatform() { extensions.configure { // Standard KMP targets for Meshtastic @@ -80,7 +73,7 @@ internal fun Project.configureKotlinMultiplatform() { extensions.findByType()?.apply { compileSdk = configProperties.getProperty("COMPILE_SDK").toInt() minSdk = configProperties.getProperty("MIN_SDK").toInt() - + // Set the namespace automatically if not already set if (namespace == null) { val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".") @@ -96,8 +89,10 @@ internal fun Project.configureKotlinMultiplatform() { tasks.configureEach { val taskName = name.lowercase() if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) { - if (taskName.startsWith("link") && taskName.contains("test") || - taskName == "iosarm64test" || taskName == "iossimulatorarm64test" || + if ( + taskName.startsWith("link") && taskName.contains("test") || + taskName == "iosarm64test" || + taskName == "iossimulatorarm64test" || taskName.endsWith("testbinaries") ) { enabled = false @@ -109,22 +104,18 @@ internal fun Project.configureKotlinMultiplatform() { configureKotlin() } -/** - * Configure Mokkery for the project - */ +/** Configure Mokkery for the project */ internal fun Project.configureMokkery() { pluginManager.withPlugin(libs.plugin("mokkery").get().pluginId) { - extensions.configure { - stubs.allowConcreteClassInstantiation.set(true) - } + extensions.configure { stubs.allowConcreteClassInstantiation.set(true) } } } /** * Configure a shared `jvmAndroidMain` source set using Kotlin's hierarchy template DSL. * - * This is for modules that intentionally share JVM-only implementations between the desktop - * `jvm()` target and the Android target without hand-written `dependsOn` edges. + * This is for modules that intentionally share JVM-only implementations between the desktop `jvm()` target and the + * Android target without hand-written `dependsOn` edges. */ @OptIn(ExperimentalKotlinGradlePluginApi::class) internal fun Project.configureJvmAndroidMainHierarchy() { @@ -133,8 +124,7 @@ internal fun Project.configureJvmAndroidMainHierarchy() { common { group("jvmAndroid") { withCompilations { compilation -> - compilation.target.targetName == "android" || - compilation.target.targetName == "jvm" + compilation.target.targetName == "android" || compilation.target.targetName == "jvm" } } } @@ -142,9 +132,7 @@ internal fun Project.configureJvmAndroidMainHierarchy() { } } -/** - * Configure common test dependencies for KMP modules - */ +/** Configure common test dependencies for KMP modules */ internal fun Project.configureKmpTestDependencies() { extensions.configure { sourceSets.apply { @@ -155,7 +143,7 @@ internal fun Project.configureKmpTestDependencies() { implementation(libs.library("kotest-property")) implementation(libs.library("turbine")) } - + // Configure androidHostTest if it exists val androidHostTest = findByName("androidHostTest") androidHostTest?.dependencies { @@ -167,23 +155,17 @@ internal fun Project.configureKmpTestDependencies() { // Configure jvmTest if it exists val jvmTest = findByName("jvmTest") - jvmTest?.dependencies { - implementation(libs.library("kotest-runner-junit6")) - } + jvmTest?.dependencies { implementation(libs.library("kotest-runner-junit6")) } } } } -/** - * Configure base Kotlin options for JVM (non-Android) - */ +/** Configure base Kotlin options for JVM (non-Android) */ internal fun Project.configureKotlinJvm() { configureKotlin() } -/** - * Configure base Kotlin options - */ +/** Configure base Kotlin options */ private inline fun Project.configureKotlin() { extensions.configure { // Using Java 17 for better compatibility with consumers (e.g. plugins, older environments) @@ -203,7 +185,7 @@ private inline fun Project.configureKotlin() { "-Xexpect-actual-classes", "-Xcontext-parameters", "-Xannotation-default-target=param-property", - "-Xskip-prerelease-check" + "-Xskip-prerelease-check", ) } } @@ -212,10 +194,12 @@ private inline fun Project.configureKotlin() { } } + val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map { it.toBoolean() }.getOrElse(false) + tasks.withType().configureEach { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) - allWarningsAsErrors.set(false) + allWarningsAsErrors.set(warningsAsErrors) freeCompilerArgs.addAll( // Enable experimental coroutines APIs, including Flow "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", @@ -225,7 +209,7 @@ private inline fun Project.configureKotlin() { "-Xexpect-actual-classes", "-Xcontext-parameters", "-Xannotation-default-target=param-property", - "-Xskip-prerelease-check" + "-Xskip-prerelease-check", ) } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt index 20b542977..f0ad9daa9 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Kover.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension @@ -25,12 +24,8 @@ fun Project.configureKover() { extensions.configure { reports { total { - xml { - onCheck.set(true) - } - html { - onCheck.set(true) - } + xml { onCheck.set(true) } + html { onCheck.set(true) } } filters { excludes { @@ -42,16 +37,14 @@ fun Project.configureKover() { classes("*.R") classes("*.R$*") + // Exclude iOS compile-only stubs (no test execution on these targets) + classes("*NoopStubs*") + // Exclude UI components annotatedBy("*Preview") // Exclude declarations - annotatedBy( - "*.Module", - "*.Provides", - "*.Binds", - "*.Composable", - ) + annotatedBy("*.Module", "*.Provides", "*.Binds", "*.Composable") // Suppress generated code packages("koin_aggregated_deps") @@ -63,13 +56,11 @@ fun Project.configureKover() { } /** - * Configure Kover aggregation in a way that is compatible with Gradle Isolated Projects. - * Instead of blindly adding all subprojects, we only add those that have the Kover plugin applied. + * Configure Kover aggregation in a way that is compatible with Gradle Isolated Projects. Instead of blindly adding all + * subprojects, we only add those that have the Kover plugin applied. */ fun Project.configureKoverAggregation() { subprojects.forEach { subproject -> - subproject.pluginManager.withPlugin("org.jetbrains.kotlinx.kover") { - dependencies.add("kover", subproject) - } + subproject.pluginManager.withPlugin("org.jetbrains.kotlinx.kover") { dependencies.add("kover", subproject) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt index ac3169101..fec14941c 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.buildlogic import org.gradle.api.Project @@ -37,17 +36,13 @@ import java.util.Properties val Project.libs get(): VersionCatalog = extensions.getByType().named("libs") -fun VersionCatalog.library(alias: String): Provider = - findLibrary(alias).get() +fun VersionCatalog.library(alias: String): Provider = findLibrary(alias).get() -fun VersionCatalog.bundle(alias: String): Provider = - findBundle(alias).get() +fun VersionCatalog.bundle(alias: String): Provider = findBundle(alias).get() -fun VersionCatalog.plugin(alias: String): Provider = - findPlugin(alias).get() +fun VersionCatalog.plugin(alias: String): Provider = findPlugin(alias).get() -fun VersionCatalog.version(alias: String): String = - findVersion(alias).get().requiredVersion +fun VersionCatalog.version(alias: String): String = findVersion(alias).get().requiredVersion val Project.configProperties: Properties get() { @@ -59,19 +54,23 @@ val Project.configProperties: Properties return properties } -/** - * Configure common test options like parallel execution and logging. - */ +/** Configure common test options like parallel execution and logging. */ internal fun Project.configureTestOptions() { tasks.withType().configureEach { // Parallelize unit tests maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) maxHeapSize = "2g" + // Allow modules with no discovered tests to pass without failing the build + filter { isFailOnNoMatchingTests = false } // Show test results in the console - testLogging { - events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - } + testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) } + } + + // Gradle 9+ fails when test sources exist but no test classes are discovered (e.g. all + // tests are commented out). Disable to avoid breaking builds for modules with WIP tests. + tasks.withType().configureEach { + failOnNoDiscoveredTests.set(false) } // Configure test retry if the plugin is applied diff --git a/build.gradle.kts b/build.gradle.kts index eedaff862..c4c4955e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,8 +15,6 @@ * along with this program. If not, see . */ - - plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.kotlin.multiplatform.library) apply false @@ -26,7 +24,6 @@ plugins { alias(libs.plugins.devtools.ksp) apply false alias(libs.plugins.koin.compiler) apply false alias(libs.plugins.firebase.crashlytics) apply false - alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.room) apply false alias(libs.plugins.kotlin.android) apply false @@ -41,20 +38,9 @@ plugins { alias(libs.plugins.spotless) apply false alias(libs.plugins.dokka) alias(libs.plugins.test.retry) apply false - alias(libs.plugins.dependency.guard) apply false alias(libs.plugins.meshtastic.root) } - - - - dependencies { dokkaPlugin(libs.dokka.android.documentation.plugin) } - -subprojects { - tasks.withType { - failOnNoDiscoveredTests = false - } -} \ No newline at end of file diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf index 032dc04e0..cd953347c 100644 --- a/compose_compiler_config.conf +++ b/compose_compiler_config.conf @@ -18,8 +18,5 @@ okio.ByteString // Kotlin Immutable Collections kotlinx.collections.immutable.* -// Java Time -java.time.* - // External Libraries com.google.android.gms.maps.model.** diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts index b9299764d..0a9491740 100644 --- a/core/ble/build.gradle.kts +++ b/core/ble/build.gradle.kts @@ -46,8 +46,6 @@ kotlin { implementation(libs.androidx.lifecycle.runtime.ktx) } - jvmMain.dependencies {} - commonTest.dependencies { implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index f1e79df34..d753a0765 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -33,8 +33,6 @@ kotlin { sourceSets { commonMain.dependencies { - api(libs.aboutlibraries.core) - implementation(libs.aboutlibraries.compose.m3) implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index d032e36e9..b6198f99c 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) + id("meshtastic.kmp.jvm.android") id("meshtastic.koin") } @@ -51,21 +52,18 @@ kotlin { implementation(libs.kotlinx.collections.immutable) } + // Room / SQLite runtime shared between Android and Desktop JVM targets + val jvmAndroidMain by getting { + dependencies { + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.paging) + implementation(libs.androidx.sqlite.bundled) + } + } + androidMain.dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.location.altitude) - - // Needed because core:data references MeshtasticDatabase (supertype RoomDatabase) - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.paging) - implementation(libs.androidx.sqlite.bundled) - } - - jvmMain.dependencies { - // Room / SQLite runtime for JVM target - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.paging) - implementation(libs.androidx.sqlite.bundled) } commonTest.dependencies { diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 472fdf023..1abbead11 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -28,7 +28,7 @@ kotlin { commonMain.dependencies { implementation(projects.core.resources) implementation(libs.kotlinx.serialization.core) - implementation(libs.jetbrains.navigation3.runtime) + implementation(libs.jetbrains.navigation3.ui) } commonTest.dependencies { implementation(kotlin("test")) } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index adc6c1153..2fb26bfa8 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -61,10 +61,6 @@ kotlin { implementation(projects.core.ble) implementation(projects.core.prefs) implementation(libs.usb.serial.android) - implementation(libs.coil.network.okhttp) - implementation(libs.coil.svg) - implementation(libs.ktor.client.okhttp) - implementation(libs.okhttp3.logging.interceptor) } commonTest.dependencies { diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt index ab46023eb..e43f45108 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/di/CoreNetworkAndroidModule.kt @@ -16,18 +16,9 @@ */ package org.meshtastic.core.network.di -import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module -import org.koin.core.annotation.Single @Module @ComponentScan("org.meshtastic.core.network") -class CoreNetworkAndroidModule { - @Single - fun provideHttpClient(json: Json): HttpClient = HttpClient(OkHttp) { install(ContentNegotiation) { json(json) } } -} +class CoreNetworkAndroidModule diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 9bd384614..33d25bc22 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -121,6 +121,11 @@ dependencies { implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) + // Coil image loading (network + SVG decoding for device hardware images) + implementation(libs.coil) + implementation(libs.coil.network.ktor3) + implementation(libs.coil.svg) + // Core KMP modules (JVM variants) implementation(projects.core.common) implementation(projects.core.di) @@ -177,7 +182,6 @@ dependencies { implementation(libs.okio) // Ktor HttpClient (Java engine for JVM/Desktop) - implementation(libs.ktor.client.core) implementation(libs.ktor.client.java) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt index 8dd8d1fd3..2ebc050a3 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -17,6 +17,7 @@ package org.meshtastic.desktop import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -28,7 +29,16 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Notification @@ -41,11 +51,21 @@ import androidx.compose.ui.window.rememberWindowState import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberNavBackStack import co.touchlab.kermit.Logger +import coil3.ImageLoader +import coil3.compose.setSingletonImageLoaderFactory +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import coil3.network.ktor3.KtorNetworkFetcherFactory +import coil3.request.crossfade +import coil3.svg.SvgDecoder import kotlinx.coroutines.flow.first +import okio.Path.Companion.toPath +import org.jetbrains.skia.Image import org.koin.core.context.startKoin import org.meshtastic.core.common.util.MeshtasticUri import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig +import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.navigation.TopLevelDestination import org.meshtastic.core.service.MeshServiceOrchestrator import org.meshtastic.core.ui.theme.AppTheme @@ -73,6 +93,26 @@ import java.util.Locale */ private val LocalAppLocale = staticCompositionLocalOf { "" } +private const val MEMORY_CACHE_MAX_BYTES = 64L * 1024L * 1024L // 64 MiB +private const val DISK_CACHE_MAX_BYTES = 32L * 1024L * 1024L // 32 MiB + +/** + * Loads a [Painter] from a Java classpath resource path (e.g. `"icon.png"`). + * + * This replaces the deprecated `androidx.compose.ui.res.painterResource(String)` API. Desktop native-distribution icons + * (`.icns`, `.ico`) remain in `src/main/resources` for the packaging plugin; this helper reads the same directory at + * runtime. + */ +@Composable +private fun classpathPainterResource(path: String): Painter { + val bitmap: ImageBitmap = + remember(path) { + val bytes = Thread.currentThread().contextClassLoader!!.getResourceAsStream(path)!!.readAllBytes() + Image.makeFromEncoded(bytes).toComposeImageBitmap() + } + return remember(bitmap) { BitmapPainter(bitmap) } +} + @Suppress("LongMethod", "CyclomaticComplexMethod") fun main(args: Array) = application(exitProcessOnExit = false) { Logger.i { "Meshtastic Desktop — Starting" } @@ -130,7 +170,7 @@ fun main(args: Array) = application(exitProcessOnExit = false) { var isAppVisible by remember { mutableStateOf(true) } var isWindowReady by remember { mutableStateOf(false) } val trayState = rememberTrayState() - val appIcon = painterResource("icon.png") + val appIcon = classpathPainterResource("icon.png") val notificationManager = remember { koinApp.koin.get() } val desktopPrefs = remember { koinApp.koin.get() } @@ -188,14 +228,81 @@ fun main(args: Array) = application(exitProcessOnExit = false) { ) if (isWindowReady && isAppVisible) { + val backStack = + rememberNavBackStack(MeshtasticNavSavedStateConfig, TopLevelDestination.Connections.route as NavKey) + Window( onCloseRequest = { isAppVisible = false }, title = "Meshtastic Desktop", icon = appIcon, state = windowState, + onPreviewKeyEvent = { event -> + if (event.type != KeyEventType.KeyDown || !event.isMetaPressed) return@Window false + when { + // ⌘Q → Quit + event.key == Key.Q -> { + exitApplication() + true + } + // ⌘, → Settings + event.key == Key.Comma -> { + if ( + TopLevelDestination.Settings != TopLevelDestination.fromNavKey(backStack.lastOrNull()) + ) { + navigateTopLevel(backStack, TopLevelDestination.Settings.route) + } + true + } + // ⌘⇧T → Toggle theme + event.key == Key.T && event.isShiftPressed -> { + uiPrefs.setTheme(if (isDarkTheme) 1 else 2) + true + } + // ⌘1 → Conversations + event.key == Key.One -> { + navigateTopLevel(backStack, TopLevelDestination.Conversations.route) + true + } + // ⌘2 → Nodes + event.key == Key.Two -> { + navigateTopLevel(backStack, TopLevelDestination.Nodes.route) + true + } + // ⌘3 → Map + event.key == Key.Three -> { + navigateTopLevel(backStack, TopLevelDestination.Map.route) + true + } + // ⌘4 → Connections + event.key == Key.Four -> { + navigateTopLevel(backStack, TopLevelDestination.Connections.route) + true + } + // ⌘/ → About + event.key == Key.Slash -> { + backStack.add(SettingsRoutes.About) + true + } + else -> false + } + }, ) { - val backStack = - rememberNavBackStack(MeshtasticNavSavedStateConfig, TopLevelDestination.Connections.route as NavKey) + // Configure Coil ImageLoader for desktop with SVG decoding and network fetching. + // This is the desktop equivalent of the Android app's NetworkModule.provideImageLoader(). + setSingletonImageLoaderFactory { context -> + val cacheDir = System.getProperty("user.home") + "/.meshtastic/image_cache" + ImageLoader.Builder(context) + .components { + add(KtorNetworkFetcherFactory()) + add(SvgDecoder.Factory()) + } + .memoryCache { MemoryCache.Builder().maxSizeBytes(MEMORY_CACHE_MAX_BYTES).build() } + .diskCache { + DiskCache.Builder().directory(cacheDir.toPath()).maxSizeBytes(DISK_CACHE_MAX_BYTES).build() + } + .crossfade(true) + .build() + } // Providing localePref via a staticCompositionLocalOf forces the entire subtree to // recompose when the locale changes — CMP Resources' rememberResourceEnvironment then @@ -207,3 +314,11 @@ fun main(args: Array) = application(exitProcessOnExit = false) { } } } + +/** Replaces the backstack with a single top-level destination route. */ +private fun navigateTopLevel(backStack: MutableList, route: NavKey) { + backStack.add(route) + while (backStack.size > 1) { + backStack.removeAt(0) + } +} diff --git a/docs/roadmap.md b/docs/roadmap.md index b97be24c1..40737b0f7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -> Last updated: 2026-03-17 +> Last updated: 2026-03-23 Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md). @@ -42,7 +42,7 @@ These items address structural gaps identified in the March 2026 architecture re - Test navigation flows end-to-end 2. **Tier 2: Polish (High Priority)** - Additional desktop-specific settings polish - - ✅ **MenuBar integration** and Keyboard shortcuts + - ✅ **Keyboard shortcuts** via `onPreviewKeyEvent` (MenuBar removed) - Window management - State persistence 3. **Tier 3: Advanced (Nice-to-have)** @@ -74,7 +74,7 @@ These items address structural gaps identified in the March 2026 architecture re | Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) | | Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) | | Notifications | ✅ Desktop native notifications with system tray icon support | -| MenuBar | ✅ Done — Native application menu bar with File/View menus | +| MenuBar | ✅ Removed — replaced with `onPreviewKeyEvent` keyboard shortcuts (⌘Q, ⌘,, ⌘⇧T, ⌘1-4, ⌘/) | | About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) | | Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) | diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 66c51bab7..b96836c28 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -45,7 +45,7 @@ kotlin { implementation(projects.core.network) implementation(projects.feature.settings) - implementation(libs.jetbrains.navigation3.runtime) + implementation(libs.jetbrains.navigation3.ui) } androidMain.dependencies { implementation(libs.usb.serial.android) } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 724178ba8..967b1d6f2 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -32,7 +32,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.jetbrains.navigation3.runtime) implementation(libs.jetbrains.navigation3.ui) implementation(projects.core.ble) implementation(projects.core.common) @@ -48,19 +47,18 @@ kotlin { implementation(projects.core.resources) implementation(projects.core.ui) + implementation(libs.coil) implementation(libs.kable.core) implementation(libs.kotlinx.collections.immutable) implementation(libs.ktor.client.core) + implementation(libs.markdown.renderer) + implementation(libs.markdown.renderer.m3) } androidMain.dependencies { implementation(libs.androidx.appcompat) implementation(libs.nordic.dfu) - implementation(libs.coil) - implementation(libs.coil.network.okhttp) implementation(libs.markdown.renderer.android) - implementation(libs.markdown.renderer.m3) - implementation(libs.markdown.renderer) } val androidHostTest by getting { diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index 6a3f9f5a0..fca91b056 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -36,11 +36,9 @@ kotlin { implementation(projects.core.ui) implementation(projects.core.resources) - implementation(libs.jetbrains.navigation3.runtime) + implementation(libs.jetbrains.navigation3.ui) } - androidMain.dependencies { implementation(libs.jetbrains.navigation3.ui) } - androidUnitTest.dependencies { implementation(libs.junit) implementation(libs.robolectric) diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index c89cbf92f..0ab6d1e33 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -29,9 +29,8 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.jetbrains.navigation3.runtime) implementation(libs.jetbrains.navigation3.ui) - implementation(projects.core.common) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.datastore) diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 7b245ac08..b517434d2 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -42,7 +42,7 @@ kotlin { implementation(projects.core.service) implementation(projects.core.ui) - implementation(libs.jetbrains.navigation3.runtime) + implementation(libs.jetbrains.navigation3.ui) implementation(libs.jetbrains.navigationevent.compose) implementation(libs.androidx.paging.common) implementation(libs.androidx.paging.compose) diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 222a87222..56c06606a 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -48,12 +48,11 @@ kotlin { implementation(projects.core.di) implementation(projects.feature.map) - implementation(libs.jetbrains.navigation3.runtime) + implementation(libs.jetbrains.navigation3.ui) implementation(libs.kotlinx.collections.immutable) implementation(libs.markdown.renderer) implementation(libs.markdown.renderer.m3) implementation(libs.vico.compose) - implementation(libs.vico.compose.m2) implementation(libs.vico.compose.m3) // JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold) @@ -65,10 +64,7 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.appcompat) - implementation(libs.coil) implementation(libs.markdown.renderer.android) - implementation(libs.markdown.renderer.m3) - implementation(libs.markdown.renderer) } androidUnitTest.dependencies { diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index df6267427..e98b068d1 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -47,7 +47,6 @@ kotlin { implementation(libs.kotlinx.collections.immutable) implementation(libs.aboutlibraries.compose.m3) - implementation(libs.jetbrains.navigation3.runtime) implementation(libs.jetbrains.navigation3.ui) } @@ -55,11 +54,6 @@ kotlin { implementation(projects.core.barcode) implementation(projects.core.nfc) implementation(libs.androidx.appcompat) - - implementation(libs.coil) - implementation(libs.markdown.renderer.android) - implementation(libs.markdown.renderer.m3) - implementation(libs.markdown.renderer) } androidUnitTest.dependencies { diff --git a/feature/widget/build.gradle.kts b/feature/widget/build.gradle.kts index 42768e685..a11e4ee7d 100644 --- a/feature/widget/build.gradle.kts +++ b/feature/widget/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.core.resources) implementation(projects.core.repository) + implementation(libs.androidx.compose.ui) // LocalConfiguration, LocalDensity implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.material3) implementation(libs.androidx.glance.preview) diff --git a/gradle.properties b/gradle.properties index 7b81ee712..8e67ce164 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,12 +11,7 @@ # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true #Sat Feb 28 21:28:07 CST 2026 -android.enableJetifier=false -android.enableR8.fullMode=true android.experimental.lint.analysisPerComponent=true -android.nonTransitiveRClass=true -android.useAndroidX=true -dependency.analysis.print.build.health=true enableComposeCompilerMetrics=false enableComposeCompilerReports=false kotlin.code.style=official @@ -26,7 +21,6 @@ ksp.incremental=true ksp.incremental.classpath=true ksp.incremental.intermodule=true ksp.run.in.process=true -ksp.useKSP2=true org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configureondemand=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 120cd13ec..e52ac1ea0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -98,9 +98,6 @@ jetbrains-lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifec jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" } jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" } jetbrains-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" } -# JetBrains Navigation 3 currently publishes `navigation3-ui` (no separate `navigation3-runtime` artifact). -# Both `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same coordinate. -jetbrains-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } jetbrains-navigationevent-compose = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" } androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" } @@ -178,12 +175,12 @@ kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } # Networking +ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } -ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version = "5.3.2" } # Testing androidx-test-core = { module = "androidx.test:core", version = "1.7.0" } @@ -204,11 +201,10 @@ aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } -coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } +coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" } dd-sdk-android-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "dd-sdk-android" } dd-sdk-android-logs = { module = "com.datadoghq:dd-sdk-android-logs", version.ref = "dd-sdk-android" } -dd-sdk-android-okhttp = { module = "com.datadoghq:dd-sdk-android-okhttp", version.ref = "dd-sdk-android" } dd-sdk-android-rum = { module = "com.datadoghq:dd-sdk-android-rum", version.ref = "dd-sdk-android" } dd-sdk-android-timber = { module = "com.datadoghq:dd-sdk-android-timber", version.ref = "dd-sdk-android" } dd-sdk-android-trace = { module = "com.datadoghq:dd-sdk-android-trace", version.ref = "dd-sdk-android" } @@ -233,7 +229,6 @@ kermit = { module = "co.touchlab:kermit", version = "2.1.0" } usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version = "3.10.0" } vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" } -vico-compose-m2 = { group = "com.patrykandpatrick.vico", name = "compose-m2", version.ref = "vico" } vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } # Build Logic @@ -283,7 +278,6 @@ secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugi # Firebase firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" } -firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.2" } # Other aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } @@ -306,7 +300,6 @@ meshtastic-android-library-compose = { id = "meshtastic.android.library.compose" meshtastic-android-library-flavors = { id = "meshtastic.android.library.flavors" } meshtastic-android-lint = { id = "meshtastic.android.lint" } meshtastic-android-room = { id = "meshtastic.android.room" } -meshtastic-android-test = { id = "meshtastic.android.test" } meshtastic-detekt = { id = "meshtastic.detekt" } meshtastic-koin = { id = "meshtastic.koin" } meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" }