refactor: migrate core modules to Kotlin Multiplatform and consolidat… (#4735)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-06 16:06:50 -06:00 committed by GitHub
parent f3775a601c
commit cffbd08806
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
265 changed files with 1383 additions and 1340 deletions

View file

@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
}
configure<ApplicationExtension> {
namespace = configProperties.getProperty("APPLICATION_ID")
namespace = "org.meshtastic.app"
signingConfigs {
create("release") {
@ -150,7 +150,7 @@ configure<ApplicationExtension> {
includeInBundle = false
}
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
testInstrumentationRunner = "org.meshtastic.app.TestRunner"
}
// Configure existing product flavors (defined by convention plugin)
@ -210,7 +210,6 @@ project.afterEvaluate {
}
dependencies {
implementation(projects.core.analytics)
implementation(projects.core.ble)
implementation(projects.core.common)
implementation(projects.core.data)
@ -251,10 +250,14 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)
implementation(libs.androidx.core.splashscreen)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.org.eclipse.paho.client.mqttv3)
implementation(libs.usb.serial.android)
implementation(libs.androidx.work.runtime.ktx)
@ -275,6 +278,16 @@ dependencies {
googleImplementation(libs.location.services)
googleImplementation(libs.play.services.maps)
googleImplementation(libs.dd.sdk.android.okhttp)
googleImplementation(libs.dd.sdk.android.compose)
googleImplementation(libs.dd.sdk.android.logs)
googleImplementation(libs.dd.sdk.android.rum)
googleImplementation(libs.dd.sdk.android.timber)
googleImplementation(libs.dd.sdk.android.trace)
googleImplementation(libs.dd.sdk.android.trace.otel)
googleImplementation(platform(libs.firebase.bom))
googleImplementation(libs.firebase.analytics)
googleImplementation(libs.firebase.crashlytics)
fdroidImplementation(libs.osmdroid.android)
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }

View file

@ -2,91 +2,26 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>CyclomaticComplexMethod:BleError.kt$BleError.Companion$fun from(exception: Throwable): BleError</ID>
<ID>CyclomaticComplexMethod:MeshMessageProcessor.kt$MeshMessageProcessor$private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?)</ID>
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
<ID>MagicNumber:Contacts.kt$7</ID>
<ID>MagicNumber:Contacts.kt$8</ID>
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$0xff</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$3</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
<ID>MagicNumber:UIState.kt$4</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"</ID>
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"</ID>
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ </ID>
<ID>NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ </ID>
<ID>NoConsecutiveBlankLines:DebugLogFile.kt$ </ID>
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$@Suppress("LongMethod") private fun handleStoreForwardPlusPlus(packet: MeshPacket)</ID>
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>MaxLineLength:DataSourceModule.kt$DataSourceModule$fun</ID>
<ID>ParameterListWrapping:DataSourceModule.kt$DataSourceModule$(impl: BootloaderOtaQuirksJsonDataSourceImpl)</ID>
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
<ID>SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException</ID>
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MeshDataHandler.kt$MeshDataHandler$e: Exception</ID>
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception</ID>
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$t: Throwable</ID>
<ID>TooGenericExceptionCaught:RadioInterfaceService.kt$RadioInterfaceService$t: Throwable</ID>
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect")</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")</ID>
<ID>TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
package org.meshtastic.app
import android.app.Application
import android.content.Context
@ -24,7 +23,6 @@ import dagger.hilt.android.testing.HiltTestApplication
@Suppress("unused")
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.filter
package org.meshtastic.app.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
@ -48,6 +48,8 @@ class MessageFilterIntegrationTest {
fun filterPrefsIntegration() = runTest {
filterPrefs.setFilterEnabled(true)
filterPrefs.setFilterWords(setOf("test", "spam"))
// Wait briefly for DataStore to process the writes and flows to emit
kotlinx.coroutines.delay(100)
filterService.rebuildPatterns()
assertTrue(filterService.shouldFilter("this is a test message"))

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.analytics
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Inject
/**
* F-Droid specific implementation of [PlatformAnalytics]. This provides no-op implementations for analytics and other
* platform services.
*/
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
init {
// For F-Droid builds we don't initialize external analytics services.
// In debug builds we attach a DebugTree for convenient local logging, but
// release builds rely on system logging only.
if (BuildConfig.DEBUG) {
Logger.setMinSeverity(Severity.Debug)
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode)." }
} else {
Logger.setMinSeverity(Severity.Info)
Logger.i { "F-Droid platform no-op analytics initialized." }
}
}
override fun setDeviceAttributes(firmwareVersion: String, model: String) {
// No-op for F-Droid
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
}
override val isPlatformServicesAvailable: Boolean
get() = false
override fun track(event: String, vararg properties: DataPair) {
Logger.d { "Track called: event=$event, properties=${properties.toList()}" }
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.NetworkDeviceHardware
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class FDroidNetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(buildConfigProvider: BuildConfigProvider): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(
interceptor =
HttpLoggingInterceptor().apply {
if (buildConfigProvider.isDebug) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
@Provides
@Singleton
fun provideApiService(): ApiService = object : ApiService {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")
override suspend fun getFirmwareReleases(): NetworkFirmwareReleases =
throw NotImplementedError("API calls to getFirmwareReleases are not supported on Fdroid builds.")
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.app.analytics.FdroidPlatformAnalytics
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Singleton
/** Hilt module to provide the [FdroidPlatformAnalytics] for the fdroid flavor. */
@Module
@InstallIn(SingletonComponent::class)
abstract class FdroidPlatformAnalyticsModule {
@Binds
@Singleton
abstract fun bindPlatformHelper(fdroidPlatformAnalytics: FdroidPlatformAnalytics): PlatformAnalytics
}

View file

@ -0,0 +1,315 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.analytics
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.provider.Settings
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
import com.datadog.android.core.configuration.Configuration
import com.datadog.android.log.Logger
import com.datadog.android.log.Logs
import com.datadog.android.log.LogsConfiguration
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.GlobalRumMonitor
import com.datadog.android.rum.Rum
import com.datadog.android.rum.RumConfiguration
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailabilityLight
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus
import com.google.firebase.analytics.FirebaseAnalytics.ConsentType
import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize
import dagger.hilt.android.qualifiers.ApplicationContext
import io.opentelemetry.api.GlobalOpenTelemetry
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.app.BuildConfig
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Inject
import co.touchlab.kermit.Logger as KermitLogger
/**
* Google Play Services specific implementation of [PlatformAnalytics]. This helper initializes and manages Firebase and
* Datadog services, and subscribes to analytics preference changes to update consent accordingly.
*
* This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and
* respect privacy-focused environments.
*/
class GooglePlatformAnalytics
@Inject
constructor(
@ApplicationContext private val context: Context,
private val analyticsPrefs: AnalyticsPrefs,
) : PlatformAnalytics {
private val sampleRate = 100f.takeIf { BuildConfig.DEBUG } ?: 10f // For Datadog remote sample rate
private var datadogLogger: Logger? = null
private var isFirebaseInitialized = false
private val isInTestLab: Boolean
get() {
val testLabSetting = Settings.System.getString(context.contentResolver, "firebase.test.lab")
return "true" == testLabSetting
}
companion object {
private const val TAG = "GooglePlatformAnalytics"
private const val SERVICE_NAME = "org.meshtastic"
private const val KEY_PRIORITY = "priority"
private const val KEY_TAG = "tag"
private const val KEY_MESSAGE = "message"
}
init {
// Setup Kermit log writers immediately, they will handle delayed SDK initialization gracefully.
val writers = buildList {
add(DatadogLogWriter())
add(CrashlyticsLogWriter())
if (BuildConfig.DEBUG) {
add(co.touchlab.kermit.LogcatWriter())
}
}
KermitLogger.setLogWriters(writers)
KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
// Initial consent state
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed.value)
// Subscribe to analytics preference changes
analyticsPrefs.analyticsAllowed
.onEach { allowed -> updateAnalyticsConsent(allowed) }
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
}
/**
* Ensures that Datadog and Firebase SDKs are initialized if allowed. This is called lazily when consent is granted.
*/
private fun ensureInitialized() {
if (!analyticsPrefs.analyticsAllowed.value || isInTestLab) return
if (!Datadog.isInitialized()) {
initDatadog(context as Application)
datadogLogger =
Logger.Builder()
.setService(SERVICE_NAME)
.setNetworkInfoEnabled(false) // Disable to avoid collecting Local IP/SSID
.setRemoteSampleRate(sampleRate)
.setBundleWithTraceEnabled(true)
.setBundleWithRumEnabled(true)
.build()
}
if (!isFirebaseInitialized) {
initCrashlytics(context as Application)
isFirebaseInitialized = true
}
}
private fun initDatadog(application: Application) {
val configuration =
Configuration.Builder(
clientToken = BuildConfig.datadogClientToken,
env = if (BuildConfig.DEBUG) "debug" else "release",
variant = BuildConfig.FLAVOR,
)
.useSite(DatadogSite.US5)
.setCrashReportsEnabled(true)
.setUseDeveloperModeWhenDebuggable(true)
.build()
// Initialize with PENDING, consent will be updated via updateAnalyticsConsent
Datadog.initialize(application, configuration, TrackingConsent.PENDING)
Datadog.setVerbosity(if (BuildConfig.DEBUG) android.util.Log.DEBUG else android.util.Log.WARN)
val rumConfiguration =
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
.trackAnonymousUser(true)
.trackBackgroundEvents(false) // Disable background noise
.trackFrustrations(false) // Disable click-tracking based frustration detection
.trackLongTasks()
.trackNonFatalAnrs(true)
.setSessionSampleRate(sampleRate)
.build()
Rum.enable(rumConfiguration)
val logsConfig = LogsConfiguration.Builder().build()
Logs.enable(logsConfig)
val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(false).build()
Trace.enable(traceConfig)
GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME))
}
private fun initCrashlytics(application: Application) {
Firebase.initialize(application)
// Deny all ad-related consent types by default to minimize tracking noise
Firebase.analytics.setConsent(
mapOf(
ConsentType.AD_STORAGE to ConsentStatus.DENIED,
ConsentType.AD_USER_DATA to ConsentStatus.DENIED,
ConsentType.AD_PERSONALIZATION to ConsentStatus.DENIED,
ConsentType.ANALYTICS_STORAGE to ConsentStatus.DENIED,
),
)
// Explicitly disable analytics collection until we confirm user consent
Firebase.analytics.setAnalyticsCollectionEnabled(false)
}
/**
* Updates the consent status for analytics, performance, and crash reporting services.
*
* @param allowed True if analytics are allowed, false otherwise.
*/
fun updateAnalyticsConsent(allowed: Boolean) {
if (isInTestLab) return
if (allowed) {
ensureInitialized()
}
KermitLogger.i { if (allowed) "Analytics enabled" else "Analytics disabled" }
if (Datadog.isInitialized()) {
Datadog.setTrackingConsent(if (allowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
}
if (isFirebaseInitialized) {
Firebase.crashlytics.isCrashlyticsCollectionEnabled = allowed
Firebase.analytics.setAnalyticsCollectionEnabled(allowed)
if (allowed) {
Firebase.crashlytics.sendUnsentReports()
// Ensure ad-related PII collection remains disabled even if analytics is allowed
Firebase.analytics.setUserProperty("allow_personalized_ads", "false")
}
// Manage Analytics Storage consent for Advanced Consent Mode
val consentStatus = if (allowed) ConsentStatus.GRANTED else ConsentStatus.DENIED
Firebase.analytics.setConsent(
mapOf(
ConsentType.ANALYTICS_STORAGE to consentStatus,
// Keep ad-related types explicitly denied
ConsentType.AD_STORAGE to ConsentStatus.DENIED,
ConsentType.AD_USER_DATA to ConsentStatus.DENIED,
ConsentType.AD_PERSONALIZATION to ConsentStatus.DENIED,
),
)
}
}
override fun setDeviceAttributes(firmwareVersion: String, model: String) {
if (!Datadog.isInitialized() || !GlobalRumMonitor.isRegistered()) return
GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion.extractSemanticVersion())
GlobalRumMonitor.get().addAttribute("device_hardware", model)
}
private val isGooglePlayAvailable: Boolean
get() =
GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(context).let {
it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
}
override val isPlatformServicesAvailable: Boolean
get() = isGooglePlayAvailable
private inner class CrashlyticsLogWriter : LogWriter() {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
if (!isFirebaseInitialized) return
if (!Firebase.crashlytics.isCrashlyticsCollectionEnabled) return
// Add the log to the Crashlytics log buffer so it appears in reports
Firebase.crashlytics.log("$severity/$tag: $message")
// Filter out normal coroutine cancellations
if (throwable is CancellationException) return
// Only record non-fatal exceptions for actual Errors (Severity.Error or Severity.Assert)
if (severity >= Severity.Error) {
if (throwable != null) {
Firebase.crashlytics.recordException(throwable)
} else {
Firebase.crashlytics.setCustomKeys {
key(KEY_PRIORITY, severity.ordinal)
key(KEY_TAG, tag)
key(KEY_MESSAGE, message)
}
Firebase.crashlytics.recordException(Exception(message))
}
}
}
}
private inner class DatadogLogWriter : LogWriter() {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
val logger = datadogLogger ?: return
val datadogPriority =
when (severity) {
Severity.Verbose -> android.util.Log.VERBOSE
Severity.Debug -> android.util.Log.DEBUG
Severity.Info -> android.util.Log.INFO
Severity.Warn -> android.util.Log.WARN
Severity.Error -> android.util.Log.ERROR
Severity.Assert -> android.util.Log.ASSERT
}
logger.log(datadogPriority, message, throwable, mapOf("tag" to tag))
}
}
private fun String.extractSemanticVersion(): String {
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?$".toRegex()
val matchResult = regex.find(this)
return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".") ?: this
}
override fun track(event: String, vararg properties: DataPair) {
if (!isFirebaseInitialized) return
val bundle = Bundle()
properties.forEach {
val value = it.value
when (value) {
is Double -> bundle.putDouble(it.name, value)
is Int -> bundle.putLong(it.name, value.toLong()) // Firebase expects Long for integer values in bundles
is Long -> bundle.putLong(it.name, value)
is Float -> bundle.putDouble(it.name, value.toDouble())
is String -> bundle.putString(it.name, value) // Explicitly handle String
else -> bundle.putString(it.name, value.toString()) // Fallback for other types
}
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : $value)" }
}
Firebase.analytics.logEvent(event, bundle)
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import java.io.File
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
interface GoogleNetworkModule {
@Binds @Singleton
fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService
companion object {
@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context,
buildConfigProvider: BuildConfigProvider,
): OkHttpClient = OkHttpClient.Builder()
.cache(
cache =
Cache(
directory = File(context.applicationContext.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L, // 50 MiB
),
)
.addInterceptor(
interceptor =
HttpLoggingInterceptor().apply {
if (buildConfigProvider.isDebug) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.addInterceptor(
interceptor = DatadogInterceptor.Builder(tracedHosts = listOf("meshtastic.org")).build(),
)
.eventListenerFactory(eventListenerFactory = DatadogEventListener.Factory())
.build()
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.app.analytics.GooglePlatformAnalytics
import org.meshtastic.core.repository.PlatformAnalytics
import javax.inject.Singleton
/** Hilt module to provide the [GooglePlatformAnalytics] for the google flavor. */
@Module
@InstallIn(SingletonComponent::class)
abstract class GooglePlatformAnalyticsModule {
@Binds @Singleton
abstract fun bindPlatformHelper(googlePlatformHelper: GooglePlatformAnalytics): PlatformAnalytics
}

View file

@ -102,7 +102,7 @@
</queries>
<application
android:name="com.geeksville.mesh.MeshUtilApplication"
android:name="org.meshtastic.app.MeshUtilApplication"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
@ -152,7 +152,7 @@
<!-- This is the public API for doing mesh radio operations from android apps -->
<service
android:name="com.geeksville.mesh.service.MeshService"
android:name="org.meshtastic.app.service.MeshService"
android:enabled="true"
android:foregroundServiceType="connectedDevice|location"
android:exported="true" tools:ignore="ExportedActivity">
@ -171,7 +171,7 @@
</service>
<activity
android:name="com.geeksville.mesh.MainActivity"
android:name="org.meshtastic.app.MainActivity"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize"
android:exported="true">
@ -228,7 +228,7 @@
android:resource="@xml/device_filter" />
</activity>
<receiver android:name="com.geeksville.mesh.service.BootCompleteReceiver"
<receiver android:name="org.meshtastic.app.service.BootCompleteReceiver"
android:exported="false">
<!-- handle boot events -->
<intent-filter>
@ -252,12 +252,12 @@
android:path="com.geeksville.mesh" /> -->
</intent-filter>
</receiver>
<receiver android:name="com.geeksville.mesh.service.ReplyReceiver" android:exported="false" />
<receiver android:name="com.geeksville.mesh.service.MarkAsReadReceiver" android:exported="false" />
<receiver android:name="com.geeksville.mesh.service.ReactionReceiver" android:exported="false" />
<receiver android:name="org.meshtastic.app.service.ReplyReceiver" android:exported="false" />
<receiver android:name="org.meshtastic.app.service.MarkAsReadReceiver" android:exported="false" />
<receiver android:name="org.meshtastic.app.service.ReactionReceiver" android:exported="false" />
<receiver
android:name="com.geeksville.mesh.widget.LocalStatsWidgetReceiver"
android:name="org.meshtastic.app.widget.LocalStatsWidgetReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

View file

@ -14,22 +14,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
package org.meshtastic.app
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.geeksville.mesh.repository.radio.AndroidRadioInterfaceService
import com.geeksville.mesh.service.AndroidAppWidgetUpdater
import com.geeksville.mesh.service.AndroidMeshLocationManager
import com.geeksville.mesh.service.AndroidMeshWorkerManager
import com.geeksville.mesh.service.MeshServiceNotificationsImpl
import com.geeksville.mesh.service.ServiceBroadcasts
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.app.repository.radio.AndroidRadioInterfaceService
import org.meshtastic.app.service.AndroidAppWidgetUpdater
import org.meshtastic.app.service.AndroidMeshLocationManager
import org.meshtastic.app.service.AndroidMeshWorkerManager
import org.meshtastic.app.service.MeshServiceNotificationsImpl
import org.meshtastic.app.service.ServiceBroadcasts
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.repository.MeshServiceNotifications

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
package org.meshtastic.app
import android.app.PendingIntent
import android.app.TaskStackBuilder
@ -43,12 +43,12 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
import org.meshtastic.app.model.UIViewModel
import org.meshtastic.app.ui.MainScreen
import org.meshtastic.core.model.util.dispatchMeshtasticUri
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.resources.Res

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
package org.meshtastic.app
import android.content.Context
import android.content.Context.BIND_ABOVE_CLIENT
@ -23,11 +23,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.startService
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
import org.meshtastic.app.service.MeshService
import org.meshtastic.app.service.startService
import org.meshtastic.core.common.util.SequentialJob
import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.BindFailedException

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh
package org.meshtastic.app
import android.app.Application
import android.appwidget.AppWidgetProviderInfo
@ -27,8 +27,6 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import co.touchlab.kermit.Logger
import com.geeksville.mesh.widget.LocalStatsWidgetReceiver
import com.geeksville.mesh.worker.MeshLogCleanupWorker
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@ -42,6 +40,8 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
import org.meshtastic.app.widget.LocalStatsWidgetReceiver
import org.meshtastic.app.worker.MeshLogCleanupWorker
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshLogPrefs
@ -96,7 +96,7 @@ open class MeshUtilApplication :
val entryPoint =
EntryPointAccessors.fromApplication(
this@MeshUtilApplication,
com.geeksville.mesh.widget.LocalStatsWidget.LocalStatsWidgetEntryPoint::class.java,
org.meshtastic.app.widget.LocalStatsWidget.LocalStatsWidgetEntryPoint::class.java,
)
try {
// Wait for real data for up to 30 seconds before pushing an updated preview

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import androidx.work.WorkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import org.meshtastic.core.di.CoroutineDispatchers
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideCoroutineDispatchers(): CoroutineDispatchers =
CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default)
@Provides
@Singleton
fun provideWorkManager(@ApplicationContext context: Context): WorkManager = WorkManager.getInstance(context)
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import android.location.LocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideLocationManager(@ApplicationContext context: Context): LocationManager =
context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSourceImpl
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSourceImpl
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSourceImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface DataSourceModule {
@Binds
@Singleton
fun bindDeviceHardwareJsonDataSource(impl: DeviceHardwareJsonDataSourceImpl): DeviceHardwareJsonDataSource
@Binds
@Singleton
fun bindFirmwareReleaseJsonDataSource(impl: FirmwareReleaseJsonDataSourceImpl): FirmwareReleaseJsonDataSource
@Binds
@Singleton
fun bindBootloaderOtaQuirksJsonDataSource(
impl: BootloaderOtaQuirksJsonDataSourceImpl,
): BootloaderOtaQuirksJsonDataSource
}

View file

@ -0,0 +1,165 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.core.okio.OkioStorage
import androidx.datastore.dataStoreFile
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import org.meshtastic.core.datastore.KEY_APP_INTRO_COMPLETED
import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN
import org.meshtastic.core.datastore.KEY_NODE_SORT
import org.meshtastic.core.datastore.KEY_ONLY_DIRECT
import org.meshtastic.core.datastore.KEY_ONLY_ONLINE
import org.meshtastic.core.datastore.KEY_SHOW_IGNORED
import org.meshtastic.core.datastore.KEY_THEME
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
import javax.inject.Qualifier
import javax.inject.Singleton
private const val USER_PREFERENCES_NAME = "user_preferences"
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DataStoreScope
@InstallIn(SingletonComponent::class)
@Module
object DataStoreModule {
@Provides
@Singleton
@DataStoreScope
fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Singleton
@Provides
fun providePreferencesDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
migrations =
listOf(
SharedPreferencesMigration(context = appContext, sharedPreferencesName = USER_PREFERENCES_NAME),
SharedPreferencesMigration(
context = appContext,
sharedPreferencesName = "ui-prefs",
keysToMigrate =
setOf(
KEY_APP_INTRO_COMPLETED,
KEY_THEME,
KEY_NODE_SORT,
KEY_INCLUDE_UNKNOWN,
KEY_ONLY_ONLINE,
KEY_ONLY_DIRECT,
KEY_SHOW_IGNORED,
),
),
),
scope = scope,
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
)
@Singleton
@Provides
fun provideLocalConfigDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalConfig> = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = LocalConfigSerializer,
producePath = { appContext.dataStoreFile("local_config.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }),
scope = scope,
)
@Singleton
@Provides
fun provideModuleConfigDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalModuleConfig> = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = ModuleConfigSerializer,
producePath = { appContext.dataStoreFile("module_config.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }),
scope = scope,
)
@Singleton
@Provides
fun provideChannelSetDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<ChannelSet> = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = ChannelSetSerializer,
producePath = { appContext.dataStoreFile("channel_set.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }),
scope = scope,
)
@Singleton
@Provides
fun provideLocalStatsDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalStats> = DataStoreFactory.create(
storage =
OkioStorage(
fileSystem = FileSystem.SYSTEM,
serializer = LocalStatsSerializer,
producePath = { appContext.dataStoreFile("local_stats.pb").toOkioPath() },
),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalStats() }),
scope = scope,
)
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
interface DatabaseModule {
@Binds
@Singleton
fun bindDatabaseManager(
impl: org.meshtastic.core.database.DatabaseManager,
): org.meshtastic.core.common.database.DatabaseManager
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.util.DebugLogger
import coil3.util.Logger
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.meshtastic.core.common.BuildConfigProvider
import javax.inject.Singleton
private const val DISK_CACHE_PERCENT = 0.02
private const val MEMORY_CACHE_PERCENT = 0.25
@InstallIn(SingletonComponent::class)
@Module
interface NetworkModule {
@Binds
@Singleton
fun bindMqttRepository(
impl: org.meshtastic.core.network.repository.MQTTRepositoryImpl,
): org.meshtastic.core.network.repository.MQTTRepository
companion object {
@Provides
@Singleton
fun provideImageLoader(
okHttpClient: OkHttpClient,
@ApplicationContext application: Context,
buildConfigProvider: BuildConfigProvider,
): ImageLoader {
val sharedOkHttp = okHttpClient.newBuilder().build()
return ImageLoader.Builder(context = application)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { sharedOkHttp }))
add(SvgDecoder.Factory(scaleToDensity = true))
}
.memoryCache {
MemoryCache.Builder().maxSizePercent(context = application, percent = MEMORY_CACHE_PERCENT).build()
}
.diskCache { DiskCache.Builder().maxSizePercent(percent = DISK_CACHE_PERCENT).build() }
.logger(
logger = if (buildConfigProvider.isDebug) DebugLogger(minLevel = Logger.Level.Verbose) else null,
)
.crossfade(enable = true)
.build()
}
@Provides
@Singleton
fun provideJson(): Json = Json {
isLenient = true
ignoreUnknownKeys = true
}
@Provides
@Singleton
fun provideHttpClient(okHttpClient: OkHttpClient, json: Json): HttpClient = HttpClient(engineFactory = OkHttp) {
engine { preconfigured = okHttpClient }
install(plugin = ContentNegotiation) { json(json) }
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.data.datasource.SwitchingNodeInfoReadDataSource
import org.meshtastic.core.data.datasource.SwitchingNodeInfoWriteDataSource
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface NodeDataSourceModule {
@Binds @Singleton
fun bindNodeInfoReadDataSource(impl: SwitchingNodeInfoReadDataSource): NodeInfoReadDataSource
@Binds @Singleton
fun bindNodeInfoWriteDataSource(impl: SwitchingNodeInfoWriteDataSource): NodeInfoWriteDataSource
}

View file

@ -0,0 +1,269 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.prefs.analytics.AnalyticsPrefsImpl
import org.meshtastic.core.prefs.di.AnalyticsDataStore
import org.meshtastic.core.prefs.di.AppDataStore
import org.meshtastic.core.prefs.di.CustomEmojiDataStore
import org.meshtastic.core.prefs.di.FilterDataStore
import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore
import org.meshtastic.core.prefs.di.MapConsentDataStore
import org.meshtastic.core.prefs.di.MapDataStore
import org.meshtastic.core.prefs.di.MapTileProviderDataStore
import org.meshtastic.core.prefs.di.MeshDataStore
import org.meshtastic.core.prefs.di.MeshLogDataStore
import org.meshtastic.core.prefs.di.RadioDataStore
import org.meshtastic.core.prefs.di.UiDataStore
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefsImpl
import org.meshtastic.core.prefs.filter.FilterPrefsImpl
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl
import org.meshtastic.core.prefs.map.MapConsentPrefsImpl
import org.meshtastic.core.prefs.map.MapPrefsImpl
import org.meshtastic.core.prefs.map.MapTileProviderPrefsImpl
import org.meshtastic.core.prefs.mesh.MeshPrefsImpl
import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl
import org.meshtastic.core.prefs.radio.RadioPrefsImpl
import org.meshtastic.core.prefs.ui.UiPrefsImpl
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.CustomEmojiPrefs
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MapConsentPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.MapTileProviderPrefs
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshPrefs
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.UiPrefs
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class AnalyticsDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class HomoglyphEncodingDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class AppDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class CustomEmojiDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapConsentDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MapTileProviderDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MeshDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class RadioDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class UiDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class MeshLogDataStore
@Qualifier
@Retention(AnnotationRetention.BINARY)
internal annotation class FilterDataStore
@Suppress("TooManyFunctions")
@InstallIn(SingletonComponent::class)
@Module
interface PrefsModule {
@Binds fun bindAnalyticsPrefs(analyticsPrefsImpl: AnalyticsPrefsImpl): AnalyticsPrefs
@Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs
@Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs
@Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs
@Binds fun bindMapPrefs(mapPrefsImpl: MapPrefsImpl): MapPrefs
@Binds fun bindMapTileProviderPrefs(mapTileProviderPrefsImpl: MapTileProviderPrefsImpl): MapTileProviderPrefs
@Binds fun bindMeshPrefs(meshPrefsImpl: MeshPrefsImpl): MeshPrefs
@Binds fun bindMeshLogPrefs(meshLogPrefsImpl: MeshLogPrefsImpl): MeshLogPrefs
@Binds fun bindRadioPrefs(radioPrefsImpl: RadioPrefsImpl): RadioPrefs
@Binds fun bindUiPrefs(uiPrefsImpl: UiPrefsImpl): UiPrefs
@Binds fun bindFilterPrefs(filterPrefsImpl: FilterPrefsImpl): FilterPrefs
companion object {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Provides
@Singleton
@AnalyticsDataStore
fun provideAnalyticsDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("analytics_ds") },
)
@Provides
@Singleton
@HomoglyphEncodingDataStore
fun provideHomoglyphEncodingDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") },
)
@Provides
@Singleton
@AppDataStore
fun provideAppDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("app_ds") },
)
@Provides
@Singleton
@CustomEmojiDataStore
fun provideCustomEmojiDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") },
)
@Provides
@Singleton
@MapDataStore
fun provideMapDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_ds") },
)
@Provides
@Singleton
@MapConsentDataStore
fun provideMapConsentDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_consent_ds") },
)
@Provides
@Singleton
@MapTileProviderDataStore
fun provideMapTileProviderDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") },
)
@Provides
@Singleton
@MeshDataStore
fun provideMeshDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("mesh_ds") },
)
@Provides
@Singleton
@RadioDataStore
fun provideRadioDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("radio_ds") },
)
@Provides
@Singleton
@UiDataStore
fun provideUiDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("ui_ds") },
)
@Provides
@Singleton
@MeshLogDataStore
fun provideMeshLogDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("meshlog_ds") },
)
@Provides
@Singleton
@FilterDataStore
fun provideFilterDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")),
scope = scope,
produceFile = { context.preferencesDataStoreFile("filter_ds") },
)
}
}

