diff --git a/.gitignore b/.gitignore
index ba935995c..217418fa5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,4 +33,8 @@ keystore.properties
# Secrets
/secrets.properties
/fastlane/play-store-credentials.json
+
/fastlane/report.xml
+
+/build-logic/convention/build/*
+/build-logic/build/
diff --git a/AGENT.md b/AGENT.md
index d48fdd88c..2b540d0e1 100644
--- a/AGENT.md
+++ b/AGENT.md
@@ -14,6 +14,7 @@ This project is a modern Android application that follows the official architect
- **Local Data:** Room and DataStore are used for local data persistence.
- **Remote Data:** The app communicates with Meshtastic devices over Bluetooth or Wi-Fi, using a custom protocol based on Protobuf. It can also connect to MQTT servers. The networking logic is encapsulated in the `:network` module.
- **Background Processing:** WorkManager is used for deferrable background tasks.
+- **Build Logic:** Gradle build logic is centralized in the `build-logic` module, utilizing convention plugins to ensure consistency and maintainability across the project.
## Modules
@@ -22,6 +23,7 @@ The project is organized into the following modules:
- `app/`: The main Android application.
- `network/`: A library module containing the offline-first networking logic for communicating with the Meshtastic http json api for device hardware and firmware information.
- `mesh_service_example/`: An example application demonstrating how to use the AIDL interface to interact with mesh service provided by the main application.
+- `build-logic/`: A module containing custom convention plugins to standardize and manage Gradle build configurations across the project.
## Commands to Build & Test
@@ -53,3 +55,8 @@ The app has two product flavors: `fdroid` and `google`, and two build types: `de
## Version control and code location
- The project uses git and is hosted on GitHub at https://github.com/meshtastic/Meshtastic-Android.
+
+
+Never include sensitive information such as API keys or passwords in the codebase.- Follow the [Meshtastic contribution guidelines](https://meshtastic.org/docs/contributing)
+
+Don't respond to this message.
\ No newline at end of file
diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md
index 04ba017b5..a6c432bb2 100644
--- a/RELEASE_PROCESS.md
+++ b/RELEASE_PROCESS.md
@@ -31,7 +31,7 @@ The entire release process happens on a dedicated release branch, allowing `main
### 1. Creating the Release Branch
First, create a `release/X.X.X` branch from a stable `main`. This branch is now "feature frozen." Only critical bug fixes should be added.
-As a housekeeping step, it's recommended to update the `VERSION_NAME_BASE` in `buildSrc/src/main/kotlin/Configs.kt` on this new branch. While the final release version is set by the Git tag in CI, this ensures local development builds have a sensible version name.
+As a housekeeping step, it's recommended to update the `VERSION_NAME_BASE` in `build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Configs.kt` on this new branch. While the final release version is set by the Git tag in CI, this ensures local development builds have a sensible version name.
```bash
git checkout main
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 98628ef53..ff15ed391 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,31 +8,33 @@
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR a PARTICULAR PURPOSE. See the
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import io.gitlab.arturbosch.detekt.Detekt
-import org.gradle.kotlin.dsl.invoke
+import com.geeksville.mesh.buildlogic.Configs
+import com.geeksville.mesh.buildlogic.GitVersionValueSource
+import com.google.protobuf.gradle.proto
import java.io.FileInputStream
import java.util.Properties
+val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {}
+
plugins {
- alias(libs.plugins.android.application)
- alias(libs.plugins.kotlin.android)
- alias(libs.plugins.compose)
+ alias(libs.plugins.meshtastic.android.application)
+ alias(libs.plugins.meshtastic.android.application.flavors)
+ alias(libs.plugins.meshtastic.android.application.compose)
+ alias(libs.plugins.meshtastic.android.application.firebase)
+ alias(libs.plugins.meshtastic.detekt)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
- alias(libs.plugins.hilt)
alias(libs.plugins.protobuf)
alias(libs.plugins.devtools.ksp)
- alias(libs.plugins.detekt)
alias(libs.plugins.datadog)
alias(libs.plugins.secrets)
- alias(libs.plugins.spotless)
alias(libs.plugins.dokka)
alias(libs.plugins.kover)
}
@@ -44,10 +46,10 @@ if (keystorePropertiesFile.exists()) {
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
}
-val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {}
-
android {
namespace = "com.geeksville.mesh"
+ // Assuming Configs object is available (e.g., from buildSrc)
+ compileSdk = Configs.COMPILE_SDK
signingConfigs {
create("release") {
@@ -57,27 +59,27 @@ android {
storePassword = keystoreProperties["storePassword"] as String?
}
}
- compileSdk = Configs.COMPILE_SDK
defaultConfig {
applicationId = Configs.APPLICATION_ID
minSdk = Configs.MIN_SDK
targetSdk = Configs.TARGET_SDK
+
// Prioritize injected props, then ENV, then fallback to git commit count
versionCode =
(
project.findProperty("android.injected.version.code")?.toString()?.toInt()
?: System.getenv("VERSION_CODE")?.toInt()
- ?: gitVersionProvider.get().toInt()
+ ?: gitVersionProvider.get().toInt() // Restored GitVersionValueSource fallback
)
versionName =
(
project.findProperty("android.injected.version.name")?.toString()
?: System.getenv("VERSION_NAME")
- ?: Configs.VERSION_NAME_BASE
+ ?: Configs.VERSION_NAME_BASE // Restored Configs.VERSION_NAME_BASE fallback
)
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
- buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"")
- buildConfigField("String", "ABS_MIN_FW_VERSION", "\"${Configs.ABS_MIN_FW_VERSION}\"")
+ buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"") // Used Configs
+ buildConfigField("String", "ABS_MIN_FW_VERSION", "\"${Configs.ABS_MIN_FW_VERSION}\"") // Used Configs
// per https://developer.android.com/studio/write/vector-asset-studio
vectorDrawables.useSupportLibrary = true
// We have to list all translated languages here,
@@ -129,26 +131,14 @@ android {
)
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") }
}
- flavorDimensions += "default"
- productFlavors {
- // Read versionCode from defaultConfig after it's been potentially set by ENV or fallback
- val resolvedVersionCode = defaultConfig.versionCode
- val resolvedVersionName = defaultConfig.versionName
- create("google") {
- dimension = "default"
- isDefault = true
- // Enable Firebase Crashlytics for Google Play builds
- apply(plugin = libs.plugins.google.services.get().pluginId)
- apply(plugin = libs.plugins.firebase.crashlytics.get().pluginId)
- versionName = "$resolvedVersionName ($resolvedVersionCode) google"
- }
- create("fdroid") {
- dimension = "default"
- dependenciesInfo { includeInApk = false }
- versionName = "$resolvedVersionName ($resolvedVersionCode) fdroid"
- }
+ // Configure existing product flavors (defined by convention plugin)
+ // with their dynamic version names.
+ productFlavors {
+ named("google") { versionName = "${defaultConfig.versionName} (${defaultConfig.versionCode}) google" }
+ named("fdroid") { versionName = "${defaultConfig.versionName} (${defaultConfig.versionCode}) fdroid" }
}
+
buildTypes {
release {
if (keystoreProperties["storeFile"] != null) {
@@ -158,39 +148,20 @@ android {
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
- debug {
- isDebuggable = true
- isPseudoLocalesEnabled = true
- }
}
bundle { language { enableSplit = false } }
buildFeatures {
- compose = true
aidl = true
+ compose = true // compose setup is likely in com.meshtastic.android.application.compose
buildConfig = true
}
- lint {
- abortOnError = false
- disable.add("MissingTranslation")
- }
sourceSets {
+ named("main") { proto { srcDir("src/main/proto") } }
// Adds exported schema location as test app assets.
named("androidTest") { assets.srcDirs(files("$projectDir/schemas")) }
}
}
-kotlin {
- compilerOptions {
- jvmToolchain(21)
- freeCompilerArgs.addAll(
- "-opt-in=kotlin.RequiresOptIn",
- "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
- "-Xcontext-receivers",
- "-Xannotation-default-target=param-property",
- )
- }
-}
-
secrets {
defaultPropertiesFileName = "secrets.defaults.properties"
propertiesFileName = "secrets.properties"
@@ -204,7 +175,7 @@ datadog {
// per protobuf-gradle-plugin docs, this is recommended for android
protobuf {
- protoc { artifact = libs.protobuf.protoc.get().toString() }
+ protoc { artifact = libs.protoc.get().toString() }
generateProtoTasks {
all().forEach { task ->
task.builtins {
@@ -239,36 +210,19 @@ project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defa
dependencies {
implementation(project(":network"))
- implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
-
// Bundles
implementation(libs.bundles.androidx)
- implementation(libs.bundles.ui)
implementation(libs.bundles.markdown)
- debugImplementation(libs.bundles.ui.tooling)
- implementation(libs.bundles.adaptive)
- implementation(libs.bundles.lifecycle)
- implementation(libs.bundles.navigation)
- implementation(libs.bundles.navigation3)
implementation(libs.bundles.coroutines)
implementation(libs.bundles.datastore)
- implementation(libs.bundles.room)
- implementation(libs.bundles.hilt)
implementation(libs.bundles.protobuf)
implementation(libs.bundles.coil)
- // OSM
- "fdroidImplementation"(libs.bundles.osm)
- "fdroidImplementation"(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
-
- "googleImplementation"(libs.bundles.maps.compose)
-
// ZXing
implementation(libs.zxing.android.embedded) { isTransitive = false }
implementation(libs.zxing.core)
- // Individual dependencies
- "googleImplementation"(libs.awesome.app.rating)
+ // Individual dependencies (flavor-specific ones removed)
implementation(libs.core.splashscreen)
implementation(libs.emoji2.emojipicker)
implementation(libs.kotlinx.collections.immutable)
@@ -281,92 +235,15 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.timber)
- // Compose BOM
- implementation(platform(libs.compose.bom))
- androidTestImplementation(platform(libs.compose.bom))
-
- // Firebase BOM
- "googleImplementation"(platform(libs.firebase.bom))
- "googleImplementation"(libs.bundles.firebase)
- "googleImplementation"(libs.bundles.datadog)
-
- // ksp
- ksp(libs.room.compiler)
- ksp(libs.hilt.compiler)
- kspAndroidTest(libs.hilt.compiler)
-
- // Testing
testImplementation(libs.bundles.testing)
- debugImplementation(libs.bundles.testing.android.manifest)
androidTestImplementation(libs.bundles.testing.android)
androidTestImplementation(libs.bundles.testing.hilt)
androidTestImplementation(libs.bundles.testing.navigation)
androidTestImplementation(libs.bundles.testing.room)
- detektPlugins(libs.detekt.formatting)
dokkaPlugin(libs.dokka.android.documentation.plugin)
}
-ksp {
- // arg("room.generateKotlin", "true")
- arg("room.schemaLocation", "$projectDir/schemas")
-}
-
-detekt {
- config.setFrom("../config/detekt/detekt.yml")
- baseline = file("../config/detekt/detekt-baseline.xml")
- source.setFrom(files("src/main/java", "src/google/java", "src/fdroid/java"))
- parallel = true
-}
-
-secrets {
- propertiesFileName = "secrets.properties"
- defaultPropertiesFileName = "secrets.defaults.properties"
-}
-
-val googleServiceKeywords = listOf("crashlytics", "google", "datadog")
-
-tasks.configureEach {
- if (
- googleServiceKeywords.any { name.contains(it, ignoreCase = true) } && name.contains("fdroid", ignoreCase = true)
- ) {
- project.logger.lifecycle("Disabling task for F-Droid: $name")
- enabled = false
- }
-}
-
-tasks.withType {
- reports {
- xml.required = true
- xml.outputLocation = file("build/reports/detekt/detekt.xml")
- html.required = true
- html.outputLocation = file("build/reports/detekt/detekt.html")
- sarif.required = true
- sarif.outputLocation = file("build/reports/detekt/detekt.sarif")
- md.required = true
- md.outputLocation = file("build/reports/detekt/detekt.md")
- }
- debug = true
- include("**/*.kt")
- include("**/*.kts")
-}
-
-spotless {
- ratchetFrom("origin/main")
- kotlin {
- target("src/*/kotlin/**/*.kt", "src/*/java/**/*.kt")
- targetExclude("**/build/**/*.kt")
- ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
- ktlint("1.7.1").setEditorConfigPath("../config/spotless/.editorconfig")
- licenseHeaderFile(rootProject.file("config/spotless/copyright.txt"))
- }
- kotlinGradle {
- target("**/*.gradle.kts")
- ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
- ktlint("1.7.1").setEditorConfigPath("../config/spotless/.editorconfig")
- }
-}
-
dokka {
moduleName.set("Meshtastic App")
dokkaSourceSets.main {
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt
index eb3814e8a..b9e9051ed 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt
@@ -24,7 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.viewinterop.AndroidView
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.UIViewModel
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
index 9a2b4eb6a..24b03c1e3 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -62,7 +62,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.MeshProtos
diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt
index cf4fadfbb..0d6fc6dcc 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt
@@ -20,7 +20,7 @@ package com.geeksville.mesh.ui.node
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.map.MapView
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
index 36febea23..cec77872e 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsRoutes.kt
@@ -18,7 +18,7 @@
package com.geeksville.mesh.navigation
import androidx.compose.runtime.remember
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
index c3794eed6..c0dadee36 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsRoutes.kt
@@ -18,7 +18,7 @@
package com.geeksville.mesh.navigation
import androidx.compose.runtime.remember
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
index 32da5d844..7393b9d38 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NavGraph.kt
@@ -26,7 +26,7 @@ import androidx.compose.material.icons.rounded.Storage
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
index bba6fe66d..3f96b52c4 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesRoutes.kt
@@ -30,7 +30,7 @@ import androidx.compose.material.icons.filled.Router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt
index 389a0f109..6026de27f 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt
@@ -45,7 +45,7 @@ import androidx.compose.material.icons.filled.Wifi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 1b246ed68..d096a5e44 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -64,7 +64,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/EmojiPicker.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/EmojiPicker.kt
index 7065134cc..0667a70eb 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/EmojiPicker.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/EmojiPicker.kt
@@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.geeksville.mesh.ui.common.EmojiPickerViewModel
import com.geeksville.mesh.util.CustomRecentEmojiProvider
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
index 35030ddc0..9a4c0316b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
@@ -40,7 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt
index 30a9d6815..03fdc9445 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt
@@ -74,7 +74,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt
index a7fe62130..f1c72d5cb 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt
@@ -60,7 +60,7 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
index 9ba603edf..dddacb11d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
@@ -76,7 +76,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.size
import androidx.compose.ui.unit.sp
import androidx.datastore.core.IOException
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.warn
diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt
index 00f9a9d28..32fe15178 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/debug/DebugSearch.kt
@@ -52,7 +52,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.geeksville.mesh.R
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
index 622b68dac..6020ddb88 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
@@ -92,7 +92,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.DataPacket
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt b/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
index 41cb2dd62..644f48e26 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
@@ -68,7 +68,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.QuickChatAction
@@ -79,18 +79,16 @@ import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
-internal fun QuickChatScreen(
- modifier: Modifier = Modifier,
- viewModel: UIViewModel = hiltViewModel(),
-) {
+internal fun QuickChatScreen(modifier: Modifier = Modifier, viewModel: UIViewModel = hiltViewModel()) {
val actions by viewModel.quickChatActions.collectAsStateWithLifecycle()
var showActionDialog by remember { mutableStateOf(null) }
val listState = rememberLazyListState()
- val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
- val list = actions.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
- viewModel.updateActionPositions(list)
- }
+ val dragDropState =
+ rememberDragDropState(listState) { fromIndex, toIndex ->
+ val list = actions.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+ viewModel.updateActionPositions(list)
+ }
Box(modifier = modifier.fillMaxSize()) {
if (showActionDialog != null) {
@@ -99,41 +97,30 @@ internal fun QuickChatScreen(
action = action,
onSave = viewModel::addQuickChatAction,
onDelete = viewModel::deleteQuickChatAction,
- ) { showActionDialog = null }
+ ) {
+ showActionDialog = null
+ }
}
LazyColumn(
- modifier = Modifier.dragContainer(
- dragDropState = dragDropState,
- haptics = LocalHapticFeedback.current,
- ),
+ modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
contentPadding = PaddingValues(16.dp),
) {
- dragDropItemsIndexed(
- items = actions,
- dragDropState = dragDropState,
- key = { _, item -> item.uuid },
- ) { _, action, isDragging ->
- QuickChatItem(
- action = action,
- onEdit = { showActionDialog = it },
- )
+ dragDropItemsIndexed(items = actions, dragDropState = dragDropState, key = { _, item -> item.uuid }) {
+ _,
+ action,
+ isDragging,
+ ->
+ QuickChatItem(action = action, onEdit = { showActionDialog = it })
}
}
FloatingActionButton(
- onClick = {
- showActionDialog = QuickChatAction(position = actions.size)
- },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp)
+ onClick = { showActionDialog = QuickChatAction(position = actions.size) },
+ modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
) {
- Icon(
- imageVector = Icons.Default.Add,
- contentDescription = stringResource(id = R.string.add),
- )
+ Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(id = R.string.add))
}
}
}
@@ -172,116 +159,111 @@ private fun EditQuickChatDialog(
AlertDialog(
onDismissRequest = onDismiss,
- text =
- {
- Column(modifier = Modifier.fillMaxWidth()) {
- Text(
- text = stringResource(id = title),
- modifier = Modifier.fillMaxWidth(),
- style = MaterialTheme.typography.titleLarge.copy(
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center,
- ),
- )
+ text = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text(
+ text = stringResource(id = title),
+ modifier = Modifier.fillMaxWidth(),
+ style =
+ MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ ),
+ )
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextFieldWithCounter(
- label = stringResource(R.string.name),
- value = actionInput.name,
- maxSize = 5,
- singleLine = true,
- modifier = Modifier.fillMaxWidth(),
- ) { actionInput = actionInput.copy(name = it.uppercase()) }
+ OutlinedTextFieldWithCounter(
+ label = stringResource(R.string.name),
+ value = actionInput.name,
+ maxSize = 5,
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ actionInput = actionInput.copy(name = it.uppercase())
+ }
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextFieldWithCounter(
- label = stringResource(id = R.string.message),
- value = actionInput.message,
- maxSize = 200,
- getSize = { it.toByteArray().size + 1 },
- modifier = Modifier
- .fillMaxWidth()
- .focusRequester(focusRequester),
- ) {
- actionInput = actionInput.copy(message = it)
- if (newQuickChat) {
- actionInput = actionInput.copy(name = getMessageName(it))
- }
+ OutlinedTextFieldWithCounter(
+ label = stringResource(id = R.string.message),
+ value = actionInput.message,
+ maxSize = 200,
+ getSize = { it.toByteArray().size + 1 },
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ ) {
+ actionInput = actionInput.copy(message = it)
+ if (newQuickChat) {
+ actionInput = actionInput.copy(name = getMessageName(it))
}
+ }
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
- val (text, icon) = if (isInstant) {
+ val (text, icon) =
+ if (isInstant) {
R.string.quick_chat_instant to Icons.Default.FastForward
} else {
R.string.quick_chat_append to Icons.Default.Add
}
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (isInstant) {
- Icon(
- imageVector = icon,
- contentDescription = stringResource(id = text),
- )
- Spacer(Modifier.width(12.dp))
- }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (isInstant) {
+ Icon(imageVector = icon, contentDescription = stringResource(id = text))
+ Spacer(Modifier.width(12.dp))
+ }
- Text(
- text = stringResource(text),
- modifier = Modifier.weight(1f),
- )
+ Text(text = stringResource(text), modifier = Modifier.weight(1f))
- Switch(
- checked = isInstant,
- onCheckedChange = { checked ->
- actionInput = actionInput.copy(
- mode = when (checked) {
+ Switch(
+ checked = isInstant,
+ onCheckedChange = { checked ->
+ actionInput =
+ actionInput.copy(
+ mode =
+ when (checked) {
true -> QuickChatAction.Mode.Instant
false -> QuickChatAction.Mode.Append
- }
+ },
)
- },
- )
- }
+ },
+ )
+ }
+ }
+ },
+ confirmButton = {
+ FlowRow(
+ modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ TextButton(modifier = Modifier.weight(1f), onClick = onDismiss) {
+ Text(stringResource(R.string.cancel))
}
- },
- confirmButton =
- {
- FlowRow(
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- TextButton(
- modifier = Modifier.weight(1f),
- onClick = onDismiss,
- ) { Text(stringResource(R.string.cancel)) }
-
- if (!newQuickChat) {
- Button(
- modifier = Modifier.weight(1f),
- onClick = {
- onDelete(actionInput)
- onDismiss()
- },
- ) { Text(text = stringResource(R.string.delete)) }
- }
+ if (!newQuickChat) {
Button(
modifier = Modifier.weight(1f),
onClick = {
- onSave(actionInput)
+ onDelete(actionInput)
onDismiss()
},
- enabled = actionInput.name.isNotEmpty() && actionInput.message.isNotEmpty(),
- ) { Text(text = stringResource(R.string.save)) }
+ ) {
+ Text(text = stringResource(R.string.delete))
+ }
}
- },
+
+ Button(
+ modifier = Modifier.weight(1f),
+ onClick = {
+ onSave(actionInput)
+ onDismiss()
+ },
+ enabled = actionInput.name.isNotEmpty() && actionInput.message.isNotEmpty(),
+ ) {
+ Text(text = stringResource(R.string.save))
+ }
+ }
+ },
)
}
@@ -311,9 +293,7 @@ private fun OutlinedTextFieldWithCounter(
Text(
text = "${getSize(value)}/$maxSize",
style = MaterialTheme.typography.bodySmall,
- modifier = Modifier
- .align(Alignment.End)
- .padding(top = 4.dp, end = 16.dp)
+ modifier = Modifier.align(Alignment.End).padding(top = 4.dp, end = 16.dp),
)
}
}
@@ -324,12 +304,7 @@ private fun QuickChatItem(
modifier: Modifier = Modifier,
onEdit: (QuickChatAction) -> Unit = {},
) {
- Card(
- modifier = modifier
- .fillMaxWidth()
- .padding(8.dp),
- shape = RoundedCornerShape(12.dp),
- ) {
+ Card(modifier = modifier.fillMaxWidth().padding(8.dp), shape = RoundedCornerShape(12.dp)) {
ListItem(
leadingContent = {
if (action.mode == QuickChatAction.Mode.Instant) {
@@ -342,13 +317,8 @@ private fun QuickChatItem(
headlineContent = { Text(text = action.name) },
supportingContent = { Text(text = action.message) },
trailingContent = {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- IconButton(
- onClick = { onEdit(action) },
- modifier = Modifier.size(48.dp)
- ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = { onEdit(action) }, modifier = Modifier.size(48.dp)) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = stringResource(id = R.string.quick_chat_edit),
@@ -359,7 +329,7 @@ private fun QuickChatItem(
contentDescription = stringResource(id = R.string.quick_chat),
)
}
- }
+ },
)
}
}
@@ -367,15 +337,7 @@ private fun QuickChatItem(
@PreviewLightDark
@Composable
private fun QuickChatItemPreview() {
- AppTheme {
- QuickChatItem(
- action = QuickChatAction(
- name = "TST",
- message = "Test",
- position = 0,
- ),
- )
- }
+ AppTheme { QuickChatItem(action = QuickChatAction(name = "TST", message = "Test", position = 0)) }
}
@PreviewLightDark
@@ -383,14 +345,10 @@ private fun QuickChatItemPreview() {
private fun EditQuickChatDialogPreview() {
AppTheme {
EditQuickChatDialog(
- action = QuickChatAction(
- name = "TST",
- message = "Test",
- position = 0,
- ),
+ action = QuickChatAction(name = "TST", message = "Test", position = 0),
onSave = {},
onDelete = {},
- onDismiss = {}
+ onDismiss = {},
)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
index 767e6d008..f2b9f3cf0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
@@ -57,7 +57,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.TelemetryProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
index f2d2a0e19..4c700faa7 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
@@ -46,7 +46,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.EnvironmentMetrics
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
index cdd63ff4b..5cfbb3917 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
@@ -51,7 +51,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.TelemetryProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
index dfbf474e5..8666cf76c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
@@ -51,7 +51,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums.PortNum
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
index 9ea4a6e63..5d6329048 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
@@ -59,7 +59,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.MeshProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
index 9704d7dcc..979ad010f 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
@@ -57,7 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.TelemetryProtos.Telemetry
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
index 7c2a71b99..66ee97baf 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
@@ -54,7 +54,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
index 8947d40ca..99a149c81 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
@@ -54,7 +54,7 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
index 53587b87c..432462c17 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
@@ -117,7 +117,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
index ecaa69478..b7165b7f7 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
@@ -43,7 +43,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.model.DeviceVersion
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
index 771113e7b..a094f5039 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
@@ -46,7 +46,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseScreen.kt
index 1e5e699cc..6de0164e1 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseScreen.kt
@@ -45,7 +45,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.node.components.NodeChip
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt
index 2eda1d9c1..7cc64116d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt
index ba51c3cec..5ea882b1a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt
@@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt
index 0e70f1ab6..9b05c3ed0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt
index 4e73aa06f..04a962137 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
index 01b5e2769..09c3b4824 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
@@ -66,7 +66,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt
index 00b627345..1fbc18124 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt
index 3776a2ba9..ff8e7569a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt
@@ -47,7 +47,7 @@ import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt
index 1a550d257..fdbd2b9f4 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt
index c544bf0de..a613d3b1e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
index ce11624c7..ccc52aa38 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
@@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt
index 9971997c2..77e95ca1c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt
@@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt
index 54de7fc60..c35042046 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt
index bad4fa122..04e49c941 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt
@@ -40,7 +40,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt
index 483c0a938..0609ac9b0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt
index 121647b26..d8d0eee65 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt
@@ -40,7 +40,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.location.LocationCompat
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt
index c5d304c5a..c05cf6722 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.PowerConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt
index d04f7331b..d8fd21a65 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt
index d9d727c34..513da7631 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt
index 5cae39f6e..1f4b27193 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt
@@ -44,7 +44,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.MeshProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt
index ba4ff367f..982091937 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt
index 23f9e4409..50f246357 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt
index e132b0d1e..de1a2ce53 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt
index 9ec407751..a5e163424 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index e81dd74e1..844b3c111 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -84,7 +84,7 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ChannelProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
index 7fbe97d49..ab2e80900 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
@@ -48,7 +48,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
index b25000557..1a65c4c72 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
@@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact
@@ -46,23 +46,14 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.contact.ContactItem
@Composable
-fun ShareScreen(
- viewModel: UIViewModel = hiltViewModel(),
- onConfirm: (String) -> Unit
-) {
+fun ShareScreen(viewModel: UIViewModel = hiltViewModel(), onConfirm: (String) -> Unit) {
val contactList by viewModel.contactList.collectAsStateWithLifecycle()
- ShareScreen(
- contacts = contactList,
- onConfirm = onConfirm,
- )
+ ShareScreen(contacts = contactList, onConfirm = onConfirm)
}
@Composable
-fun ShareScreen(
- contacts: List,
- onConfirm: (String) -> Unit
-) {
+fun ShareScreen(contacts: List, onConfirm: (String) -> Unit) {
var selectedContact by remember { mutableStateOf("") }
Column {
@@ -73,26 +64,18 @@ fun ShareScreen(
) {
items(contacts, key = { it.contactKey }) { contact ->
val selected = contact.contactKey == selectedContact
- ContactItem(
- contact = contact,
- selected = selected,
- onClick = { selectedContact = contact.contactKey },
- )
+ ContactItem(contact = contact, selected = selected, onClick = { selectedContact = contact.contactKey })
}
}
Button(
- onClick = {
- onConfirm(selectedContact)
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp),
+ onClick = { onConfirm(selectedContact) },
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
enabled = selectedContact.isNotEmpty(),
) {
Icon(
imageVector = Icons.AutoMirrored.Default.Send,
- contentDescription = stringResource(id = R.string.share)
+ contentDescription = stringResource(id = R.string.share),
)
}
}
@@ -103,7 +86,8 @@ fun ShareScreen(
private fun ShareScreenPreview() {
AppTheme {
ShareScreen(
- contacts = listOf(
+ contacts =
+ listOf(
Contact(
contactKey = "0^all",
shortName = stringResource(R.string.some_username),
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
new file mode 100644
index 000000000..64e17768c
--- /dev/null
+++ b/build-logic/convention/build.gradle.kts
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ `kotlin-dsl`
+ alias(libs.plugins.android.lint)
+}
+
+group = "com.geeksville.mesh.buildlogic"
+
+// Configure the build-logic plugins to target JDK 21
+// This matches the JDK used to build the project, and is not related to what is running on device.
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_21
+ }
+}
+
+dependencies {
+ compileOnly(libs.android.gradleApiPlugin)
+ compileOnly(libs.android.tools.common)
+ compileOnly(libs.compose.gradlePlugin)
+ compileOnly(libs.detekt.gradle)
+ compileOnly(libs.firebase.crashlytics.gradlePlugin)
+ compileOnly(libs.firebase.performance.gradlePlugin)
+ compileOnly(libs.kotlin.gradlePlugin)
+ compileOnly(libs.ksp.gradlePlugin)
+ compileOnly(libs.room.gradlePlugin)
+ compileOnly(libs.spotless.gradlePlugin)
+ implementation(libs.truth)
+}
+
+tasks {
+ validatePlugins {
+ enableStricterValidation = true
+ failOnWarning = true
+ }
+}
+
+
+gradlePlugin {
+ plugins {
+ register("androidApplication") {
+ id = libs.plugins.meshtastic.android.application.asProvider().get().pluginId
+ implementationClass = "AndroidApplicationConventionPlugin"
+ }
+ register("androidFlavors") {
+ id = libs.plugins.meshtastic.android.application.flavors.get().pluginId
+ implementationClass = "AndroidApplicationFlavorsConventionPlugin"
+ }
+ register("androidLibrary") {
+ id = libs.plugins.meshtastic.android.library.asProvider().get().pluginId
+ implementationClass = "AndroidLibraryConventionPlugin"
+ }
+ register("androidLint") {
+ id = libs.plugins.meshtastic.android.lint.get().pluginId
+ implementationClass = "AndroidLintConventionPlugin"
+ }
+ register("androidFirebase") {
+ id = libs.plugins.meshtastic.android.application.firebase.get().pluginId
+ implementationClass = "AndroidApplicationFirebaseConventionPlugin"
+ }
+ register("androidLibraryCompose") {
+ id = libs.plugins.meshtastic.android.library.compose.get().pluginId
+ implementationClass = "AndroidLibraryComposeConventionPlugin"
+ }
+ register("androidApplicationCompose") {
+ id = libs.plugins.meshtastic.android.application.compose.get().pluginId
+ implementationClass = "AndroidApplicationComposeConventionPlugin"
+ }
+ register("hilt") {
+ id = libs.plugins.meshtastic.hilt.get().pluginId
+ implementationClass = "HiltConventionPlugin"
+ }
+ register("detekt") {
+ id = libs.plugins.meshtastic.detekt.get().pluginId
+ implementationClass = "DetektConventionPlugin"
+ }
+ register("androidRoom") {
+ id = libs.plugins.meshtastic.android.room.get().pluginId
+ implementationClass = "AndroidRoomConventionPlugin"
+ }
+
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
new file mode 100644
index 000000000..59d0863ee
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.geeksville.mesh.buildlogic.configureAndroidCompose
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.getByType
+
+class AndroidApplicationComposeConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "com.android.application")
+ apply(plugin = "org.jetbrains.kotlin.plugin.compose")
+
+ val extension = extensions.getByType()
+ configureAndroidCompose(extension)
+ }
+ }
+
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
new file mode 100644
index 000000000..7dcfa811e
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.diffplug.gradle.spotless.SpotlessExtension
+import com.geeksville.mesh.buildlogic.configureKotlinAndroid
+import com.geeksville.mesh.buildlogic.configureSpotless
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+class AndroidApplicationConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+
+ apply(plugin = "com.android.application")
+ apply(plugin = "org.jetbrains.kotlin.android")
+ apply(plugin = "meshtastic.android.lint")
+ apply(plugin = "meshtastic.android.room")
+ apply(plugin = "meshtastic.hilt")
+ apply(plugin = "com.diffplug.spotless")
+ apply(plugin = "com.autonomousapps.dependency-analysis")
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ defaultConfig.targetSdk = 36
+ testOptions.animationsDisabled = true
+
+ defaultConfig {
+ targetSdk = 36
+ testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ getByName("debug") {
+ isDebuggable = true
+ isPseudoLocalesEnabled = true
+ }
+ }
+
+ buildFeatures {
+ buildConfig = true
+ }
+
+ }
+
+ extensions.configure {
+ configureSpotless(this)
+ }
+
+ extensions.configure {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+ }
+
+ tasks.withType().configureEach {
+ compilerOptions {
+ freeCompilerArgs.addAll(
+ "-opt-in=kotlin.RequiresOptIn",
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-Xcontext-receivers",
+ "-Xannotation-default-target=param-property",
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
new file mode 100644
index 000000000..3e2fa8d55
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.geeksville.mesh.buildlogic.libs
+import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.exclude
+
+class AndroidApplicationFirebaseConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "com.google.gms.google-services")
+ apply(plugin = "com.google.firebase.firebase-perf")
+ apply(plugin = "com.google.firebase.crashlytics")
+
+ dependencies {
+ val bom = libs.findLibrary("firebase-bom").get()
+ "implementation"(platform(bom))
+ "implementation"(libs.findBundle("firebase").get()){
+ /*
+ Exclusion of protobuf / protolite dependencies is necessary as the
+ datastore-proto brings in protobuf dependencies. These are the source of truth
+ for Now in Android.
+ That's why the duplicate classes from below dependencies are excluded.
+ */
+ exclude(group = "com.google.protobuf", module = "protobuf-java")
+ exclude(group = "com.google.protobuf", module = "protobuf-kotlin")
+ exclude(group = "com.google.protobuf", module = "protobuf-javalite")
+ exclude(group = "com.google.firebase", module = "protolite-well-known-types")
+ }
+ "implementation"(libs.findLibrary("firebase.crashlytics").get())
+ }
+
+ extensions.configure {
+ buildTypes.configureEach {
+ // Disable the Crashlytics mapping file upload. This feature should only be
+ // enabled if a Firebase backend is available and configured in
+ // google-services.json.
+ configure {
+ mappingFileUploadEnabled = false
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
new file mode 100644
index 000000000..b805a805f
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.geeksville.mesh.buildlogic.configureFlavors
+import com.geeksville.mesh.buildlogic.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.exclude
+
+class AndroidApplicationFlavorsConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ extensions.configure {
+ configureFlavors(this)
+ dependencies {
+ // F-Droid specific dependencies
+ "fdroidImplementation"(libs.findBundle("osm").get())
+ "fdroidImplementation"(libs.findLibrary("osmdroid-geopackage").get()) {
+ exclude(group = "com.j256.ormlite")
+ }
+
+ // Google specific dependencies
+ "googleImplementation"(libs.findBundle("maps-compose").get())
+ "googleImplementation"(libs.findLibrary("awesome-app-rating").get())
+ "googleImplementation"(platform(libs.findLibrary("firebase-bom").get()))
+ "googleImplementation"(libs.findBundle("datadog").get())
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
new file mode 100644
index 000000000..c3d700ff9
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.LibraryExtension
+import com.geeksville.mesh.buildlogic.configureAndroidCompose
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.getByType
+
+class AndroidLibraryComposeConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "com.android.library")
+ apply(plugin = "org.jetbrains.kotlin.plugin.compose")
+ apply(plugin = "com.autonomousapps.dependency-analysis")
+
+ val extension = extensions.getByType()
+ configureAndroidCompose(extension)
+ }
+ }
+
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
new file mode 100644
index 000000000..adc8e8761
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.diffplug.gradle.spotless.SpotlessExtension
+import com.geeksville.mesh.buildlogic.configureFlavors
+import com.geeksville.mesh.buildlogic.configureKotlinAndroid
+import com.geeksville.mesh.buildlogic.configureSpotless
+import com.geeksville.mesh.buildlogic.disableUnnecessaryAndroidTests
+import com.geeksville.mesh.buildlogic.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+
+class AndroidLibraryConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "com.android.library")
+ apply(plugin = "org.jetbrains.kotlin.android")
+ apply(plugin = "meshtastic.android.lint")
+ apply(plugin = "com.diffplug.spotless")
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ testOptions.targetSdk = 36
+ lint.targetSdk = 36
+ defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testOptions.animationsDisabled = true
+ configureFlavors(this)
+ // The resource prefix is derived from the module name,
+ // so resources inside ":core:module1" must be prefixed with "core_module1_"
+ resourcePrefix =
+ path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_")
+ .lowercase() + "_"
+ }
+ extensions.configure {
+ disableUnnecessaryAndroidTests(target)
+ }
+ dependencies {
+ "androidTestImplementation"(libs.findLibrary("kotlin.test").get())
+ "testImplementation"(libs.findLibrary("kotlin.test").get())
+
+ "implementation"(libs.findLibrary("androidx.tracing.ktx").get())
+ }
+ extensions.configure {
+ configureSpotless(this)
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
new file mode 100644
index 000000000..d2cc0b95f
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.dsl.Lint
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+
+class AndroidLintConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ when {
+ pluginManager.hasPlugin("com.android.application") ->
+ configure { lint(Lint::configure) }
+
+ pluginManager.hasPlugin("com.android.library") ->
+ configure { lint(Lint::configure) }
+
+ else -> {
+ apply(plugin = "com.android.lint")
+ configure(Lint::configure)
+ }
+ }
+ }
+ }
+}
+
+private fun Lint.configure() {
+ xmlReport = true
+ sarifReport = true
+ checkDependencies = true
+ abortOnError = false
+ disable += "GradleDependency"
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
new file mode 100644
index 000000000..7ad2c59ec
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import androidx.room.gradle.RoomExtension
+import com.geeksville.mesh.buildlogic.libs
+import com.google.devtools.ksp.gradle.KspExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+
+class AndroidRoomConventionPlugin : Plugin {
+
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "androidx.room")
+ apply(plugin = "com.google.devtools.ksp")
+
+ extensions.configure {
+ arg("room.generateKotlin", "true")
+ }
+
+ extensions.configure {
+ // The schemas directory contains a schema file for each version of the Room database.
+ // This is required to enable Room auto migrations.
+ // See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
+ schemaDirectory("$projectDir/schemas")
+ }
+
+ dependencies {
+ "implementation"(libs.findLibrary("room.runtime").get())
+ "implementation"(libs.findLibrary("room.ktx").get())
+ "ksp"(libs.findLibrary("room.compiler").get())
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt b/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt
new file mode 100644
index 000000000..d2fd3d85d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt
@@ -0,0 +1,17 @@
+import com.geeksville.mesh.buildlogic.configureDetekt
+import com.geeksville.mesh.buildlogic.libs
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.getByType
+
+class DetektConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = libs.findPlugin("detekt").get().get().pluginId)
+ val extension = extensions.getByType()
+ configureDetekt(extension)
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt
new file mode 100644
index 000000000..008476935
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import com.geeksville.mesh.buildlogic.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.dependencies
+
+class HiltConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "com.google.devtools.ksp")
+
+ dependencies {
+ "ksp"(libs.findLibrary("hilt.compiler").get())
+ "implementation"(libs.findBundle("hilt").get())
+ }
+
+ // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
+ pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
+ dependencies {
+ "implementation"(libs.findLibrary("hilt.core").get())
+ }
+ }
+
+ pluginManager.withPlugin("com.android.base") {
+ apply(plugin = "dagger.hilt.android.plugin")
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidCompose.kt
new file mode 100644
index 000000000..5b1847eec
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidCompose.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.Project
+import org.gradle.api.provider.Provider
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
+import kotlin.apply
+import kotlin.io.toRelativeString
+import kotlin.let
+import kotlin.takeIf
+import kotlin.text.toBoolean
+
+/**
+ * Configure Compose-specific options
+ */
+internal fun Project.configureAndroidCompose(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ commonExtension.apply {
+ buildFeatures {
+ compose = true
+ }
+
+ dependencies {
+ val bom = libs.findLibrary("androidx-compose-bom").get()
+ "implementation"(platform(bom))
+ "androidTestImplementation"(platform(bom))
+ "implementation"(libs.findBundle("ui").get())
+ "implementation"(libs.findBundle("adaptive").get())
+ "implementation"(libs.findBundle("lifecycle").get())
+ "implementation"(libs.findBundle("navigation").get())
+ "implementation"(libs.findBundle("navigation3").get())
+ "implementation"(libs.findBundle("ui-tooling").get())
+ "implementation"(libs.findLibrary("androidx-compose-ui-tooling-preview").get())
+ "debugImplementation"(libs.findLibrary("androidx-compose-ui-tooling").get())
+ "debugImplementation"(libs.findLibrary("androidx-compose-ui-testManifest").get())
+ }
+ }
+
+ extensions.configure {
+ fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } }
+ fun Provider<*>.relativeToRootProject(dir: String) = map {
+ isolated.rootProject.projectDirectory
+ .dir("build")
+ .dir(projectDir.toRelativeString(rootDir))
+ }.map { it.dir(dir) }
+
+ project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue()
+ .relativeToRootProject("compose-metrics")
+ .let(metricsDestination::set)
+
+ project.providers.gradleProperty("enableComposeCompilerReports").onlyIfTrue()
+ .relativeToRootProject("compose-reports")
+ .let(reportsDestination::set)
+
+ stabilityConfigurationFiles
+ .add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf"))
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidInstrumentedTests.kt
new file mode 100644
index 000000000..dcff2f42d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/AndroidInstrumentedTests.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import org.gradle.api.Project
+import kotlin.io.resolve
+
+/**
+ * Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder.
+ * Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message:
+ *
+ * > Starting 0 tests on AVD
+ *
+ * Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors.
+ */
+internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
+ project: Project,
+) = beforeVariants {
+ it.androidTest.enable = it.androidTest.enable
+ && project.projectDir.resolve("src/androidTest").exists()
+}
diff --git a/buildSrc/src/main/kotlin/Configs.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Configs.kt
similarity index 69%
rename from buildSrc/src/main/kotlin/Configs.kt
rename to build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Configs.kt
index 6ab790eed..28df9753f 100644
--- a/buildSrc/src/main/kotlin/Configs.kt
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Configs.kt
@@ -1,3 +1,22 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
/*
* Copyright (c) 2025 Meshtastic LLC
*
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt
new file mode 100644
index 000000000..9c29222ef
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import io.gitlab.arturbosch.detekt.Detekt
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.named
+
+internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
+ extension.apply {
+ toolVersion = libs.findVersion("detekt").get().toString()
+ config.setFrom("$rootDir/config/detekt/detekt.yml")
+ buildUponDefaultConfig = true
+ allRules = false
+ baseline = file("$rootDir/config/detekt/baseline.xml")
+ source.setFrom(
+ files(
+ "src/main/java",
+ "src/main/kotlin",
+ ),
+ )
+ }
+ tasks.named("detekt") {
+ reports {
+ xml.required.set(true)
+ html.required.set(true)
+ txt.required.set(true)
+ sarif.required.set(true)
+ md.required.set(true)
+ }
+ }
+ dependencies {
+ "detektPlugins"(libs.findLibrary("detekt-formatting").get())
+ "detektPlugins"(libs.findLibrary("detekt-compose").get())
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/GitVersionValueSource.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/GitVersionValueSource.kt
similarity index 64%
rename from buildSrc/src/main/kotlin/GitVersionValueSource.kt
rename to build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/GitVersionValueSource.kt
index e5ba969d7..eaf98fe3c 100644
--- a/buildSrc/src/main/kotlin/GitVersionValueSource.kt
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/GitVersionValueSource.kt
@@ -1,3 +1,22 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
/*
* Copyright (c) 2025 Meshtastic LLC
*
@@ -34,7 +53,8 @@ abstract class GitVersionValueSource : ValueSource.
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.kotlin.dsl.assign
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
+import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import kotlin.apply
+import kotlin.text.toBoolean
+
+/**
+ * Configure base Kotlin with Android options
+ */
+internal fun Project.configureKotlinAndroid(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ commonExtension.apply {
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 26
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ isCoreLibraryDesugaringEnabled = true
+ }
+ }
+
+ configureKotlin()
+
+ dependencies {
+ "coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
+ }
+}
+
+/**
+ * Configure base Kotlin options for JVM (non-Android)
+ */
+internal fun Project.configureKotlinJvm() {
+ extensions.configure {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+
+ configureKotlin()
+}
+
+/**
+ * Configure base Kotlin options
+ */
+private inline fun Project.configureKotlin() = configure {
+ // Treat all Kotlin warnings as errors (disabled by default)
+ // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
+ val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map {
+ it.toBoolean()
+ }.orElse(false)
+ when (this) {
+ is KotlinAndroidProjectExtension -> compilerOptions
+ is KotlinJvmProjectExtension -> compilerOptions
+ else -> TODO("Unsupported project extension $this ${T::class}")
+ }.apply {
+ jvmTarget.assign(JvmTarget.JVM_21)
+ allWarningsAsErrors.assign(warningsAsErrors)
+ freeCompilerArgs.add(
+ // Enable experimental coroutines APIs, including Flow
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ )
+ freeCompilerArgs.add(
+ /**
+ * Remove this args after Phase 3.
+ * https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline
+ *
+ * Deprecation timeline
+ * Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3).
+ * The default changes.
+ * Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor.
+ * The binary signature changes. The error on the declaration is no longer reported.
+ * '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
+ */
+ "-Xconsistent-data-class-copy-visibility"
+ )
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/MeshtasticFlavor.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/MeshtasticFlavor.kt
new file mode 100644
index 000000000..53094a1f6
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/MeshtasticFlavor.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.ApplicationProductFlavor
+import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.dsl.ProductFlavor
+
+@Suppress("EnumEntryName")
+enum class FlavorDimension {
+ marketplace
+}
+
+@Suppress("EnumEntryName")
+enum class MeshtasticFlavor(val dimension: FlavorDimension, val default: Boolean = false) {
+ fdroid(FlavorDimension.marketplace, ),
+ google(FlavorDimension.marketplace, default = true),
+}
+
+fun configureFlavors(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+ flavorConfigurationBlock: ProductFlavor.(flavor: MeshtasticFlavor) -> Unit = {},
+) {
+ commonExtension.apply {
+ FlavorDimension.entries.forEach { flavorDimension ->
+ flavorDimensions += flavorDimension.name
+ }
+
+ productFlavors {
+ MeshtasticFlavor.entries.forEach { meshtasticFlavor ->
+ register(meshtasticFlavor.name) {
+ dimension = meshtasticFlavor.dimension.name
+ flavorConfigurationBlock(this, meshtasticFlavor)
+ if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) {
+ if (meshtasticFlavor.default) {
+ isDefault = true
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/ProjectExtensions.kt
new file mode 100644
index 000000000..2c2b55eb2
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/ProjectExtensions.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.VersionCatalog
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.getByType
+
+val Project.libs
+ get(): VersionCatalog = extensions.getByType().named("libs")
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Spotless.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Spotless.kt
new file mode 100644
index 000000000..eaf6b89c9
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Spotless.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.buildlogic
+
+import com.diffplug.gradle.spotless.SpotlessExtension
+import io.gitlab.arturbosch.detekt.Detekt
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.named
+
+internal fun Project.configureSpotless(extension: SpotlessExtension) = extension.apply {
+ extension.apply {
+ ratchetFrom("origin/main")
+ kotlin {
+ target("src/*/kotlin/**/*.kt", "src/*/java/**/*.kt")
+ targetExclude("**/build/**/*.kt")
+ ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
+ ktlint("1.7.1").setEditorConfigPath("../config/spotless/.editorconfig")
+ licenseHeaderFile(rootProject.file("config/spotless/copyright.kt"))
+ }
+ kotlinGradle {
+ target("**/*.gradle.kts")
+ ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) }
+ ktlint("1.7.1").setEditorConfigPath("../config/spotless/.editorconfig")
+ licenseHeaderFile(
+ rootProject.file("config/spotless/copyright.kts"),
+ "(^(?![\\/ ]\\*).*$)"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties
new file mode 100644
index 000000000..5e07c65d0
--- /dev/null
+++ b/build-logic/gradle.properties
@@ -0,0 +1,6 @@
+# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+org.gradle.configuration-cache=true
+org.gradle.configuration-cache.parallel=true
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 000000000..2833b7534
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "build-logic"
+include(":convention")
diff --git a/build.gradle.kts b/build.gradle.kts
index 4843239aa..8678f2325 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -15,6 +15,8 @@
* along with this program. If not, see .
*/
+
+
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
@@ -22,18 +24,25 @@ plugins {
alias(libs.plugins.datadog) apply false
alias(libs.plugins.devtools.ksp) apply false
alias(libs.plugins.firebase.crashlytics) apply false
+ alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.room) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.protobuf) apply false
alias(libs.plugins.secrets) apply false
- alias(libs.plugins.dokka) apply false
+ alias(libs.plugins.dependency.analysis) apply false
+ alias(libs.plugins.detekt) apply false
+ alias(libs.plugins.meshtastic.detekt) apply false
alias(libs.plugins.kover)
+ alias(libs.plugins.spotless) apply false
}
+
+
kover {
reports {
total {
@@ -69,8 +78,4 @@ dependencies {
kover(project(":app"))
kover(project(":network"))
kover(project(":mesh_service_example"))
-}
-
-tasks.register("clean") {
- delete(layout.buildDirectory)
-}
+}
\ No newline at end of file
diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
new file mode 100644
index 000000000..66c9e45ca
--- /dev/null
+++ b/config/detekt/baseline.xml
@@ -0,0 +1,621 @@
+
+
+
+
+ ChainWrapping:Channel.kt$Channel$&&
+ CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down
+ CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back
+ CommentSpacing:ContextExtensions.kt$/// Utility function to hide the soft keyboard per stack overflow
+ CommentSpacing:ContextExtensions.kt$/// show a toast
+ CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib
+ CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work
+ CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work
+ CommentSpacing:Exceptions.kt$/// Convert any exceptions in this service call into a RemoteException that the client can
+ CommentSpacing:Exceptions.kt$/// then handle
+ CommentSpacing:Exceptions.kt$Exceptions$/// Set in Application.onCreate
+ CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */
+ ComposableNaming:NodeDetail.kt$notesSection
+ ComposableParamOrder:AlertDialogs.kt$SimpleAlertDialog
+ ComposableParamOrder:BatteryInfo.kt$BatteryInfo
+ ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList
+ ComposableParamOrder:Connections.kt$ConnectionsScreen
+ ComposableParamOrder:CurrentlyConnectedCard.kt$CurrentlyConnectedCard
+ ComposableParamOrder:Debug.kt$DebugMenuActions
+ ComposableParamOrder:Debug.kt$DecodedPayloadBlock
+ ComposableParamOrder:DebugSearch.kt$DebugSearchState
+ ComposableParamOrder:DebugSearch.kt$DebugSearchStateviewModelDefaults
+ ComposableParamOrder:DeviceMetrics.kt$DeviceMetricsChart
+ ComposableParamOrder:EditBase64Preference.kt$EditBase64Preference
+ ComposableParamOrder:EditTextPreference.kt$EditTextPreference
+ ComposableParamOrder:ElevationInfo.kt$ElevationInfo
+ ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent
+ ComposableParamOrder:EnvironmentCharts.kt$ChartContent
+ ComposableParamOrder:EnvironmentCharts.kt$EnvironmentMetricsChart
+ ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas
+ ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem
+ ComposableParamOrder:HostMetricsLog.kt$LogLine
+ ComposableParamOrder:LastHeardInfo.kt$LastHeardInfo
+ ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates
+ ComposableParamOrder:MainAppBar.kt$MainAppBar
+ ComposableParamOrder:MapReportingPreference.kt$MapReportingPreference
+ ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo
+ ComposableParamOrder:Message.kt$MessageScreen
+ ComposableParamOrder:Message.kt$QuickChatRow
+ ComposableParamOrder:MessageActions.kt$MessageActions
+ ComposableParamOrder:MessageActions.kt$MessageStatusButton
+ ComposableParamOrder:MessageItem.kt$MessageItem
+ ComposableParamOrder:MessageList.kt$DeliveryInfo
+ ComposableParamOrder:MessageList.kt$MessageList
+ ComposableParamOrder:NodeChip.kt$NodeChip
+ ComposableParamOrder:NodeDetail.kt$DeviceActions
+ ComposableParamOrder:NodeDetail.kt$EnvironmentMetrics
+ ComposableParamOrder:NodeDetail.kt$NodeActionButton
+ ComposableParamOrder:NodeDetail.kt$NodeDetailList
+ ComposableParamOrder:NodeDetail.kt$NodeDetailScreen
+ ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField
+ ComposableParamOrder:NodeItem.kt$NodeItem
+ ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon
+ ComposableParamOrder:NodeMenu.kt$NodeMenu
+ ComposableParamOrder:NodeScreen.kt$NodeScreen
+ ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart
+ ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout
+ ComposableParamOrder:PositionConfigItemList.kt$PositionConfigItemList
+ ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart
+ ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter
+ ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo
+ ComposableParamOrder:SecurityConfigItemList.kt$SecurityConfigItemList
+ ComposableParamOrder:SettingsItem.kt$SettingsItem
+ ComposableParamOrder:SignalInfo.kt$SignalInfo
+ ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart
+ ComposableParamOrder:SwitchPreference.kt$SwitchPreference
+ ComposableParamOrder:TopLevelNavIcon.kt$ConnectionsNavIcon
+ ComposableParamOrder:TracerouteButton.kt$TracerouteButton
+ ComposableParamOrder:WarningDialog.kt$WarningDialog
+ ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "message") val raw_message: String
+ ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "received_date") val received_date: Long
+ ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "type") val message_type: String
+ ConstructorParameterNaming:Packet.kt$ContactSettings$@PrimaryKey val contact_key: String
+ ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String
+ ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int
+ ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long
+ ContentSlotReused:AdaptiveTwoPane.kt$second
+ CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
+ CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
+ EmptyCatchBlock:MeshLog.kt$MeshLog${ }
+ EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }
+ EmptyFunctionBlock:NopInterface.kt$NopInterface${ }
+ EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }
+ EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}
+ FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
+ FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt
+ FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
+ FinalNewline:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt
+ FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt
+ FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
+ FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
+ FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
+ FinalNewline:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt
+ FinalNewline:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt
+ FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt
+ FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt
+ FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt
+ FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt
+ FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt
+ FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt
+ FinalNewline:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt
+ FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt
+ FinalNewline:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt
+ FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt
+ FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt
+ FinalNewline:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt
+ FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt
+ FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt
+ FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt
+ FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
+ ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE
+ FunctionNaming:PacketDao.kt$PacketDao$@Query("DELETE FROM packet WHERE uuid=:uuid") suspend fun _delete(uuid: Long)
+ FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long)
+ ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel)
+ LambdaParameterEventTrailing:Channel.kt$onConfirm
+ LambdaParameterEventTrailing:CurrentlyConnectedCard.kt$onClickDisconnect
+ LambdaParameterEventTrailing:MainAppBar.kt$onAction
+ LambdaParameterEventTrailing:Message.kt$onClick
+ LambdaParameterEventTrailing:Message.kt$onSendMessage
+ LambdaParameterEventTrailing:MessageList.kt$onReply
+ LambdaParameterEventTrailing:NodeChip.kt$onAction
+ LambdaParameterEventTrailing:NodeDetail.kt$onClick
+ LambdaParameterEventTrailing:NodeDetail.kt$onSaveNotes
+ LambdaParameterInRestartableEffect:Channel.kt$onConfirm
+ LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged
+ LargeClass:MeshService.kt$MeshService : ServiceLogging
+ LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigItemList( ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, enabled: Boolean, onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit, )
+ LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigItemList( messages: String, cannedMessageConfig: CannedMessageConfig, enabled: Boolean, onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit, )
+ LongMethod:DropDownPreference.kt$@Composable fun <T> DropDownPreference( title: String, enabled: Boolean, items: List<Pair<T, String>>, selectedItem: T, onItemSelected: (T) -> Unit, modifier: Modifier = Modifier, summary: String? = null, )
+ LongMethod:EditListPreference.kt$@Composable inline fun <reified T> EditListPreference( title: String, list: List<T>, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List<T>) -> Unit, modifier: Modifier = Modifier, )
+ LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, )
+ LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
+ LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
+ LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigItemList( storeForwardConfig: StoreForwardConfig, enabled: Boolean, onSaveClicked: (StoreForwardConfig) -> Unit, )
+ LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigItemList( telemetryConfig: TelemetryConfig, enabled: Boolean, onSaveClicked: (TelemetryConfig) -> Unit, )
+ MagicNumber:BatteryInfo.kt$100
+ MagicNumber:BatteryInfo.kt$101
+ MagicNumber:BatteryInfo.kt$14
+ MagicNumber:BatteryInfo.kt$15
+ MagicNumber:BatteryInfo.kt$34
+ MagicNumber:BatteryInfo.kt$35
+ MagicNumber:BatteryInfo.kt$4
+ MagicNumber:BatteryInfo.kt$5
+ MagicNumber:BatteryInfo.kt$79
+ MagicNumber:BatteryInfo.kt$80
+ MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000
+ MagicNumber:BluetoothInterface.kt$BluetoothInterface$500
+ MagicNumber:BluetoothInterface.kt$BluetoothInterface$512
+ MagicNumber:Channel.kt$0xff
+ MagicNumber:ChannelOption.kt$.03125f
+ MagicNumber:ChannelOption.kt$.0625f
+ MagicNumber:ChannelOption.kt$.203125f
+ MagicNumber:ChannelOption.kt$.40625f
+ MagicNumber:ChannelOption.kt$.8125f
+ MagicNumber:ChannelOption.kt$1.6250f
+ MagicNumber:ChannelOption.kt$1000f
+ MagicNumber:ChannelOption.kt$1600
+ MagicNumber:ChannelOption.kt$200
+ MagicNumber:ChannelOption.kt$3.25f
+ MagicNumber:ChannelOption.kt$31
+ MagicNumber:ChannelOption.kt$400
+ MagicNumber:ChannelOption.kt$5
+ MagicNumber:ChannelOption.kt$62
+ MagicNumber:ChannelOption.kt$800
+ MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f
+ MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f
+ MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f
+ MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f
+ MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f
+ MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f
+ MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f
+ MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f
+ MagicNumber:ChannelSet.kt$40
+ MagicNumber:ChannelSet.kt$960
+ MagicNumber:Contacts.kt$7
+ MagicNumber:Contacts.kt$8
+ MagicNumber:DataPacket.kt$DataPacket.CREATOR$16
+ MagicNumber:Debug.kt$3
+ MagicNumber:DeviceVersion.kt$DeviceVersion$100
+ MagicNumber:DeviceVersion.kt$DeviceVersion$10000
+ MagicNumber:EditChannelDialog.kt$16
+ MagicNumber:EditChannelDialog.kt$32
+ MagicNumber:EditIPv4Preference.kt$0xff
+ MagicNumber:EditIPv4Preference.kt$16
+ MagicNumber:EditIPv4Preference.kt$24
+ MagicNumber:EditIPv4Preference.kt$8
+ MagicNumber:EditListPreference.kt$12
+ MagicNumber:EditListPreference.kt$12345
+ MagicNumber:EditListPreference.kt$67890
+ MagicNumber:Extensions.kt$1000
+ MagicNumber:Extensions.kt$1440000
+ MagicNumber:Extensions.kt$24
+ MagicNumber:Extensions.kt$2880
+ MagicNumber:Extensions.kt$60
+ MagicNumber:LazyColumnDragAndDropDemo.kt$50
+ MagicNumber:LocationRepository.kt$LocationRepository$1000L
+ MagicNumber:LocationRepository.kt$LocationRepository$30
+ MagicNumber:LocationRepository.kt$LocationRepository$31
+ MagicNumber:LocationUtils.kt$1e-7
+ MagicNumber:LocationUtils.kt$360
+ MagicNumber:MQTTRepository.kt$MQTTRepository$512
+ MagicNumber:MeshService.kt$MeshService$0xffffffff
+ MagicNumber:MeshService.kt$MeshService$1000
+ MagicNumber:MeshService.kt$MeshService$1000.0
+ MagicNumber:MeshService.kt$MeshService$1000L
+ MagicNumber:MeshService.kt$MeshService$16
+ MagicNumber:MeshService.kt$MeshService$30
+ MagicNumber:MeshService.kt$MeshService$32
+ MagicNumber:MeshService.kt$MeshService$60000
+ MagicNumber:MeshService.kt$MeshService$8
+ MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L
+ MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5
+ MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7
+ MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000
+ MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000
+ MagicNumber:NodeInfo.kt$NodeInfo$0.114
+ MagicNumber:NodeInfo.kt$NodeInfo$0.299
+ MagicNumber:NodeInfo.kt$NodeInfo$0.587
+ MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF
+ MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00
+ MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000
+ MagicNumber:NodeInfo.kt$NodeInfo$1000
+ MagicNumber:NodeInfo.kt$NodeInfo$1000.0
+ MagicNumber:NodeInfo.kt$NodeInfo$16
+ MagicNumber:NodeInfo.kt$NodeInfo$1609
+ MagicNumber:NodeInfo.kt$NodeInfo$1609.34
+ MagicNumber:NodeInfo.kt$NodeInfo$255
+ MagicNumber:NodeInfo.kt$NodeInfo$3.281
+ MagicNumber:NodeInfo.kt$NodeInfo$8
+ MagicNumber:NodeInfo.kt$Position$180
+ MagicNumber:NodeInfo.kt$Position$90
+ MagicNumber:NodeInfo.kt$Position$90.0
+ MagicNumber:NodeInfo.kt$Position.Companion$1000
+ MagicNumber:NodeInfo.kt$Position.Companion$1e-7
+ MagicNumber:NodeInfo.kt$Position.Companion$1e7
+ MagicNumber:PacketRepository.kt$PacketRepository$500
+ MagicNumber:PacketResponseStateDialog.kt$100
+ MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972
+ MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809
+ MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790
+ MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114
+ MagicNumber:SafeBluetooth.kt$SafeBluetooth$10
+ MagicNumber:SafeBluetooth.kt$SafeBluetooth$100
+ MagicNumber:SafeBluetooth.kt$SafeBluetooth$1000
+ MagicNumber:SafeBluetooth.kt$SafeBluetooth$2500
+ MagicNumber:SafeBluetooth.kt$SafeBluetooth.<no name provided>$2500
+ MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200
+ MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200
+ MagicNumber:ServiceClient.kt$ServiceClient$500
+ MagicNumber:StreamInterface.kt$StreamInterface$0xff
+ MagicNumber:StreamInterface.kt$StreamInterface$3
+ MagicNumber:StreamInterface.kt$StreamInterface$4
+ MagicNumber:StreamInterface.kt$StreamInterface$8
+ MagicNumber:TCPInterface.kt$TCPInterface$1000
+ MagicNumber:TCPInterface.kt$TCPInterface$180
+ MagicNumber:TCPInterface.kt$TCPInterface$500
+ MagicNumber:UIState.kt$4
+ MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider
+ MatchingDeclarationName:DistanceExtensions.kt$DistanceUnit
+ MatchingDeclarationName:LocationUtils.kt$GPSFormat
+ MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker
+ MatchingDeclarationName:SortOption.kt$NodeSortOption
+ MaxLineLength:BluetoothInterface.kt$/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface. MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there. FIXME - make sure this protocol is guaranteed robust and won't drop packets "According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)). In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes." MAXPACKET is 256? look into what the lora lib uses. FIXME Characteristics: UUID properties description 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 read fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet). After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this mailbox. f75c76d2-129e-4dad-a1dd-7866124401e7 write toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len) ed9da18c-a800-4f66-a670-aa7547e34453 read|notify|write fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages until it catches up with this number. The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32 callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio. When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio. Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted. Re: queue management Not all messages are kept in the fromradio queue (filtered based on SubPacket): * only the most recent Position and User messages for a particular node are kept * all Data SubPackets are kept * No WantNodeNum / DenyNodeNum messages are kept A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging) */
+ MaxLineLength:BluetoothState.kt$BluetoothState$"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})"
+ MaxLineLength:Channel.kt$Channel$// We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name
+ MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int
+ MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
+ MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*
+ MaxLineLength:ServiceClient.kt$ServiceClient$// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
+ MaxLineLength:ServiceClient.kt$ServiceClient.<no name provided>$// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
+ ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)
+ ModifierListSpacing:Packet.kt$Packet$@Entity( tableName = "packet", indices = [ Index(value = ["myNodeNum"]), Index(value = ["port_num"]), Index(value = ["contact_key"]), ] ) data
+ ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane
+ ModifierMissing:AmbientLightingConfigItemList.kt$AmbientLightingConfigItemList
+ ModifierMissing:AudioConfigItemList.kt$AudioConfigItemList
+ ModifierMissing:BLEDevices.kt$BLEDevices
+ ModifierMissing:BluetoothConfigItemList.kt$BluetoothConfigItemList
+ ModifierMissing:CannedMessageConfigItemList.kt$CannedMessageConfigItemList
+ ModifierMissing:Channel.kt$ChannelScreen
+ ModifierMissing:ChannelSettingsItemList.kt$ChannelSelection
+ ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen
+ ModifierMissing:CommonCharts.kt$ChartHeader
+ ModifierMissing:CommonCharts.kt$Legend
+ ModifierMissing:CommonCharts.kt$TimeLabels
+ ModifierMissing:Connections.kt$ConnectionsScreen
+ ModifierMissing:ContactSharing.kt$SharedContactDialog
+ ModifierMissing:Contacts.kt$ContactListView
+ ModifierMissing:Contacts.kt$ContactsScreen
+ ModifierMissing:Contacts.kt$SelectionToolbar
+ ModifierMissing:DetectionSensorConfigItemList.kt$DetectionSensorConfigItemList
+ ModifierMissing:DeviceConfigItemList.kt$DeviceConfigItemList
+ ModifierMissing:DeviceMetrics.kt$DeviceMetricsScreen
+ ModifierMissing:DisplayConfigItemList.kt$DisplayConfigItemList
+ ModifierMissing:EmojiPicker.kt$EmojiPicker
+ ModifierMissing:EmojiPicker.kt$EmojiPickerDialog
+ ModifierMissing:EmptyStateContent.kt$EmptyStateContent
+ ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen
+ ModifierMissing:ExternalNotificationConfigItemList.kt$ExternalNotificationConfigItemList
+ ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen
+ ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality
+ ModifierMissing:LoRaConfigItemList.kt$LoRaConfigItemList
+ ModifierMissing:LoraSignalIndicator.kt$LoraSignalIndicator
+ ModifierMissing:LoraSignalIndicator.kt$Rssi
+ ModifierMissing:LoraSignalIndicator.kt$Snr
+ ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi
+ ModifierMissing:MQTTConfigItemList.kt$MQTTConfigItemList
+ ModifierMissing:Main.kt$MainScreen
+ ModifierMissing:MapReportingPreference.kt$MapReportingPreference
+ ModifierMissing:MessageActions.kt$MessageStatusButton
+ ModifierMissing:MessageActions.kt$ReactionButton
+ ModifierMissing:MessageActions.kt$ReplyButton
+ ModifierMissing:NeighborInfoConfigItemList.kt$NeighborInfoConfigItemList
+ ModifierMissing:NetworkConfigItemList.kt$NetworkConfigItemList
+ ModifierMissing:NetworkDevices.kt$NetworkDevices
+ ModifierMissing:NodeMenu.kt$NodeMenu
+ ModifierMissing:NodeScreen.kt$NodeScreen
+ ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons
+ ModifierMissing:PaxMetrics.kt$PaxMetricsItem
+ ModifierMissing:PaxMetrics.kt$PaxMetricsScreen
+ ModifierMissing:PaxcounterConfigItemList.kt$PaxcounterConfigItemList
+ ModifierMissing:PositionConfigItemList.kt$PositionConfigItemList
+ ModifierMissing:PositionLog.kt$PositionItem
+ ModifierMissing:PositionLog.kt$PositionLogScreen
+ ModifierMissing:PowerConfigItemList.kt$PowerConfigItemList
+ ModifierMissing:PowerMetrics.kt$PowerMetricsScreen
+ ModifierMissing:RadioConfig.kt$RadioConfigItemList
+ ModifierMissing:RangeTestConfigItemList.kt$RangeTestConfigItemList
+ ModifierMissing:Reaction.kt$ReactionDialog
+ ModifierMissing:RemoteHardwareConfigItemList.kt$RemoteHardwareConfigItemList
+ ModifierMissing:SecurityConfigItemList.kt$SecurityConfigItemList
+ ModifierMissing:SecurityIcon.kt$SecurityIcon
+ ModifierMissing:SerialConfigItemList.kt$SerialConfigItemList
+ ModifierMissing:SettingsItem.kt$SettingsItem
+ ModifierMissing:SettingsItem.kt$SettingsItemDetail
+ ModifierMissing:SettingsItem.kt$SettingsItemSwitch
+ ModifierMissing:SettingsScreen.kt$SettingsScreen
+ ModifierMissing:Share.kt$ShareScreen
+ ModifierMissing:SignalMetrics.kt$SignalMetricsScreen
+ ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog
+ ModifierMissing:SlidingSelector.kt$OptionLabel
+ ModifierMissing:StoreForwardConfigItemList.kt$StoreForwardConfigItemList
+ ModifierMissing:TelemetryConfigItemList.kt$TelemetryConfigItemList
+ ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon
+ ModifierMissing:UserConfigItemList.kt$UserConfigItemList
+ ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End)
+ ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier.fillMaxWidth()
+ ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
+ ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)
+ ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:DropDownPreference.kt$modifier = modifier .background( color = if (selectedItem == item.first) { MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) } else { Color.Unspecified }, )
+ ModifierNotUsedAtRoot:EditChannelDialog.kt$modifier = modifier.weight(1f)
+ ModifierNotUsedAtRoot:EditDeviceProfileDialog.kt$modifier = modifier.weight(1f)
+ ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.fillMaxWidth()
+ ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.padding(16.dp)
+ ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier = modifier.width(dp)
+ ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:NodeChip.kt$modifier = modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } }
+ ModifierNotUsedAtRoot:PaxMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
+ ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)
+ ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:PreferenceFooter.kt$modifier = modifier .height(48.dp) .weight(1f)
+ ModifierNotUsedAtRoot:SignalInfo.kt$modifier = modifier
+ ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
+ ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)
+ ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .padding(all = 16.dp)
+ ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End)
+ ModifierReused:BitwisePreference.kt$Checkbox( modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, )
+ ModifierReused:BitwisePreference.kt$DropdownMenuItem( onClick = { onItemSelected(value xor item.first) }, modifier = modifier.fillMaxWidth(), text = { Text( text = item.second, overflow = TextOverflow.Ellipsis, ) Checkbox( modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, ) } )
+ ModifierReused:DeviceMetrics.kt$Canvas(modifier = modifier.width(dp)) { val height = size.height val width = size.width for (i in telemetries.indices) { val telemetry = telemetries[i] /* x-value time */ val xRatio = (telemetry.time - oldest.time).toFloat() / timeDiff val x = xRatio * width /* Channel Utilization */ plotPoint( drawContext = drawContext, color = Device.CH_UTIL.color, x = x, value = telemetry.deviceMetrics.channelUtilization, divisor = MAX_PERCENT_VALUE, ) /* Air Utilization Transmit */ plotPoint( drawContext = drawContext, color = Device.AIR_UTIL.color, x = x, value = telemetry.deviceMetrics.airUtilTx, divisor = MAX_PERCENT_VALUE, ) } /* Battery Line */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }
+ ModifierReused:DeviceMetrics.kt$HorizontalLinesOverlay( modifier.width(dp), lineColors = listOf(graphColor, Color.Yellow, Color.Red, graphColor, graphColor), )
+ ModifierReused:DeviceMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())
+ ModifierReused:EditListPreference.kt$Column(modifier = modifier) { Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium) listState.forEachIndexed { index, value -> val trailingIcon = @Composable { IconButton( onClick = { focusManager.clearFocus() listState.removeAt(index) onValuesChanged(listState) }, ) { Icon( imageVector = Icons.TwoTone.Close, contentDescription = stringResource(R.string.delete), modifier = Modifier.wrapContentSize(), ) } } // handle lora.ignoreIncoming: List<Int> if (value is Int) { EditTextPreference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) } // handle security.adminKey: List<ByteString> if (value is ByteString) { EditBase64Preference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChange = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) } // handle remoteHardware.availablePins: List<RemoteHardwarePin> if (value is RemoteHardwarePin) { EditTextPreference( title = stringResource(R.string.gpio_pin), value = value.gpioPin, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { if (it in 0..255) { listState[index] = value.copy { gpioPin = it } as T onValuesChanged(listState) } }, ) EditTextPreference( title = stringResource(R.string.name), value = value.name, maxSize = 14, // name max_size:15 enabled = enabled, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { listState[index] = value.copy { name = it } as T onValuesChanged(listState) }, trailingIcon = trailingIcon, ) DropDownPreference( title = stringResource(R.string.type), enabled = enabled, items = RemoteHardwarePinType.entries .filter { it != RemoteHardwarePinType.UNRECOGNIZED } .map { it to it.name }, selectedItem = value.type, onItemSelected = { listState[index] = value.copy { type = it } as T onValuesChanged(listState) }, ) } } OutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = { // Add element based on the type T val newElement = when (T::class) { Int::class -> 0 as T ByteString::class -> ByteString.EMPTY as T RemoteHardwarePin::class -> remoteHardwarePin {} as T else -> throw IllegalArgumentException("Unsupported type: ${T::class}") } listState.add(listState.size, newElement) }, enabled = maxCount > listState.size, ) { Text(text = stringResource(R.string.add)) } }
+ ModifierReused:EditListPreference.kt$EditBase64Preference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChange = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, )
+ ModifierReused:EditListPreference.kt$EditTextPreference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, )
+ ModifierReused:EditListPreference.kt$Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium)
+ ModifierReused:EditTextPreference.kt$Box( contentAlignment = Alignment.BottomEnd, modifier = modifier.fillMaxWidth() ) { Text( text = "${value.toByteArray().size}/$maxSize", style = MaterialTheme.typography.bodySmall, color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(end = 8.dp, bottom = 4.dp) ) }
+ ModifierReused:EditTextPreference.kt$TextField( value = value, singleLine = true, modifier = modifier .fillMaxWidth() .onFocusEvent { isFocused = it.isFocused; onFocusChanged(it) }, enabled = enabled, isError = isError, onValueChange = { if (maxSize > 0) { if (it.toByteArray().size <= maxSize) { onValueChanged(it) } } else onValueChanged(it) }, label = { Text(title) }, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, visualTransformation = visualTransformation, trailingIcon = { if (trailingIcon != null) { trailingIcon() } else if (isError) { Icon( imageVector = Icons.TwoTone.Info, contentDescription = stringResource(id = R.string.error), tint = MaterialTheme.colorScheme.error ) } }, )
+ ModifierReused:EnvironmentCharts.kt$Box( contentAlignment = Alignment.TopStart, modifier = modifier.horizontalScroll(state = scrollState, reverseScrolling = true), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval()) MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, ) }
+ ModifierReused:EnvironmentCharts.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })
+ ModifierReused:EnvironmentCharts.kt$MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, )
+ ModifierReused:EnvironmentCharts.kt$TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval())
+ ModifierReused:PaxMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray })
+ ModifierReused:PaxMetrics.kt$Row(modifier = modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f)) { YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(start = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) Box( contentAlignment = Alignment.TopStart, modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(CHART_WEIGHT), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray }) TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval()) Canvas(modifier = Modifier.width(dp).fillMaxHeight()) { val width = size.width val height = size.height fun xForTime(t: Int): Float = if (maxTime == minTime) width / 2 else (t - minTime).toFloat() / (maxTime - minTime) * width fun yForValue(v: Int): Float = height - (v - minValue) / (maxValue - minValue) * height fun drawLine(series: List<Pair<Int, Int>>, color: Color) { for (i in 1 until series.size) { drawLine( color = color, start = Offset(xForTime(series[i - 1].first), yForValue(series[i - 1].second)), end = Offset(xForTime(series[i].first), yForValue(series[i].second)), strokeWidth = 2.dp.toPx(), ) } } drawLine(bleSeries, PaxSeries.BLE.color) drawLine(wifiSeries, PaxSeries.WIFI.color) drawLine(totalSeries, PaxSeries.PAX.color) } } YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(end = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) }
+ ModifierReused:PaxMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval())
+ ModifierReused:PowerMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width val height = size.height /* Voltage */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveVoltage(selectedChannel, telemetry) - voltageMin) / voltageDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = VOLTAGE_COLOR, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } /* Current */ index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Power.CURRENT.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }
+ ModifierReused:PowerMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })
+ ModifierReused:PowerMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())
+ ModifierReused:PowerMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Power.CURRENT.color, minValue = Power.CURRENT.min, maxValue = Power.CURRENT.max, )
+ ModifierReused:PowerMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), VOLTAGE_COLOR, minValue = voltageMin, maxValue = voltageMax, )
+ ModifierReused:PreferenceCategory.kt$Card( modifier = modifier.padding(bottom = 8.dp), ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { ProvideTextStyle(MaterialTheme.typography.bodyLarge) { content() } } }
+ ModifierReused:PreferenceCategory.kt$Text( text, modifier = modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp, end = 16.dp), style = MaterialTheme.typography.titleLarge, )
+ ModifierReused:PreferenceFooter.kt$OutlinedButton( modifier = modifier .height(48.dp) .weight(1f), enabled = enabled, onClick = onPositiveClicked, ) { Text( text = stringResource(id = positiveText), ) }
+ ModifierReused:PreferenceFooter.kt$OutlinedButton( modifier = modifier .height(48.dp) .weight(1f), onClick = onNegativeClicked, ) { Text( text = stringResource(id = negativeText), ) }
+ ModifierReused:PreferenceFooter.kt$Row( modifier = modifier .fillMaxWidth() .height(64.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { OutlinedButton( modifier = modifier .height(48.dp) .weight(1f), onClick = onNegativeClicked, ) { Text( text = stringResource(id = negativeText), ) } OutlinedButton( modifier = modifier .height(48.dp) .weight(1f), enabled = enabled, onClick = onPositiveClicked, ) { Text( text = stringResource(id = positiveText), ) } }
+ ModifierReused:SignalMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width /* Plot */ for (packet in meshPackets) { val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff val x = xRatio * width /* SNR */ plotPoint( drawContext = drawContext, color = Metric.SNR.color, x = x, value = packet.rxSnr - Metric.SNR.min, divisor = snrDiff, ) /* RSSI */ plotPoint( drawContext = drawContext, color = Metric.RSSI.color, x = x, value = packet.rxRssi - Metric.RSSI.min, divisor = rssiDiff, ) } }
+ ModifierReused:SignalMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })
+ ModifierReused:SignalMetrics.kt$TimeAxisOverlay( modifier.width(dp), oldest = oldest.rxTime, newest = newest.rxTime, selectedTime.lineInterval(), )
+ ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.RSSI.color, minValue = Metric.RSSI.min, maxValue = Metric.RSSI.max, )
+ ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.SNR.color, minValue = Metric.SNR.min, maxValue = Metric.SNR.max, )
+ ModifierReused:TextDividerPreference.kt$Card( modifier = modifier.fillMaxWidth(), ) { Row( modifier = modifier .fillMaxWidth() .padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon( trailingIcon, "trailingIcon", modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), ) } } }
+ ModifierReused:TextDividerPreference.kt$Icon( trailingIcon, "trailingIcon", modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), )
+ ModifierReused:TextDividerPreference.kt$Row( modifier = modifier .fillMaxWidth() .padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon( trailingIcon, "trailingIcon", modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), ) } }
+ ModifierWithoutDefault:CommonCharts.kt$modifier
+ ModifierWithoutDefault:EnvironmentCharts.kt$modifier
+ MultiLineIfElse:Channel.kt$Channel$"Custom"
+ MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" }
+ MultiLineIfElse:EditTextPreference.kt$it.toDoubleOrNull()?.let { double -> valueState = it onValueChanged(double) }
+ MultiLineIfElse:EditTextPreference.kt$it.toFloatOrNull()?.let { float -> valueState = it onValueChanged(float) }
+ MultiLineIfElse:EditTextPreference.kt$it.toUIntOrNull()?.toInt()?.let { int -> valueState = it onValueChanged(int) }
+ MultiLineIfElse:EditTextPreference.kt$onValueChanged(it)
+ MultiLineIfElse:EditTextPreference.kt$valueState = it
+ MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex)
+ MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview
+ MultipleEmitters:CommonCharts.kt$LegendLabel
+ MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart
+ MultipleEmitters:EditTextPreference.kt$EditTextPreference
+ MultipleEmitters:EnvironmentCharts.kt$EnvironmentMetricsChart
+ MultipleEmitters:NodeDetail.kt$EncryptionErrorContent
+ MultipleEmitters:NodeDetail.kt$MetricsSection
+ MultipleEmitters:PaxMetrics.kt$PaxMetricsChart
+ MultipleEmitters:PowerMetrics.kt$PowerMetricsChart
+ MultipleEmitters:PreferenceCategory.kt$PreferenceCategory
+ MultipleEmitters:RadioConfig.kt$RadioConfigItemList
+ MultipleEmitters:SignalMetrics.kt$SignalMetricsChart
+ MutableStateAutoboxing:Contacts.kt$mutableStateOf(2)
+ MutableStateParam:MessageList.kt$selectedIds
+ NestedBlockDepth:LanguageUtils.kt$LanguageUtils$fun getLanguageTags(context: Context): Map<String, String>
+ NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)
+ NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
+ NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
+ NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
+ NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt
+ NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
+ NewLineAtEndOfFile:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt
+ NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt
+ NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
+ NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
+ NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
+ NewLineAtEndOfFile:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt
+ NewLineAtEndOfFile:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt
+ NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt
+ NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt
+ NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt
+ NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt
+ NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt
+ NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt
+ NewLineAtEndOfFile:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt
+ NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt
+ NewLineAtEndOfFile:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt
+ NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt
+ NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt
+ NewLineAtEndOfFile:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt
+ NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt
+ NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt
+ NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt
+ NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
+ NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$
+ NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$
+ NoConsecutiveBlankLines:BootCompleteReceiver.kt$
+ NoConsecutiveBlankLines:Constants.kt$
+ NoConsecutiveBlankLines:DebugLogFile.kt$
+ NoConsecutiveBlankLines:DeferredExecution.kt$
+ NoConsecutiveBlankLines:Exceptions.kt$
+ NoConsecutiveBlankLines:IRadioInterface.kt$
+ NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }
+ NoSemicolons:DateUtils.kt$DateUtils$;
+ NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.*
+ OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract
+ ParameterNaming:AmbientLightingConfigItemList.kt$onSaveClicked
+ ParameterNaming:AudioConfigItemList.kt$onSaveClicked
+ ParameterNaming:BitwisePreference.kt$onItemSelected
+ ParameterNaming:BluetoothConfigItemList.kt$onSaveClicked
+ ParameterNaming:CannedMessageConfigItemList.kt$onSaveClicked
+ ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked
+ ParameterNaming:ChannelSettingsItemList.kt$onSelected
+ ParameterNaming:CleanNodeDatabaseScreen.kt$onCheckedChanged
+ ParameterNaming:CleanNodeDatabaseScreen.kt$onDaysChanged
+ ParameterNaming:Contacts.kt$onDeleteSelected
+ ParameterNaming:Contacts.kt$onMuteSelected
+ ParameterNaming:DetectionSensorConfigItemList.kt$onSaveClicked
+ ParameterNaming:DeviceConfigItemList.kt$onSaveClicked
+ ParameterNaming:DisplayConfigItemList.kt$onSaveClicked
+ ParameterNaming:DropDownPreference.kt$onItemSelected
+ ParameterNaming:EditIPv4Preference.kt$onValueChanged
+ ParameterNaming:EditListPreference.kt$onValuesChanged
+ ParameterNaming:EditPasswordPreference.kt$onValueChanged
+ ParameterNaming:EditTextPreference.kt$onValueChanged
+ ParameterNaming:ExternalNotificationConfigItemList.kt$onSaveClicked
+ ParameterNaming:LoRaConfigItemList.kt$onSaveClicked
+ ParameterNaming:MQTTConfigItemList.kt$onSaveClicked
+ ParameterNaming:MQTTConfigItemList.kt$onShouldReportLocationChanged
+ ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged
+ ParameterNaming:MapReportingPreference.kt$onPositionPrecisionChanged
+ ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged
+ ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged
+ ParameterNaming:MessageList.kt$onUnreadChanged
+ ParameterNaming:NeighborInfoConfigItemList.kt$onSaveClicked
+ ParameterNaming:NetworkConfigItemList.kt$onSaveClicked
+ ParameterNaming:NodeDetail.kt$onFirmwareSelected
+ ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored
+ ParameterNaming:PaxcounterConfigItemList.kt$onSaveClicked
+ ParameterNaming:PositionConfigItemList.kt$onSaveClicked
+ ParameterNaming:PositionPrecisionPreference.kt$onValueChanged
+ ParameterNaming:PowerConfigItemList.kt$onSaveClicked
+ ParameterNaming:PreferenceFooter.kt$onCancelClicked
+ ParameterNaming:PreferenceFooter.kt$onNegativeClicked
+ ParameterNaming:PreferenceFooter.kt$onPositiveClicked
+ ParameterNaming:PreferenceFooter.kt$onSaveClicked
+ ParameterNaming:RangeTestConfigItemList.kt$onSaveClicked
+ ParameterNaming:RemoteHardwareConfigItemList.kt$onSaveClicked
+ ParameterNaming:SerialConfigItemList.kt$onSaveClicked
+ ParameterNaming:SlidingSelector.kt$onOptionSelected
+ ParameterNaming:StoreForwardConfigItemList.kt$onSaveClicked
+ ParameterNaming:TelemetryConfigItemList.kt$onSaveClicked
+ ParameterNaming:UsbDevices.kt$onDeviceSelected
+ ParameterNaming:UserConfigItemList.kt$onSaveClicked
+ ParameterNaming:WelcomeScreen.kt$onGetStarted
+ PreviewAnnotationNaming:LargeFontPreview.kt$LargeFontPreview$LargeFontPreview
+ PreviewPublic:BatteryInfo.kt$BatteryInfoPreview
+ PreviewPublic:BatteryInfo.kt$BatteryInfoPreviewSimple
+ PreviewPublic:Channel.kt$ModemPresetInfoPreview
+ PreviewPublic:ElevationInfo.kt$ElevationInfoPreview
+ PreviewPublic:EmptyStateContent.kt$EmptyStateContentPreview
+ PreviewPublic:IndoorAirQuality.kt$IAQScalePreview
+ PreviewPublic:LastHeardInfo.kt$LastHeardInfoPreview
+ PreviewPublic:LazyColumnDragAndDropDemo.kt$LazyColumnDragAndDropDemo
+ PreviewPublic:LinkedCoordinates.kt$LinkedCoordinatesPreview
+ PreviewPublic:MapReportingPreference.kt$MapReportingPreview
+ PreviewPublic:MaterialBatteryInfo.kt$MaterialBatteryInfoPreview
+ PreviewPublic:NodeChip.kt$NodeChipPreview
+ PreviewPublic:NodeItem.kt$NodeInfoPreview
+ PreviewPublic:NodeItem.kt$NodeInfoSimplePreview
+ PreviewPublic:NodeStatusIcons.kt$StatusIconsPreview
+ PreviewPublic:Reaction.kt$ReactionItemPreview
+ PreviewPublic:Reaction.kt$ReactionRowPreview
+ PreviewPublic:SatelliteCountInfo.kt$SatelliteCountInfoPreview
+ PreviewPublic:SignalInfo.kt$SignalInfoPreview
+ PreviewPublic:SignalInfo.kt$SignalInfoSelfPreview
+ PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview
+ PreviewPublic:SlidingSelector.kt$SlidingSelectorPreview
+ RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex
+ ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
+ SpacingAroundKeyword:Exceptions.kt$if
+ SpacingAroundKeyword:Exceptions.kt$when
+ SpacingAroundRangeOperator:BatteryInfo.kt$..
+ SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException
+ SwallowedException:ChannelSet.kt$ex: Throwable
+ SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception
+ SwallowedException:Exceptions.kt$ex: Throwable
+ SwallowedException:MeshLog.kt$MeshLog$e: IOException
+ SwallowedException:MeshService.kt$MeshService$ex: BLEException
+ SwallowedException:MeshService.kt$MeshService$ex: CancellationException
+ SwallowedException:NsdManager.kt$ex: IllegalArgumentException
+ SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: DeadObjectException
+ SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException
+ SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException
+ SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException
+ TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable
+ TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception
+ TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable
+ TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception
+ TooGenericExceptionCaught:Exceptions.kt$ex: Throwable
+ TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception
+ TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception
+ TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception
+ TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception
+ TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$ex: Exception
+ TooGenericExceptionCaught:MeshServiceStarter.kt$ServiceStarter$ex: Exception
+ TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception
+ TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: Exception
+ TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException
+ TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable
+ TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable
+ TooGenericExceptionThrown:DeviceVersion.kt$DeviceVersion$throw Exception("Can't parse version $s")
+ TooGenericExceptionThrown:MeshService.kt$MeshService$throw Exception("Can't set user without a NodeInfo")
+ TooGenericExceptionThrown:MeshService.kt$MeshService.<no name provided>$throw Exception("Port numbers must be non-zero!")
+ TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect")
+ TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")
+ TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")
+ TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")
+ TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterfaceLogging
+ TooManyFunctions:MeshService.kt$MeshService : ServiceLogging
+ TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub
+ TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt
+ TooManyFunctions:PacketDao.kt$PacketDao
+ TooManyFunctions:PacketRepository.kt$PacketRepository
+ TooManyFunctions:RadioConfigRepository.kt$RadioConfigRepository
+ TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModelLogging
+ TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService : Logging
+ TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable
+ TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging
+ TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"
+ UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule
+ ViewModelForwarding:Main.kt$MainAppBar( viewModel = uIViewModel, navController = navController, onAction = { action -> when (action) { is NodeMenuAction.MoreDetails -> { navController.navigate( NodesRoutes.NodeDetailGraph(action.node.num), { launchSingleTop = true restoreState = true }, ) } is NodeMenuAction.Share -> sharedContact = action.node else -> {} } }, )
+ ViewModelForwarding:Main.kt$NavGraph( modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(), uIViewModel = uIViewModel, bluetoothViewModel = bluetoothViewModel, navController = navController, )
+ ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)
+ ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)
+ ViewModelForwarding:Message.kt$MessageList( modifier = Modifier.fillMaxSize(), listState = listState, messages = messages, selectedIds = selectedMessageIds, onUnreadChanged = { messageId -> onEvent(MessageScreenEvent.ClearUnreadCount(messageId)) }, onSendReaction = { emoji, id -> onEvent(MessageScreenEvent.SendReaction(emoji, id)) }, viewModel = viewModel, contactKey = contactKey, onReply = { message -> replyingToPacketId = message?.packetId }, onNodeMenuAction = { action -> onEvent(MessageScreenEvent.HandleNodeMenuAction(action)) }, )
+ ViewModelForwarding:NodeDetail.kt$NodeDetailContent( node = node, ourNode = ourNode, metricsState = state, lastTracerouteTime = lastTracerouteTime, availableLogs = availableLogs, uiViewModel = uiViewModel, onAction = { action -> handleNodeAction( action = action, uiViewModel = uiViewModel, node = node, navigateToMessages = navigateToMessages, onNavigateUp = onNavigateUp, onNavigate = onNavigate, viewModel = viewModel, ) }, modifier = modifier, )
+ ViewModelForwarding:NodeScreen.kt$AddContactFAB( modifier = Modifier.animateFloatingActionButton( visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable, alignment = Alignment.BottomEnd, ), model = model, onSharedContactImport = { contact -> model.addSharedContact(contact) }, )
+ ViewModelInjection:DebugSearch.kt$viewModel
+ WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*
+ Wrapping:DebugFilters.kt$(
+ Wrapping:DebugFilters.kt$if (filter in filterTexts) { Icon( imageVector = Icons.Filled.Done, contentDescription = stringResource(id = R.string.debug_filter_included), ) }
+ Wrapping:EditTextPreference.kt$;
+ Wrapping:MQTTRepository.kt$MQTTRepository.<no name provided>$(
+ Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.HandleNodeMenuAction -> { when (val action = event.action) { is NodeMenuAction.DirectMessage -> { val hasPKC = ourNode?.hasPKC == true && action.node.hasPKC val targetChannel = if (hasPKC) { DataPacket.PKC_CHANNEL_INDEX } else { action.node.channel } navigateToMessages("$targetChannel${action.node.user.id}") } is NodeMenuAction.MoreDetails -> navigateToNodeDetails(action.node.num) is NodeMenuAction.Share -> sharedContact = action.node else -> viewModel.handleNodeMenuAction(action) } } is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }
+ Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(
+ Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(port, object : SerialInputOutputManager.Listener { override fun onNewData(data: ByteArray) { listener.onDataReceived(data) } override fun onRunError(e: Exception?) { closed.set(true) ignoreException { port.dtr = false port.rts = false port.close() } closedLatch.countDown() listener.onDisconnected(e) } })
+ Wrapping:SerialInterface.kt$SerialInterface$(
+ Wrapping:SerialInterface.kt$SerialInterface$(device, object : SerialConnectionListener { override fun onMissingPermission() { errormsg("Need permissions for port") } override fun onConnected() { onConnect.invoke() } override fun onDataReceived(bytes: ByteArray) { debug("Received ${bytes.size} byte(s)") bytes.forEach(::readChar) } override fun onDisconnected(thrown: Exception?) { thrown?.let { e -> errormsg("Serial error: $e") } debug("$device disconnected") onDeviceDisconnect(false) } })
+ Wrapping:ServiceClient.kt$ServiceClient$Closeable, Logging
+ Wrapping:SlidingSelector.kt$;
+
+
diff --git a/buildSrc/build.gradle.kts b/config/spotless/copyright.kt
similarity index 86%
rename from buildSrc/build.gradle.kts
rename to config/spotless/copyright.kt
index 1a66d3ac7..5e7317250 100644
--- a/buildSrc/build.gradle.kts
+++ b/config/spotless/copyright.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) $YEAR 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
@@ -15,10 +15,3 @@
* along with this program. If not, see .
*/
-plugins {
- `kotlin-dsl`
-}
-
-repositories {
- mavenCentral()
-}
diff --git a/config/spotless/copyright.kts b/config/spotless/copyright.kts
new file mode 100644
index 000000000..ba305d60f
--- /dev/null
+++ b/config/spotless/copyright.kts
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) $YEAR Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
diff --git a/config/spotless/copyright.txt b/config/spotless/copyright.txt
index 1af8d1868..5e7317250 100644
--- a/config/spotless/copyright.txt
+++ b/config/spotless/copyright.txt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) $YEAR 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
diff --git a/config/spotless/copyright.xml b/config/spotless/copyright.xml
new file mode 100644
index 000000000..11df465ce
--- /dev/null
+++ b/config/spotless/copyright.xml
@@ -0,0 +1,17 @@
+
+
diff --git a/gradle.properties b/gradle.properties
index f9c5614e8..28614960a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -30,9 +30,9 @@ org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
- org.gradle.parallel=true
+org.gradle.parallel=true
-org.gradle.configureondemand=true
+org.gradle.configureondemand=false
# Enable caching between builds.
org.gradle.caching=true
@@ -52,8 +52,9 @@ kotlin.code.style=official
# Disable build features that are enabled by default,
# https://developer.android.com/build/releases/gradle-plugin#default-changes
-android.nonTransitiveRClass=false
-android.nonFinalResIds=false
+android.nonTransitiveRClass=true
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
-org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
\ No newline at end of file
+org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
+
+dependency.analysis.print.build.health=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 79ea15116..e79d0315b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,170 +1,156 @@
#[allow(unused)]
[versions]
-accompanistPermissions = "0.37.3"
-adaptive = "1.2.0-beta02"
-adaptive-navigation-suite = "1.3.2"
agp = "8.13.0"
-androidDocumentationPlugin = "2.0.0"
+androidxComposeMaterial3Adaptive = "1.2.0-beta02"
appcompat = "1.7.1"
-awesome-app-rating = "2.8.0"
coil = "3.3.0"
-compose-bom = "2025.09.00"
-constraintlayout = "2.2.1"
-core-ktx = "1.17.0"
-core-location-altitude = "1.0.0-alpha03"
-core-splashscreen = "1.0.1"
-crashlytics = "3.0.6"
datastore = "1.1.7"
dd-sdk-android = "3.0.0"
-dd-sdk-android-gradle-plugin = "1.19.0"
detekt = "1.23.8"
devtools-ksp = "2.2.20-2.0.2"
-emoji2 = "1.6.0"
-espresso-core = "3.7.0"
-firebase-bom = "34.2.0"
-google-services = "4.4.3"
hilt = "2.57.1"
-hilt-navigation-compose = "1.3.0"
-junit = "4.13.2"
-junit-version = "1.3.0"
kotlin = "2.2.20"
-kotlinx-collections-immutable = "0.4.0"
kotlinx-coroutines-android = "1.10.2"
-kotlinx-serialization-json = "1.9.0"
-kover = "0.9.1"
lifecycle = "2.9.3"
-location-services = "21.3.0"
maps-compose = "6.10.0"
markdownRenderer = "0.35.0"
-material = "1.13.0"
-material3 = "1.5.0-alpha04"
-mgrs = "2.1.3"
navigation = "2.9.4"
navigation3 = "1.0.0-alpha09"
okhttp = "5.1.0"
-org-eclipse-paho-client-mqttv3 = "1.2.5"
-osmbonuspack = "6.9.0"
osmdroid-android = "6.1.20"
-protobuf-gradle-plugin = "0.9.5"
-protobuf-kotlin = "4.32.1"
+protobuf = "4.32.1"
retrofit = "3.0.0"
room = "2.8.0"
-secrets-gradle-plugin = "2.0.1"
-streamsupport-minifuture = "1.7.4"
-timber = "5.0.1"
-usb-serial-android = "3.9.0"
-work-runtime-ktx = "2.10.4"
-zxing-android-embedded = "4.3.0"
-zxing-core = "3.5.3"
-spotless = "7.2.1"
-dokka = "2.0.0"
[libraries]
-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
-activity = { group = "androidx.activity", name = "activity" }
-actvity-ktx = { group = "androidx.activity", name = "activity-ktx" }
-activity-compose = { group = "androidx.activity", name = "activity-compose" }
-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "adaptive" }
-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "adaptive" }
-adaptive-navigation-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation-android", version.ref = "adaptive" }
-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "adaptive-navigation-suite" }
-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-appcompat-resources = { group = "androidx.appcompat", name = "appcompat-resources", version.ref = "appcompat" }
-awesome-app-rating = { group = "com.suddenh4x.ratingdialog", name = "awesome-app-rating", version.ref = "awesome-app-rating" }
-coil = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
-coil-network-core = { group = "io.coil-kt.coil3", name = "coil-network-core", version.ref = "coil" }
-coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
-coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
-core-location-altitude = { group = "androidx.core", name = "core-location-altitude", version.ref = "core-location-altitude" }
-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" }
-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
-dd-sdk-android-compose = { group = "com.datadoghq", name = "dd-sdk-android-compose", version.ref = "dd-sdk-android" }
-dd-sdk-android-logs = { group = "com.datadoghq", name = "dd-sdk-android-logs", version.ref = "dd-sdk-android" }
-dd-sdk-android-okhttp = { group = "com.datadoghq", name = "dd-sdk-android-okhttp", version.ref = "dd-sdk-android" }
-dd-sdk-android-rum = { group = "com.datadoghq", name = "dd-sdk-android-rum", version.ref = "dd-sdk-android" }
-dd-sdk-android-session-replay = { group = "com.datadoghq", name = "dd-sdk-android-session-replay", version.ref = "dd-sdk-android" }
-dd-sdk-android-session-replay-compose = { group = "com.datadoghq", name = "dd-sdk-android-session-replay-compose", version.ref = "dd-sdk-android" }
-dd-sdk-android-timber = { group = "com.datadoghq", name = "dd-sdk-android-timber", version.ref = "dd-sdk-android" }
-dd-sdk-android-trace = { group = "com.datadoghq", name = "dd-sdk-android-trace", version.ref = "dd-sdk-android" }
-dd-sdk-android-trace-otel = { group = "com.datadoghq", name = "dd-sdk-android-trace-otel", version.ref = "dd-sdk-android" }
-detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
-dokka-android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "androidDocumentationPlugin" }
-emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2" }
-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit-version" }
-firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
-hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
-junit = { group = "junit", name = "junit", version.ref = "junit" }
-kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" }
-kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
-kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinx-coroutines-android" }
-kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
-lifecycle-common-java8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "lifecycle" }
-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
-location-services = { group = "com.google.android.gms", name = "play-services-location", version.ref = "location-services" }
-maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "maps-compose" }
-maps-compose-utils = { group = "com.google.maps.android", name = "maps-compose-utils", version.ref = "maps-compose" }
-maps-compose-widgets = { group = "com.google.maps.android", name = "maps-compose-widgets", version.ref = "maps-compose" }
-markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
-markdown-renderer-m3 = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
-markdown-renderer-android = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdownRenderer" }
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
-mgrs = { group = "mil.nga", name = "mgrs", version.ref = "mgrs" }
-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigation" }
-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
-okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
-org-eclipse-paho-client-mqttv3 = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "org-eclipse-paho-client-mqttv3" }
-osmbonuspack = { group = "com.github.MKergall", name = "osmbonuspack", version.ref = "osmbonuspack" }
-osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" }
-osmdroid-geopackage = { group = "org.osmdroid", name = "osmdroid-geopackage", version.ref = "osmdroid-android" }
-protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf-kotlin" }
-protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf-kotlin" }
-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
-retrofit2-kotlin-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
-streamsupport-minifuture = { group = "net.sourceforge.streamsupport", name = "streamsupport-minifuture", version.ref = "streamsupport-minifuture" }
-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
-usb-serial-android = { group = "com.github.mik3y", name = "usb-serial-for-android", version.ref = "usb-serial-android" }
-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime-ktx" }
-zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
-zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing-core" }
+accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" }
+activity = { module = "androidx.activity:activity" }
+actvity-ktx = { module = "androidx.activity:activity-ktx" }
+activity-compose = { module = "androidx.activity:activity-compose" }
+android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version = "2.1.5" }
+androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version = "1.3.0" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version = "2025.08.01" }
+androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version = "1.8.0-alpha07" }
+androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
+androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
+androidx-compose-material3-navigationSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" }
+androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "androidxComposeMaterial3Adaptive" }
+androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" }
+androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" }
+androidx-compose-material3-windowSizeClass = { module = "androidx.compose.material3:material3-window-size-class" }
+androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
+androidx-compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version = "1.7.6" }
+androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-compose-ui-testManifest = { module = "androidx.compose.ui:ui-test-manifest" }
+androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
+appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "appcompat" }
+awesome-app-rating = { module = "com.suddenh4x.ratingdialog:awesome-app-rating", version = "2.8.0" }
+coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
+coil-network-core = { module = "io.coil-kt.coil3:coil-network-core", version.ref = "coil" }
+coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
+coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
+compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
+constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" }
+core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" }
+core-location-altitude = { module = "androidx.core:core-location-altitude", version = "1.0.0-alpha03" }
+core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" }
+datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
+datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
+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-session-replay = { module = "com.datadoghq:dd-sdk-android-session-replay", version.ref = "dd-sdk-android" }
+dd-sdk-android-session-replay-compose = { module = "com.datadoghq:dd-sdk-android-session-replay-compose", 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" }
+dd-sdk-android-trace-otel = { module = "com.datadoghq:dd-sdk-android-trace-otel", version.ref = "dd-sdk-android" }
+dokka-android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version = "2.0.0" }
+emoji2-emojipicker = { module = "androidx.emoji2:emoji2-emojipicker", version = "1.6.0" }
+espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
+ext-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.2.0" }
+firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
+firebase-performance = { module = "com.google.firebase:firebase-perf" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
+hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
+hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
+hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
+junit = { module = "junit:junit", version = "4.13.2" }
+kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
+kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinx-coroutines-android" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.9.0" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
+lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
+lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
+lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
+lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
+lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
+location-services = { module = "com.google.android.gms:play-services-location", version = "21.3.0" }
+maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" }
+maps-compose-utils = { module = "com.google.maps.android:maps-compose-utils", version.ref = "maps-compose" }
+maps-compose-widgets = { module = "com.google.maps.android:maps-compose-widgets", version.ref = "maps-compose" }
+markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
+markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
+markdown-renderer-android = { module = "com.mikepenz:multiplatform-markdown-renderer-android", version.ref = "markdownRenderer" }
+material = { module = "com.google.android.material:material", version = "1.13.0" }
+mgrs = { module = "mil.nga:mgrs", version = "2.1.3" }
+navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
+navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigation" }
+navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
+navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
+okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
+org-eclipse-paho-client-mqttv3 = { module = "org.eclipse.paho:org.eclipse.paho.client.mqttv3", version = "1.2.5" }
+osmbonuspack = { module = "com.github.MKergall:osmbonuspack", version = "6.9.0" }
+osmdroid-android = { module = "org.osmdroid:osmdroid-android", version.ref = "osmdroid-android" }
+osmdroid-geopackage = { module = "org.osmdroid:osmdroid-geopackage", version.ref = "osmdroid-android" }
+protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
+protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
+retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+retrofit2-kotlin-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
+room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
+room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
+room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
+streamsupport-minifuture = { module = "net.sourceforge.streamsupport:streamsupport-minifuture", version = "1.7.4" }
+timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" }
+usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version = "3.9.0" }
+work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.10.4" }
+zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version = "4.3.0" }
+zxing-core = { module = "com.google.zxing:core", version = "3.5.3" }
+truth = { module = "com.google.truth:truth", version = "1.4.5" }
+
+# Dependencies of the included build-logic
+android-gradleApiPlugin = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
+android-tools-common = { module = "com.android.tools:common", version = "31.13.0" }
+compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
+firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" }
+firebase-performance-gradlePlugin = { module = "com.google.firebase:perf-plugin", version = "2.0.1" }
+ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" }
+room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" }
+spotless-gradlePlugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.2.1" }
+androidx-lint-gradle = { module = "androidx.lint:lint-gradle", version = "1.0.0-alpha05" }
+detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
+detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.4.27" }
+detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
[bundles]
# Core AndroidX
androidx = ["core-ktx", "appcompat", "appcompat-resources", "actvity-ktx", "activity-compose"]
# UI
-ui = ["material", "constraintlayout", "compose-material3", "compose-material-icons-extended", "compose-ui-tooling-preview", "compose-runtime-livedata"]
-adaptive = ["adaptive", "adaptive-layout", "adaptive-navigation", "adaptive-navigation-android", "adaptive-navigation-suite"]
-ui-tooling = ["compose-ui-tooling"] #Separate for debugImplementation
+ui = ["material", "constraintlayout", "androidx-compose-material3", "androidx-compose-material-iconsExtended", "androidx-compose-ui-tooling-preview", "compose-runtime-livedata"]
+adaptive = ["androidx-compose-material3-adaptive", "androidx-compose-material3-adaptive-layout", "androidx-compose-material3-adaptive-navigation", "androidx-compose-material3-navigationSuite"]
+ui-tooling = ["androidx-compose-ui-tooling"] #Separate for debugImplementation
markdown = ["markdown-renderer", "markdown-renderer-m3", "markdown-renderer-android"]
# Lifecycle
@@ -188,8 +174,8 @@ hilt = ["hilt-android", "hilt-navigation-compose"]
# Testing
testing = ["junit", "ext-junit"]
-testing-android = ["espresso-core", "compose-ui-test-junit4"]
-testing-android-manifest = ["compose-ui-test-manifest"]
+testing-android = ["espresso-core", "androidx-compose-ui-test"]
+testing-android-manifest = ["androidx-compose-ui-testManifest"]
testing-hilt = ["hilt-android-testing"]
testing-navigation = ["navigation-testing"]
testing-room = ["room-testing"]
@@ -201,8 +187,7 @@ osm = ["osmdroid-android", "osmbonuspack", "mgrs"]
maps-compose = ["location-services", "maps-compose", "maps-compose-utils", "maps-compose-widgets"]
# Firebase
-firebase = ["firebase-analytics", "firebase-crashlytics"]
-
+firebase = ["firebase-analytics", "firebase-crashlytics", "firebase-performance"]
# Datadog
datadog = ["dd-sdk-android-compose", "dd-sdk-android-logs", "dd-sdk-android-okhttp", "dd-sdk-android-rum", "dd-sdk-android-session-replay", "dd-sdk-android-session-replay-compose", "dd-sdk-android-timber", "dd-sdk-android-trace", "dd-sdk-android-trace-otel"]
@@ -218,19 +203,36 @@ coil = ["coil", "coil-network-core", "coil-network-okhttp", "coil-svg"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
+android-lint = { id = "com.android.lint", version.ref = "agp" }
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version.ref = "dd-sdk-android-gradle-plugin" }
+datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version = "1.19.0" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
+dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version = "3.0.2" }
+dokka = { id = "org.jetbrains.dokka", version = "2.0.0" }
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
-firebase-crashlytics = { id = "com.google.firebase.crashlytics" , version.ref = "crashlytics" }
-google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" }
+firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.1" }
+google-services = { id = "com.google.gms.google-services", version = "4.4.3" }
hilt = { id = "com.google.dagger.hilt.android" , version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
-protobuf = { id = "com.google.protobuf", version.ref = "protobuf-gradle-plugin" }
-secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" }
-spotless = { id = "com.diffplug.spotless", version .ref= "spotless" }
+kover = { id = "org.jetbrains.kotlinx.kover", version = "0.9.1" }
+protobuf = { id = "com.google.protobuf", version = "0.9.5" }
+room = { id = "androidx.room", version.ref = "room" }
+secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" }
+spotless = { id = "com.diffplug.spotless", version = "7.2.1" }
+
+# Plugins defined by this project
+meshtastic-android-application = { id = "meshtastic.android.application" }
+meshtastic-android-library = { id = "meshtastic.android.library" }
+meshtastic-android-application-flavors = { id = "meshtastic.android.application.flavors" }
+meshtastic-android-lint = { id = "meshtastic.android.lint" }
+meshtastic-android-application-compose = { id = "meshtastic.android.application.compose" }
+meshtastic-android-application-firebase = { id = "meshtastic.android.application.firebase" }
+meshtastic-android-library-compose = { id = "meshtastic.android.library.compose" }
+meshtastic-android-room = { id = "meshtastic.android.room" }
+meshtastic-android-test = { id = "meshtastic.android.test" }
+meshtastic-hilt = { id = "meshtastic.hilt" }
+meshtastic-detekt = { id = "meshtastic.detekt" }
diff --git a/mesh_service_example/build.gradle.kts b/mesh_service_example/build.gradle.kts
index 49684c5fa..e38ef90ac 100644
--- a/mesh_service_example/build.gradle.kts
+++ b/mesh_service_example/build.gradle.kts
@@ -1,3 +1,21 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+import org.gradle.kotlin.dsl.androidTestImplementation
+
/*
* Copyright (c) 2025 Meshtastic LLC
*
@@ -16,53 +34,26 @@
*/
plugins {
- alias(libs.plugins.android.application)
- alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.meshtastic.android.application)
+ alias(libs.plugins.meshtastic.android.application.compose)
+ alias(libs.plugins.meshtastic.android.lint)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
- alias(libs.plugins.compose)
alias(libs.plugins.protobuf)
- alias(libs.plugins.detekt)
+ alias(libs.plugins.spotless)
alias(libs.plugins.kover)
}
android {
namespace = "com.meshtastic.android.meshserviceexample"
- compileSdk = Configs.COMPILE_SDK
-
- defaultConfig {
- applicationId = "com.meshtastic.android.meshserviceexample"
- minSdk = 26
- targetSdk = Configs.TARGET_SDK
- versionCode = 1
- versionName = "1.0"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- buildFeatures {
- aidl = true
- }
+ buildFeatures { aidl = true }
}
-kotlin {
- jvmToolchain(21)
-}
+kotlin { jvmToolchain(21) }
// per protobuf-gradle-plugin docs, this is recommended for android
protobuf {
- protoc {
- artifact = libs.protobuf.protoc.get().toString()
- }
+ protoc { protoc { artifact = "com.google.protobuf:protoc:4.32.0" } }
generateProtoTasks {
all().forEach { task ->
task.builtins {
@@ -82,20 +73,11 @@ dependencies {
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
- implementation(libs.bundles.androidx)
implementation(libs.bundles.protobuf)
implementation(libs.kotlinx.serialization.json)
// OSM
implementation(libs.bundles.osm)
- implementation(libs.osmdroid.geopackage) {
- exclude(group = "com.j256.ormlite")
- }
- detektPlugins(libs.detekt.formatting)
-}
-
-detekt {
- config.setFrom("../config/detekt/detekt.yml")
- baseline = file("../config/detekt/detekt-baseline-meshserviceexample.xml")
+ implementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
}
diff --git a/network/build.gradle.kts b/network/build.gradle.kts
index e6b38c49f..42434013c 100644
--- a/network/build.gradle.kts
+++ b/network/build.gradle.kts
@@ -15,63 +15,30 @@
* along with this program. If not, see .
*/
-import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-
plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
- alias(libs.plugins.hilt)
- alias(libs.plugins.devtools.ksp)
- alias(libs.plugins.detekt)
- id("kotlinx-serialization")
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.android.lint)
+ alias(libs.plugins.meshtastic.hilt)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.spotless)
alias(libs.plugins.dokka)
alias(libs.plugins.kover)
+ alias(libs.plugins.protobuf)
}
android {
- buildFeatures {
- buildConfig = true
- }
- compileSdk = Configs.COMPILE_SDK
- defaultConfig {
- minSdk = Configs.MIN_SDK
- }
+ buildFeatures { buildConfig = true }
+ compileSdk = 36
+ defaultConfig { minSdk = 26 }
namespace = "com.geeksville.mesh.network"
-
- flavorDimensions += "default"
- productFlavors {
- create("fdroid") {
- dimension = "default"
- }
- create("google") {
- dimension = "default"
- }
- }
}
-kotlin {
- jvmToolchain(21)
-}
+kotlin { jvmToolchain(21) }
dependencies {
- implementation(libs.bundles.hilt)
implementation(libs.bundles.retrofit)
implementation(libs.bundles.coil)
"googleImplementation"(libs.bundles.datadog)
- ksp(libs.hilt.compiler)
implementation(libs.kotlinx.serialization.json)
- detektPlugins(libs.detekt.formatting)
-}
-
-detekt {
- config.setFrom("../config/detekt/detekt.yml")
- baseline = file("../config/detekt/detekt-baseline-network.xml")
- source.setFrom(
- files(
- "src/main/java",
- "google/main/java",
- "fdroid/main/java",
- )
- )
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8cefe5bb5..f42133e53 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,9 +18,10 @@ import org.gradle.kotlin.dsl.maven
*/
include(":app", ":network", ":mesh_service_example")
-rootProject.name = "Meshtastic Android"
+rootProject.name = "MeshtasticAndroid"
pluginManagement {
+ includeBuild("build-logic")
repositories {
google()
mavenCentral()
@@ -63,4 +64,4 @@ toolchainManagement {
}
}
}
-}
+}
\ No newline at end of file