View file

@ -0,0 +1,163 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.data.manager.CommandSenderImpl
import org.meshtastic.core.data.manager.FromRadioPacketHandlerImpl
import org.meshtastic.core.data.manager.HistoryManagerImpl
import org.meshtastic.core.data.manager.MeshActionHandlerImpl
import org.meshtastic.core.data.manager.MeshConfigFlowManagerImpl
import org.meshtastic.core.data.manager.MeshConfigHandlerImpl
import org.meshtastic.core.data.manager.MeshConnectionManagerImpl
import org.meshtastic.core.data.manager.MeshDataHandlerImpl
import org.meshtastic.core.data.manager.MeshMessageProcessorImpl
import org.meshtastic.core.data.manager.MeshRouterImpl
import org.meshtastic.core.data.manager.MessageFilterImpl
import org.meshtastic.core.data.manager.MqttManagerImpl
import org.meshtastic.core.data.manager.NeighborInfoHandlerImpl
import org.meshtastic.core.data.manager.NodeManagerImpl
import org.meshtastic.core.data.manager.PacketHandlerImpl
import org.meshtastic.core.data.manager.TracerouteHandlerImpl
import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl
import org.meshtastic.core.data.repository.LocationRepositoryImpl
import org.meshtastic.core.data.repository.MeshLogRepositoryImpl
import org.meshtastic.core.data.repository.NodeRepositoryImpl
import org.meshtastic.core.data.repository.PacketRepositoryImpl
import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl
import org.meshtastic.core.model.util.MeshDataMapper
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.FromRadioPacketHandler
import org.meshtastic.core.repository.HistoryManager
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshActionHandler
import org.meshtastic.core.repository.MeshConfigFlowManager
import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.MeshDataHandler
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.MeshMessageProcessor
import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MessageFilter
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.NeighborInfoHandler
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.TracerouteHandler
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds @Singleton
abstract fun bindNodeRepository(nodeRepositoryImpl: NodeRepositoryImpl): NodeRepository
@Binds
@Singleton
abstract fun bindRadioConfigRepository(radioConfigRepositoryImpl: RadioConfigRepositoryImpl): RadioConfigRepository
@Binds
@Singleton
abstract fun bindLocationRepository(locationRepositoryImpl: LocationRepositoryImpl): LocationRepository
@Binds
@Singleton
abstract fun bindDeviceHardwareRepository(
deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl,
): DeviceHardwareRepository
@Binds @Singleton
abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository
@Binds
@Singleton
abstract fun bindMeshLogRepository(meshLogRepositoryImpl: MeshLogRepositoryImpl): MeshLogRepository
@Binds @Singleton
abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager
@Binds @Singleton
abstract fun bindCommandSender(commandSenderImpl: CommandSenderImpl): CommandSender
@Binds @Singleton
abstract fun bindHistoryManager(historyManagerImpl: HistoryManagerImpl): HistoryManager
@Binds
@Singleton
abstract fun bindTracerouteHandler(tracerouteHandlerImpl: TracerouteHandlerImpl): TracerouteHandler
@Binds
@Singleton
abstract fun bindNeighborInfoHandler(neighborInfoHandlerImpl: NeighborInfoHandlerImpl): NeighborInfoHandler
@Binds @Singleton
abstract fun bindMqttManager(mqttManagerImpl: MqttManagerImpl): MqttManager
@Binds @Singleton
abstract fun bindPacketHandler(packetHandlerImpl: PacketHandlerImpl): PacketHandler
@Binds
@Singleton
abstract fun bindMeshConnectionManager(meshConnectionManagerImpl: MeshConnectionManagerImpl): MeshConnectionManager
@Binds @Singleton
abstract fun bindMeshDataHandler(meshDataHandlerImpl: MeshDataHandlerImpl): MeshDataHandler
@Binds
@Singleton
abstract fun bindMeshActionHandler(meshActionHandlerImpl: MeshActionHandlerImpl): MeshActionHandler
@Binds
@Singleton
abstract fun bindMeshMessageProcessor(meshMessageProcessorImpl: MeshMessageProcessorImpl): MeshMessageProcessor
@Binds @Singleton
abstract fun bindMeshRouter(meshRouterImpl: MeshRouterImpl): MeshRouter
@Binds
@Singleton
abstract fun bindFromRadioPacketHandler(
fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl,
): FromRadioPacketHandler
@Binds
@Singleton
abstract fun bindMeshConfigHandler(meshConfigHandlerImpl: MeshConfigHandlerImpl): MeshConfigHandler
@Binds
@Singleton
abstract fun bindMeshConfigFlowManager(meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl): MeshConfigFlowManager
@Binds @Singleton
abstract fun bindMessageFilter(messageFilterImpl: MessageFilterImpl): MessageFilter
companion object {
@Provides
@Singleton
fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager)
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MessageQueue
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {
@Provides
@Singleton
fun provideSendMessageUseCase(
nodeRepository: NodeRepository,
packetRepository: PacketRepository,
radioController: RadioController,
homoglyphEncodingPrefs: HomoglyphPrefs,
messageQueue: MessageQueue,
): SendMessageUseCase =
SendMessageUseCase(nodeRepository, packetRepository, radioController, homoglyphEncodingPrefs, messageQueue)
}

View file

@ -14,19 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.domain.usecase
package org.meshtastic.app.domain.usecase
import android.hardware.usb.UsbManager
import android.net.nsd.NsdServiceInfo
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.model.getMeshtasticShortName
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
import com.geeksville.mesh.repository.usb.UsbRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.jetbrains.compose.resources.getString
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.model.getMeshtasticShortName
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.app.repository.network.NetworkRepository.Companion.toAddressString
import org.meshtastic.app.repository.usb.UsbRepository
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.datastore.RecentAddressesDataSource

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.model
package org.meshtastic.app.model
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.UsbSerialDriver

View file

@ -14,21 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.model
package org.meshtastic.app.model
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
@ -37,10 +33,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.datastore.UiPreferencesDataSource
@ -83,7 +77,6 @@ constructor(
firmwareReleaseRepository: FirmwareReleaseRepository,
private val uiPreferencesDataSource: UiPreferencesDataSource,
private val meshServiceNotifications: MeshServiceNotifications,
private val analytics: PlatformAnalytics,
packetRepository: PacketRepository,
private val alertManager: AlertManager,
) : ViewModel() {
@ -99,12 +92,8 @@ constructor(
meshServiceNotifications.clearClientNotification(notification)
}
/**
* Emits events for mesh network send/receive activity. This is a SharedFlow to ensure all events are delivered,
* even if they are the same.
*/
val meshActivity: SharedFlow<MeshActivity> =
radioInterfaceService.meshActivity.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
/** Emits events for mesh network send/receive activity. */
val meshActivity: Flow<MeshActivity> = radioInterfaceService.meshActivity
private val _scrollToTopEventFlow =
MutableSharedFlow<ScrollToTopEvent>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@ -276,9 +265,4 @@ constructor(
fun onAppIntroCompleted() {
uiPreferencesDataSource.setAppIntroCompleted(true)
}
@Composable
fun AddNavigationTrackingEffect(navController: NavHostController) {
analytics.AddNavigationTrackingEffect(navController)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -24,7 +23,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import com.geeksville.mesh.ui.sharing.ChannelScreen
import org.meshtastic.app.ui.sharing.ChannelScreen
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.SettingsRoutes

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.compose.runtime.remember
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -24,7 +23,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import com.geeksville.mesh.ui.connections.ConnectionsScreen
import org.meshtastic.app.ui.connections.ConnectionsScreen
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.NodesRoutes

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.compose.runtime.getValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -25,8 +25,8 @@ import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import androidx.navigation.toRoute
import com.geeksville.mesh.model.UIViewModel
import kotlinx.coroutines.flow.Flow
import org.meshtastic.app.model.UIViewModel
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.ui.component.ScrollToTopEvent

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CellTower
@ -38,9 +38,9 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import com.geeksville.mesh.ui.node.AdaptiveNodeListScreen
import kotlinx.coroutines.flow.Flow
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.navigation.NodeDetailRoutes

View file

@ -16,7 +16,7 @@
*/
@file:Suppress("Wrapping", "SpacingAroundColon")
package com.geeksville.mesh.navigation
package org.meshtastic.app.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.network
package org.meshtastic.app.repository.network
import android.net.ConnectivityManager
import android.net.Network
@ -27,9 +26,8 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
internal fun ConnectivityManager.networkAvailable(): Flow<Boolean> = observeNetworks()
.map { activeNetworksList -> activeNetworksList.isNotEmpty() }
.distinctUntilChanged()
internal fun ConnectivityManager.networkAvailable(): Flow<Boolean> =
observeNetworks().map { activeNetworksList -> activeNetworksList.isNotEmpty() }.distinctUntilChanged()
internal fun ConnectivityManager.observeNetworks(
networkRequest: NetworkRequest = NetworkRequest.Builder().build(),
@ -37,30 +35,26 @@ internal fun ConnectivityManager.observeNetworks(
// Keep track of the current active networks
val activeNetworks = mutableSetOf<Network>()
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
activeNetworks.add(network)
trySend(activeNetworks.toList())
}
override fun onLost(network: Network) {
activeNetworks.remove(network)
trySend(activeNetworks.toList())
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
if (activeNetworks.contains(network)) {
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
activeNetworks.add(network)
trySend(activeNetworks.toList())
}
override fun onLost(network: Network) {
activeNetworks.remove(network)
trySend(activeNetworks.toList())
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
if (activeNetworks.contains(network)) {
trySend(activeNetworks.toList())
}
}
}
}
registerNetworkCallback(networkRequest, callback)
awaitClose {
unregisterNetworkCallback(callback)
}
awaitClose { unregisterNetworkCallback(callback) }
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.network
package org.meshtastic.app.repository.network
import android.net.ConnectivityManager
import android.net.nsd.NsdManager

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.network
package org.meshtastic.app.repository.network
import android.app.Application
import android.content.Context
@ -31,13 +30,11 @@ import dagger.hilt.components.SingletonComponent
class NetworkRepositoryModule {
companion object {
@Provides
fun provideConnectivityManager(application: Application): ConnectivityManager {
return application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
fun provideConnectivityManager(application: Application): ConnectivityManager =
application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@Provides
fun provideNsdManager(application: Application): NsdManager {
return application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
fun provideNsdManager(application: Application): NsdManager =
application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.network
package org.meshtastic.app.repository.network
import android.annotation.SuppressLint
import android.net.nsd.NsdManager

View file

@ -14,15 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import android.app.Application
import android.provider.Settings
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.repository.network.NetworkRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
@ -37,10 +35,10 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.common.util.BinaryLogFile
import org.meshtastic.core.common.util.BuildUtils
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.common.util.nowMillis
@ -51,6 +49,7 @@ import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.model.MeshActivity
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.proto.Heartbeat
@ -125,6 +124,15 @@ constructor(
if (listenersInitialized) return
listenersInitialized = true
radioPrefs.devAddr
.onEach { addr ->
if (_currentDeviceAddressFlow.value != addr) {
_currentDeviceAddressFlow.value = addr
startInterface()
}
}
.launchIn(processLifecycle.coroutineScope)
bluetoothRepository.state
.onEach { state ->
if (state.enabled) {
@ -176,31 +184,9 @@ constructor(
override fun isMockInterface(): Boolean =
BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
/**
* Determines whether to default to mock interface for device address. This keeps the decision logic separate and
* easy to extend.
*/
private fun shouldDefaultToMockInterface(): Boolean = BuildUtils.isEmulator
/**
* Return the device we are configured to use, or null for none device address strings are of the form:
*
* at
*
* where a is either x for bluetooth or s for serial and t is an interface specific address (macaddr or a device
* path)
*/
override fun getDeviceAddress(): String? {
// If the user has unpaired our device, treat things as if we don't have one
var address = radioPrefs.devAddr.value
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the
// user
if (address == null && shouldDefaultToMockInterface()) {
address = mockInterfaceAddress
}
return address
return _currentDeviceAddressFlow.value
}
/**
@ -380,21 +366,18 @@ constructor(
_serviceScope.handledLaunch { handleSendToRadio(bytes) }
}
private val _meshActivity = MutableSharedFlow<MeshActivity>(extraBufferCapacity = 64)
private val _meshActivity =
MutableSharedFlow<MeshActivity>(
extraBufferCapacity = 64,
onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST,
)
override val meshActivity: SharedFlow<MeshActivity> = _meshActivity.asSharedFlow()
private fun emitSendActivity() {
// Use tryEmit for SharedFlow as it's non-blocking
val emitted = _meshActivity.tryEmit(MeshActivity.Send)
if (!emitted) {
Logger.d { "MeshActivity.Send event was not emitted due to buffer overflow or no collectors" }
}
_meshActivity.tryEmit(MeshActivity.Send)
}
private fun emitReceiveActivity() {
val emitted = _meshActivity.tryEmit(MeshActivity.Receive)
if (!emitted) {
Logger.d { "MeshActivity.Receive event was not emitted due to buffer overflow or no collectors" }
}
_meshActivity.tryEmit(MeshActivity.Receive)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import java.io.Closeable

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import org.meshtastic.core.model.InterfaceId
import javax.inject.Inject

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,17 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
/**
* Radio interface factory service provider interface. Each radio backend implementation needs
* to have a factory to create new instances. These instances are specific to a particular
* address. This interface defines a common API across all radio interfaces for obtaining
* implementation instances.
* Radio interface factory service provider interface. Each radio backend implementation needs to have a factory to
* create new instances. These instances are specific to a particular address. This interface defines a common API
* across all radio interfaces for obtaining implementation instances.
*
* This is primarily used in conjunction with Dagger assisted injection for each backend
* interface type.
* This is primarily used in conjunction with Dagger assisted injection for each backend interface type.
*/
interface InterfaceFactorySpi<T : IRadioInterface> {
fun create(rest: String): T

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.MapKey
import org.meshtastic.core.model.InterfaceId

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.repository.radio
package com.geeksville.mesh.repository.radio
/**
* This interface defines the contract that all radio backend implementations must adhere to.
*/
/** This interface defines the contract that all radio backend implementations must adhere to. */
interface InterfaceSpec<T : IRadioInterface> {
fun createInterface(rest: String): T
/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid(rest: String): Boolean = true
}
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import kotlinx.coroutines.flow.Flow

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import dagger.assisted.Assisted

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `MockInterface` instances.
*/
/** Factory for creating `MockInterface` instances. */
@AssistedFactory
interface MockInterfaceFactory {
fun create(rest: String): MockInterface
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,20 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import javax.inject.Inject
/**
* Mock interface backend implementation.
*/
class MockInterfaceSpec @Inject constructor(
private val factory: MockInterfaceFactory
) : InterfaceSpec<MockInterface> {
override fun createInterface(rest: String): MockInterface {
return factory.create(rest)
}
/** Mock interface backend implementation. */
class MockInterfaceSpec @Inject constructor(private val factory: MockInterfaceFactory) : InterfaceSpec<MockInterface> {
override fun createInterface(rest: String): MockInterface = factory.create(rest)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean = true

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,17 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
class NopInterface @AssistedInject constructor(@Assisted val address: String) : IRadioInterface {
override fun handleSendToRadio(p: ByteArray) {
// No-op
}
override fun close() {
// No-op
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `NopInterface` instances.
*/
/** Factory for creating `NopInterface` instances. */
@AssistedFactory
interface NopInterfaceFactory {
fun create(rest: String): NopInterface
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,18 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import javax.inject.Inject
/**
* No-op interface backend implementation.
*/
class NopInterfaceSpec @Inject constructor(
private val factory: NopInterfaceFactory
) : InterfaceSpec<NopInterface> {
override fun createInterface(rest: String): NopInterface {
return factory.create(rest)
}
/** No-op interface backend implementation. */
class NopInterfaceSpec @Inject constructor(private val factory: NopInterfaceFactory) : InterfaceSpec<NopInterface> {
override fun createInterface(rest: String): NopInterface = factory.create(rest)
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import android.annotation.SuppressLint
import co.touchlab.kermit.Logger

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import org.meshtastic.core.ble.BluetoothRepository

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.Binds
import dagger.Module

View file

@ -14,14 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.usb.SerialConnection
import com.geeksville.mesh.repository.usb.SerialConnectionListener
import com.geeksville.mesh.repository.usb.UsbRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.meshtastic.app.repository.usb.SerialConnection
import org.meshtastic.app.repository.usb.SerialConnectionListener
import org.meshtastic.app.repository.usb.UsbRepository
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.RadioInterfaceService
import java.util.concurrent.atomic.AtomicReference

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `SerialInterface` instances.
*/
/** Factory for creating `SerialInterface` instances. */
@AssistedFactory
interface SerialInterfaceFactory {
fun create(rest: String): SerialInterface
}
}

View file

@ -14,11 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import android.hardware.usb.UsbManager
import com.geeksville.mesh.repository.usb.UsbRepository
import com.hoho.android.usbserial.driver.UsbSerialDriver
import org.meshtastic.app.repository.usb.UsbRepository
import javax.inject.Inject
/** Serial/USB interface backend implementation. */

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch

View file

@ -14,14 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import co.touchlab.kermit.Logger
import com.geeksville.mesh.repository.network.NetworkRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.core.common.util.Exceptions
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `TCPInterface` instances.
*/
/** Factory for creating `TCPInterface` instances. */
@AssistedFactory
interface TCPInterfaceFactory {
fun create(rest: String): TCPInterface
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,18 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.radio
package org.meshtastic.app.repository.radio
import javax.inject.Inject
/**
* TCP interface backend implementation.
*/
class TCPInterfaceSpec @Inject constructor(
private val factory: TCPInterfaceFactory
) : InterfaceSpec<TCPInterface> {
override fun createInterface(rest: String): TCPInterface {
return factory.create(rest)
}
/** TCP interface backend implementation. */
class TCPInterfaceSpec @Inject constructor(private val factory: TCPInterfaceFactory) : InterfaceSpec<TCPInterface> {
override fun createInterface(rest: String): TCPInterface = factory.create(rest)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
import com.hoho.android.usbserial.driver.ProbeTable
@ -25,18 +24,15 @@ import javax.inject.Inject
import javax.inject.Provider
/**
* Creates a probe table for the USB driver. This augments the default device-to-driver
* mappings with additional known working configurations. See this package's README for
* more info.
* Creates a probe table for the USB driver. This augments the default device-to-driver mappings with additional known
* working configurations. See this package's README for more info.
*/
@Reusable
class ProbeTableProvider @Inject constructor() : Provider<ProbeTable> {
override fun get(): ProbeTable {
return UsbSerialProber.getDefaultProbeTable().apply {
// RAK 4631:
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
// LilyGo TBeam v1.1:
addProduct(6790, 21972, CdcAcmSerialDriver::class.java)
}
override fun get(): ProbeTable = UsbSerialProber.getDefaultProbeTable().apply {
// RAK 4631:
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
// LilyGo TBeam v1.1:
addProduct(6790, 21972, CdcAcmSerialDriver::class.java)
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,21 +14,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.repository.usb
package com.geeksville.mesh.repository.usb
/**
* USB serial connection.
*/
/** USB serial connection. */
interface SerialConnection : AutoCloseable {
/**
* Called to initiate the serial connection.
*/
/** Called to initiate the serial connection. */
fun connect()
/**
* Send data (asynchronously) to the serial device. If the connection is not presently
* established then the data provided is ignored / dropped.
* Send data (asynchronously) to the serial device. If the connection is not presently established then the data
* provided is ignored / dropped.
*/
fun sendBytes(bytes: ByteArray)
@ -40,4 +35,4 @@ interface SerialConnection : AutoCloseable {
fun close(waitForStopped: Boolean)
override fun close()
}
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import android.hardware.usb.UsbManager
import co.touchlab.kermit.Logger

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,31 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.repository.usb
package com.geeksville.mesh.repository.usb
/**
* Callbacks indicating state changes in the USB serial connection.
*/
/** Callbacks indicating state changes in the USB serial connection. */
interface SerialConnectionListener {
/**
* Unable to initiate the connection due to missing permissions. This is a terminal
* state.
*/
/** Unable to initiate the connection due to missing permissions. This is a terminal state. */
fun onMissingPermission() {}
/**
* Called when a connection has been established.
*/
/** Called when a connection has been established. */
fun onConnected() {}
/**
* Called when serial data is received.
*/
/** Called when serial data is received. */
fun onDataReceived(bytes: ByteArray) {}
/**
* Called when the connection has been terminated.
*/
/** Called when the connection has been terminated. */
fun onDisconnected(thrown: Exception?) {}
}
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import android.content.BroadcastReceiver
import android.content.Context

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import android.content.BroadcastReceiver
import android.content.Context

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import android.app.Application
import android.hardware.usb.UsbDevice

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.usb
package org.meshtastic.app.repository.usb
import android.app.Application
import android.content.Context
@ -35,10 +34,8 @@ interface UsbRepositoryModule {
fun provideUsbManager(application: Application): UsbManager? =
application.getSystemService(Context.USB_SERVICE) as UsbManager?
@Provides
fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
@Provides fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
@Provides
fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
@Provides fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
}
}
}

View file

@ -14,12 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.Context
import androidx.glance.appwidget.updateAll
import com.geeksville.mesh.widget.LocalStatsWidget
import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.app.widget.LocalStatsWidget
import org.meshtastic.core.repository.AppWidgetUpdater
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.annotation.SuppressLint
import android.app.Application
@ -27,8 +27,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.data.repository.LocationRepository
import org.meshtastic.core.model.Position
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,8 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import org.meshtastic.core.api.MeshtasticIntent

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.app.Service
import android.content.Context
@ -24,14 +24,14 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.ServiceCompat
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.ui.connections.NO_DEVICE_SELECTED
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.toRemoteExceptions

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.app.Notification
import android.app.NotificationChannel
@ -36,16 +36,16 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R.raw
import com.geeksville.mesh.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION
import com.geeksville.mesh.service.ReactionReceiver.Companion.REACT_ACTION
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.app.MainActivity
import org.meshtastic.app.R.raw
import org.meshtastic.app.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION
import org.meshtastic.app.service.ReactionReceiver.Companion.REACT_ACTION
import org.meshtastic.app.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Message
@ -459,7 +459,7 @@ constructor(
val summaryNotification =
commonBuilder(NotificationType.DirectMessage)
.setSmallIcon(com.geeksville.mesh.R.drawable.app_icon)
.setSmallIcon(org.meshtastic.app.R.drawable.app_icon)
.setStyle(messagingStyle)
.setGroup(GROUP_KEY_MESSAGES)
.setGroupSummary(true)
@ -817,7 +817,7 @@ constructor(
type: NotificationType,
contentIntent: PendingIntent? = null,
): NotificationCompat.Builder {
val smallIcon = com.geeksville.mesh.R.drawable.app_icon
val smallIcon = org.meshtastic.app.R.drawable.app_icon
return NotificationCompat.Builder(context, type.channelId)
.setSmallIcon(smallIcon)

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.app.ForegroundServiceStartNotAllowedException
import android.content.Context
@ -23,8 +23,8 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.worker.ServiceKeepAliveWorker
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.worker.ServiceKeepAliveWorker
// / Helper function to start running our service
fun MeshService.Companion.startService(context: Context) {

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context
@ -54,7 +54,7 @@ class ReactionReceiver : BroadcastReceiver() {
}
companion object {
const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
const val REACT_ACTION = "org.meshtastic.app.REACT_ACTION"
const val EXTRA_CONTACT_KEY = "extra_contact_key"
const val EXTRA_REACTION = "extra_reaction"
const val EXTRA_REPLY_ID = "extra_reply_id"

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.BroadcastReceiver
import android.content.Context
@ -47,7 +47,7 @@ class ReplyReceiver : BroadcastReceiver() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
companion object {
const val REPLY_ACTION = "com.geeksville.mesh.REPLY_ACTION"
const val REPLY_ACTION = "org.meshtastic.app.REPLY_ACTION"
const val CONTACT_KEY = "contactKey"
const val KEY_TEXT_REPLY = "key_text_reply"
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
package org.meshtastic.app.service
import android.content.Context
import android.content.Intent

View file

@ -16,7 +16,7 @@
*/
@file:Suppress("MatchingDeclarationName")
package com.geeksville.mesh.ui
package org.meshtastic.app.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
@ -77,25 +77,25 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.channelsGraph
import com.geeksville.mesh.navigation.connectionsGraph
import com.geeksville.mesh.navigation.contactsGraph
import com.geeksville.mesh.navigation.firmwareGraph
import com.geeksville.mesh.navigation.mapGraph
import com.geeksville.mesh.navigation.nodesGraph
import com.geeksville.mesh.navigation.settingsGraph
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.ScannerViewModel
import com.geeksville.mesh.ui.connections.components.ConnectionsNavIcon
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.model.UIViewModel
import org.meshtastic.app.navigation.channelsGraph
import org.meshtastic.app.navigation.connectionsGraph
import org.meshtastic.app.navigation.contactsGraph
import org.meshtastic.app.navigation.firmwareGraph
import org.meshtastic.app.navigation.mapGraph
import org.meshtastic.app.navigation.nodesGraph
import org.meshtastic.app.navigation.settingsGraph
import org.meshtastic.app.service.MeshService
import org.meshtastic.app.ui.connections.DeviceType
import org.meshtastic.app.ui.connections.ScannerViewModel
import org.meshtastic.app.ui.connections.components.ConnectionsNavIcon
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.model.MeshActivity
@ -181,8 +181,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie
}
}
uIViewModel.AddNavigationTrackingEffect(navController)
VersionChecks(uIViewModel)
val alertDialogState by uIViewModel.currentAlert.collectAsStateWithLifecycle()
@ -273,6 +271,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie
val receiveColor = capturedColorScheme.StatusBlue
LaunchedEffect(uIViewModel.meshActivity, capturedColorScheme) {
uIViewModel.meshActivity.collectLatest { activity ->
Logger.d { "MeshActivity received in UI: $activity" }
val newTargetColor =
when (activity) {
is MeshActivity.Send -> sendColor

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections
package org.meshtastic.app.ui.connections
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
@ -48,17 +48,17 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.ui.connections.components.BLEDevices
import com.geeksville.mesh.ui.connections.components.ConnectingDeviceInfo
import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
import com.geeksville.mesh.ui.connections.components.EmptyStateContent
import com.geeksville.mesh.ui.connections.components.NetworkDevices
import com.geeksville.mesh.ui.connections.components.UsbDevices
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.ui.connections.components.BLEDevices
import org.meshtastic.app.ui.connections.components.ConnectingDeviceInfo
import org.meshtastic.app.ui.connections.components.ConnectionsSegmentedBar
import org.meshtastic.app.ui.connections.components.CurrentlyConnectedInfo
import org.meshtastic.app.ui.connections.components.EmptyStateContent
import org.meshtastic.app.ui.connections.components.NetworkDevices
import org.meshtastic.app.ui.connections.components.UsbDevices
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections
package org.meshtastic.app.ui.connections
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections
package org.meshtastic.app.ui.connections
/** Represent the different ways a device can connect to the phone. */
enum class DeviceType {

View file

@ -14,15 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections
package org.meshtastic.app.ui.connections
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.repository.usb.UsbRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@ -34,6 +31,9 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.app.domain.usecase.GetDiscoveredDevicesUseCase
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.repository.usb.UsbRepository
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -30,11 +30,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.ui.connections.ScannerViewModel
import no.nordicsemi.android.common.scanner.rememberFilterState
import no.nordicsemi.android.common.scanner.view.ScannerView
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.ui.connections.ScannerViewModel
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.model.ConnectionState

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.animation.Crossfade
import androidx.compose.material.icons.Icons
@ -37,7 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.ui.connections.DeviceType
import org.meshtastic.app.ui.connections.DeviceType
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.ui.icon.Device
import org.meshtastic.core.ui.icon.MeshtasticIcons

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bluetooth
@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.ui.connections.DeviceType
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.ui.connections.DeviceType
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bluetooth
import org.meshtastic.core.resources.network

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -40,13 +40,13 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import com.geeksville.mesh.model.DeviceListEntry
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import no.nordicsemi.android.common.ui.view.RssiIcon
import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException
import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.disconnect

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.Indication
import androidx.compose.foundation.LocalIndication
@ -52,10 +52,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import kotlinx.coroutines.delay
import no.nordicsemi.android.common.ui.view.RssiIcon
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -27,7 +27,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.core.model.ConnectionState
@Composable

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -47,11 +47,11 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.ui.connections.ScannerViewModel
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.repository.network.NetworkRepository
import org.meshtastic.app.ui.connections.ScannerViewModel
import org.meshtastic.core.common.util.isValidAddress
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package org.meshtastic.app.ui.connections.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -24,9 +24,9 @@ import androidx.compose.material.icons.rounded.UsbOff
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.ui.connections.ScannerViewModel
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.app.model.DeviceListEntry
import org.meshtastic.app.ui.connections.ScannerViewModel
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.no_usb_devices

Some files were not shown because too many files have changed in this diff Show more