feat(logging): Replace Timber with Kermit for multiplatform logging (#4083)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-12-28 08:30:15 -06:00 committed by GitHub
parent a927481e4d
commit 0776e029f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 727 additions and 957 deletions

View file

@ -46,7 +46,7 @@ dependencies {
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.navigation.runtime)
implementation(libs.timber)
implementation(libs.kermit)
googleApi(libs.dd.sdk.android.compose)
googleApi(libs.dd.sdk.android.logs)

View file

@ -19,9 +19,10 @@ package org.meshtastic.core.analytics.platform
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import org.meshtastic.core.analytics.BuildConfig
import org.meshtastic.core.analytics.DataPair
import timber.log.Timber
import javax.inject.Inject
/**
@ -34,16 +35,17 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
// In debug builds we attach a DebugTree for convenient local logging, but
// release builds rely on system logging only.
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Timber.i("F-Droid platform no-op analytics initialized (DebugTree planted).")
Logger.setMinSeverity(Severity.Debug)
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode }." }
} else {
Timber.i("F-Droid platform no-op analytics initialized.")
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
Timber.d("Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model")
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
}
@Composable
@ -51,7 +53,7 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
// No-op for F-Droid, but we can log navigation if needed for debugging
if (BuildConfig.DEBUG) {
navController.addOnDestinationChangedListener { _, destination, _ ->
Timber.d("Navigation changed to: ${destination.route}")
Logger.d { "Navigation changed to: ${destination.route}" }
}
}
}
@ -60,6 +62,6 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
get() = false
override fun track(event: String, vararg properties: DataPair) {
Timber.d("Track called: event=$event, properties=${properties.toList()}")
Logger.d { "Track called: event=$event, properties=${properties.toList()}" }
}
}

View file

@ -26,6 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
import com.datadog.android.compose.ExperimentalTrackingApi
@ -43,7 +45,6 @@ import com.datadog.android.rum.tracking.AcceptAllNavDestinations
import com.datadog.android.sessionreplay.SessionReplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.compose.ComposeExtensionSupport
import com.datadog.android.timber.DatadogTree
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
@ -61,10 +62,8 @@ import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.analytics.BuildConfig
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import timber.log.Timber
import timber.log.Timber.DebugTree
import timber.log.Timber.Tree
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
@ -102,14 +101,15 @@ constructor(
.setBundleWithTraceEnabled(true)
.setBundleWithRumEnabled(true)
.build()
buildList {
add(DatadogTree(datadogLogger))
add(CrashlyticsTree())
val writers = buildList {
add(DatadogLogWriter(datadogLogger))
add(CrashlyticsLogWriter())
if (BuildConfig.DEBUG) {
add(DebugTree())
add(co.touchlab.kermit.LogcatWriter())
}
}
.forEach(Timber::plant)
KermitLogger.setLogWriters(writers)
KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
// Initial consent state
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed)
@ -177,10 +177,10 @@ constructor(
*/
fun updateAnalyticsConsent(allowed: Boolean) {
if (!isPlatformServicesAvailable || isInTestLab) {
Timber.i("Analytics not available or in test lab, consent update skipped.")
KermitLogger.i { "Analytics not available or in test lab, consent update skipped." }
return
}
Timber.i(if (allowed) "Analytics enabled" else "Analytics disabled")
KermitLogger.i { if (allowed) "Analytics enabled" else "Analytics disabled" }
Datadog.setTrackingConsent(if (allowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
Firebase.crashlytics.isCrashlyticsCollectionEnabled = allowed
@ -221,30 +221,45 @@ constructor(
override val isPlatformServicesAvailable: Boolean
get() = isGooglePlayAvailable && isDatadogAvailable
private class CrashlyticsTree : Tree() {
private class CrashlyticsLogWriter : LogWriter() {
companion object {
private const val KEY_PRIORITY = "priority"
private const val KEY_TAG = "tag"
private const val KEY_MESSAGE = "message"
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
if (!Firebase.crashlytics.isCrashlyticsCollectionEnabled) return
Firebase.crashlytics.setCustomKeys {
key(KEY_PRIORITY, priority)
key(KEY_TAG, tag ?: "No Tag")
key(KEY_PRIORITY, severity.ordinal)
key(KEY_TAG, tag)
key(KEY_MESSAGE, message)
}
if (t == null) {
if (throwable == null) {
Firebase.crashlytics.recordException(Exception(message))
} else {
Firebase.crashlytics.recordException(t)
Firebase.crashlytics.recordException(throwable)
}
}
}
private class DatadogLogWriter(private val datadogLogger: Logger) : LogWriter() {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
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
}
datadogLogger.log(datadogPriority, message, throwable, mapOf("tag" to tag))
}
}
private fun String.extractSemanticVersion(): String {
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?".toRegex()
val matchResult = regex.find(this)
@ -263,7 +278,7 @@ constructor(
is String -> bundle.putString(it.name, it.value as String?) // Explicitly handle String
else -> bundle.putString(it.name, it.value.toString()) // Fallback for other types
}
Timber.tag(TAG).d("Analytics: track $event (${it.name} : ${it.value})")
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : ${it.value})" }
}
Firebase.analytics.logEvent(event, bundle)
}

View file

@ -61,5 +61,5 @@ dependencies {
implementation(libs.androidx.core.location.altitude)
implementation(libs.androidx.paging.common)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
}

View file

@ -6,6 +6,11 @@
<ID>MagicNumber:LocationRepository.kt$LocationRepository$30</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$31</ID>
<ID>MagicNumber:PacketRepository.kt$PacketRepository$500</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=${quirk.requiresBootloaderUpgradeForOta}, infoUrl=${quirk.infoUrl}"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: cache ${if (staleEntity == null) "empty" else "incomplete"} for hwModel=$hwModel, falling back to bundled JSON asset"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=$hwModel"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: lookup after JSON load for hwModel=$hwModel ${if (base != null) "succeeded" else "returned null"}"</ID>
<ID>MaxLineLength:DeviceHardwareRepository.kt$DeviceHardwareRepository$"DeviceHardwareRepository: lookup after remote fetch for hwModel=$hwModel ${if (fromDb != null) "succeeded" else "returned null"}"</ID>
<ID>TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception</ID>
<ID>TooManyFunctions:PacketRepository.kt$PacketRepository</ID>
</CurrentIssues>

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -26,10 +27,8 @@ import kotlinx.serialization.json.Json
import org.meshtastic.core.data.model.CustomTileProviderConfig
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.prefs.map.MapTileProviderPrefs
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.plus
interface CustomTileProviderRepository {
fun getCustomTileProviders(): Flow<List<CustomTileProviderConfig>>
@ -88,7 +87,7 @@ constructor(
try {
customTileProvidersStateFlow.value = json.decodeFromString<List<CustomTileProviderConfig>>(jsonString)
} catch (e: SerializationException) {
Timber.e(e, "Error deserializing tile providers")
Logger.e(e) { "Error deserializing tile providers" }
customTileProvidersStateFlow.value = emptyList()
}
} else {
@ -102,7 +101,7 @@ constructor(
val jsonString = json.encodeToString(providers)
mapTileProviderPrefs.customTileProviders = jsonString
} catch (e: SerializationException) {
Timber.e(e, "Error serializing tile providers")
Logger.e(e) { "Error serializing tile providers" }
}
}
}

View file

@ -18,12 +18,12 @@
package org.meshtastic.core.data.datasource
import android.app.Application
import co.touchlab.kermit.Logger
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.meshtastic.core.model.BootloaderOtaQuirk
import timber.log.Timber
import javax.inject.Inject
class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val application: Application) {
@ -32,7 +32,7 @@ class BootloaderOtaQuirksJsonDataSource @Inject constructor(private val applicat
val inputStream = application.assets.open("device_bootloader_ota_quirks.json")
inputStream.use { Json.decodeFromStream<ListWrapper>(it).devices }
}
.onFailure { e -> Timber.w(e, "Failed to load device_bootloader_ota_quirks.json") }
.onFailure { e -> Logger.w(e) { "Failed to load device_bootloader_ota_quirks.json" } }
.getOrDefault(emptyList())
@Serializable private data class ListWrapper(val devices: List<BootloaderOtaQuirk> = emptyList())

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
@ -27,7 +28,6 @@ import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.model.BootloaderOtaQuirk
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -59,45 +59,40 @@ constructor(
@Suppress("LongMethod")
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
withContext(Dispatchers.IO) {
Timber.d(
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)",
hwModel,
forceRefresh,
)
Logger.d {
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel, forceRefresh=$forceRefresh)"
}
val quirks = loadQuirks()
if (forceRefresh) {
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
Logger.d { "DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache" }
localDataSource.deleteAllDeviceHardware()
} else {
// 1. Attempt to retrieve from cache first
val cachedEntity = localDataSource.getByHwModel(hwModel)
if (cachedEntity != null && !cachedEntity.isStale()) {
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using fresh cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, cachedEntity.asExternalModel(), quirks),
)
}
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
Logger.d { "DeviceHardwareRepository: no fresh cache for hwModel=$hwModel, attempting remote fetch" }
}
// 2. Fetch from remote API
runCatching {
Timber.d("DeviceHardwareRepository: fetching device hardware from remote API")
Logger.d { "DeviceHardwareRepository: fetching device hardware from remote API" }
val remoteHardware = remoteDataSource.getAllDeviceHardware()
Timber.d(
"DeviceHardwareRepository: remote API returned %d device hardware entries",
remoteHardware.size,
)
Logger.d {
"DeviceHardwareRepository: remote API returned ${remoteHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(remoteHardware)
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s",
hwModel,
if (fromDb != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after remote fetch for hwModel=$hwModel ${if (fromDb != null) "succeeded" else "returned null"}"
}
fromDb
}
.onSuccess {
@ -105,57 +100,48 @@ constructor(
return@withContext Result.success(applyBootloaderQuirk(hwModel, it, quirks))
}
.onFailure { e ->
Timber.w(
e,
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d",
hwModel,
)
Logger.w(e) {
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=$hwModel"
}
// 3. Attempt to use stale cache as a fallback, but only if it looks complete.
val staleEntity = localDataSource.getByHwModel(hwModel)
if (staleEntity != null && !staleEntity.isIncomplete()) {
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: using stale cached device hardware for hwModel=$hwModel" }
return@withContext Result.success(
applyBootloaderQuirk(hwModel, staleEntity.asExternalModel(), quirks),
)
}
// 4. Fallback to bundled JSON if cache is empty or incomplete
Timber.d(
"DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset",
if (staleEntity == null) "empty" else "incomplete",
hwModel,
)
Logger.d {
"DeviceHardwareRepository: cache ${if (staleEntity == null) "empty" else "incomplete"} for hwModel=$hwModel, falling back to bundled JSON asset"
}
return@withContext loadFromBundledJson(hwModel, quirks)
}
}
private suspend fun loadFromBundledJson(hwModel: Int, quirks: List<BootloaderOtaQuirk>): Result<DeviceHardware?> =
runCatching {
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
Logger.d { "DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=$hwModel" }
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
Timber.d(
"DeviceHardwareRepository: bundled JSON returned %d device hardware entries",
jsonHardware.size,
)
Logger.d {
"DeviceHardwareRepository: bundled JSON returned ${jsonHardware.size} device hardware entries"
}
localDataSource.insertAllDeviceHardware(jsonHardware)
val base = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
hwModel,
if (base != null) "succeeded" else "returned null",
)
Logger.d {
"DeviceHardwareRepository: lookup after JSON load for hwModel=$hwModel ${if (base != null) "succeeded" else "returned null"}"
}
applyBootloaderQuirk(hwModel, base, quirks)
}
.also { result ->
result.exceptionOrNull()?.let { e ->
Timber.e(
e,
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=%d",
hwModel,
)
Logger.e(e) {
"DeviceHardwareRepository: failed to load device hardware from bundled JSON for hwModel=$hwModel"
}
}
}
@ -174,7 +160,7 @@ constructor(
private fun loadQuirks(): List<BootloaderOtaQuirk> {
val quirks = bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset()
Timber.d("DeviceHardwareRepository: loaded %d bootloader quirks", quirks.size)
Logger.d { "DeviceHardwareRepository: loaded ${quirks.size} bootloader quirks" }
return quirks
}
@ -186,17 +172,11 @@ constructor(
if (base == null) return null
val quirk = quirks.firstOrNull { it.hwModel == hwModel }
Timber.d(
"DeviceHardwareRepository: applyBootloaderQuirk for hwModel=%d, quirk found=%b",
hwModel,
quirk != null,
)
Logger.d { "DeviceHardwareRepository: applyBootloaderQuirk for hwModel=$hwModel, quirk found=${quirk != null}" }
return if (quirk != null) {
Timber.d(
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=%b, infoUrl=%s",
quirk.requiresBootloaderUpgradeForOta,
quirk.infoUrl,
)
Logger.d {
"DeviceHardwareRepository: applying quirk: requiresBootloaderUpgradeForOta=${quirk.requiresBootloaderUpgradeForOta}, infoUrl=${quirk.infoUrl}"
}
base.copy(
requiresBootloaderUpgradeForOta = quirk.requiresBootloaderUpgradeForOta,
bootloaderInfoUrl = quirk.infoUrl,

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.data.repository
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
@ -26,7 +27,6 @@ import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -68,7 +68,7 @@ constructor(
// This gives the UI something to show immediately.
val cachedRelease = localDataSource.getLatestRelease(releaseType)
cachedRelease?.let {
Timber.d("Emitting cached firmware for $releaseType (isStale=${it.isStale()})")
Logger.d { "Emitting cached firmware for $releaseType (isStale=${it.isStale()})" }
emit(it.asExternalModel())
}
@ -84,7 +84,7 @@ constructor(
// The `distinctUntilChanged()` operator on the collector side will prevent
// re-emitting the same data if the cache wasn't actually updated.
val finalRelease = localDataSource.getLatestRelease(releaseType)
Timber.d("Emitting final firmware for $releaseType from cache.")
Logger.d { "Emitting final firmware for $releaseType from cache." }
emit(finalRelease?.asExternalModel())
}
@ -98,7 +98,7 @@ constructor(
private suspend fun updateCacheFromSources() {
val remoteFetchSuccess =
runCatching {
Timber.d("Fetching fresh firmware releases from remote API.")
Logger.d { "Fetching fresh firmware releases from remote API." }
val networkReleases = remoteDataSource.getFirmwareReleases()
// The API fetches all release types, so we cache them all at once.
@ -109,13 +109,13 @@ constructor(
// If remote fetch failed, try the JSON fallback as a last resort.
if (!remoteFetchSuccess) {
Timber.w("Remote fetch failed, attempting to cache from bundled JSON.")
Logger.w { "Remote fetch failed, attempting to cache from bundled JSON." }
runCatching {
val jsonReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
localDataSource.insertFirmwareReleases(jsonReleases.releases.stable, FirmwareReleaseType.STABLE)
localDataSource.insertFirmwareReleases(jsonReleases.releases.alpha, FirmwareReleaseType.ALPHA)
}
.onFailure { Timber.w("Failed to cache from JSON: ${it.message}") }
.onFailure { Logger.w { "Failed to cache from JSON: ${it.message}" } }
}
}

View file

@ -27,6 +27,7 @@ import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
@ -34,7 +35,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -68,7 +68,7 @@ constructor(
try {
AltitudeConverterCompat.addMslAltitudeToLocation(context, location)
} catch (e: Exception) {
Timber.e(e, "addMslAltitudeToLocation() failed")
Logger.e(e) { "addMslAltitudeToLocation() failed" }
}
}
// info("New location: $location")
@ -85,9 +85,9 @@ constructor(
}
}
Timber.i(
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m",
)
Logger.i {
"Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m"
}
_receivingLocationUpdates.value = true
analytics.track("location_start") // Figure out how many users needed to use the phone GPS
@ -106,7 +106,7 @@ constructor(
}
awaitClose {
Timber.i("Stopping location requests")
Logger.i { "Stopping location requests" }
_receivingLocationUpdates.value = false
analytics.track("location_stop")

View file

@ -55,7 +55,7 @@ dependencies {
implementation(libs.androidx.room.paging)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
ksp(libs.androidx.room.compiler)

View file

@ -18,6 +18,7 @@
package org.meshtastic.core.database
import androidx.room.TypeConverter
import co.touchlab.kermit.Logger
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import kotlinx.serialization.json.Json
@ -25,7 +26,6 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.PaxcountProtos
import org.meshtastic.proto.TelemetryProtos
import timber.log.Timber
@Suppress("TooManyFunctions")
class Converters {
@ -45,7 +45,7 @@ class Converters {
fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try {
MeshProtos.FromRadio.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToFromRadio TypeConverter error")
Logger.e(ex) { "bytesToFromRadio TypeConverter error" }
MeshProtos.FromRadio.getDefaultInstance()
}
@ -55,7 +55,7 @@ class Converters {
fun bytesToUser(bytes: ByteArray): MeshProtos.User = try {
MeshProtos.User.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToUser TypeConverter error")
Logger.e(ex) { "bytesToUser TypeConverter error" }
MeshProtos.User.getDefaultInstance()
}
@ -65,7 +65,7 @@ class Converters {
fun bytesToPosition(bytes: ByteArray): MeshProtos.Position = try {
MeshProtos.Position.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToPosition TypeConverter error")
Logger.e(ex) { "bytesToPosition TypeConverter error" }
MeshProtos.Position.getDefaultInstance()
}
@ -75,7 +75,7 @@ class Converters {
fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry = try {
TelemetryProtos.Telemetry.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToTelemetry TypeConverter error")
Logger.e(ex) { "bytesToTelemetry TypeConverter error" }
TelemetryProtos.Telemetry.newBuilder().build() // Return an empty Telemetry object
}
@ -85,7 +85,7 @@ class Converters {
fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount = try {
PaxcountProtos.Paxcount.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToPaxcounter TypeConverter error")
Logger.e(ex) { "bytesToPaxcounter TypeConverter error" }
PaxcountProtos.Paxcount.getDefaultInstance()
}
@ -95,7 +95,7 @@ class Converters {
fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata = try {
MeshProtos.DeviceMetadata.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
Timber.e(ex, "bytesToMetadata TypeConverter error")
Logger.e(ex) { "bytesToMetadata TypeConverter error" }
MeshProtos.DeviceMetadata.getDefaultInstance()
}

View file

@ -21,6 +21,7 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -33,7 +34,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.security.MessageDigest
import javax.inject.Inject
@ -106,7 +106,7 @@ class DatabaseManager @Inject constructor(private val app: Application) {
// One-time cleanup: remove legacy DB if present and not active
managerScope.launch(Dispatchers.IO) { cleanupLegacyDbIfNeeded(activeDbName = dbName) }
Timber.i("Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}")
Logger.i { "Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}" }
}
/** Execute [block] with the current DB instance. */
@ -138,26 +138,28 @@ class DatabaseManager @Inject constructor(private val app: Application) {
// Only enforce the limit over device-specific DBs; exclude legacy and default DBs
val deviceDbs =
all.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME }
Timber.d(
"LRU check: limit=%d, active=%s, deviceDbs=%s",
limit,
anonymizeDbName(activeDbName),
deviceDbs.joinToString(", ") { anonymizeDbName(it) },
)
Logger.d {
"LRU check: limit=$limit, active=${anonymizeDbName(
activeDbName,
)}, deviceDbs=${deviceDbs.joinToString(", ") {
anonymizeDbName(it)
}}"
}
if (deviceDbs.size <= limit) return
val usageSnapshot = deviceDbs.associateWith { lastUsed(it) }
Timber.d(
"LRU lastUsed(ms): %s",
usageSnapshot.entries.joinToString(", ") { (name, ts) -> "${anonymizeDbName(name)}=$ts" },
)
Logger.d {
"LRU lastUsed(ms): ${usageSnapshot.entries.joinToString(", ") { (name, ts) ->
"${anonymizeDbName(name)}=$ts"
}}"
}
val victims = selectEvictionVictims(deviceDbs, activeDbName, limit, usageSnapshot)
Timber.i("LRU victims: %s", victims.joinToString(", ") { anonymizeDbName(it) })
Logger.i { "LRU victims: ${victims.joinToString(", ") { anonymizeDbName(it) }}" }
victims.forEach { name ->
runCatching { dbCache.remove(name)?.close() }
.onFailure { Timber.w(it, "Failed to close database %s", name) }
.onFailure { Logger.w(it) { "Failed to close database $name" } }
app.deleteDatabase(name)
prefs.edit().remove(lastUsedKey(name)).apply()
Timber.i("Evicted cached DB ${anonymizeDbName(name)}")
Logger.i { "Evicted cached DB ${anonymizeDbName(name)}" }
}
}
@ -186,12 +188,12 @@ class DatabaseManager @Inject constructor(private val app: Application) {
val legacyFile = getDbFile(app, legacy)
if (legacyFile != null) {
runCatching { dbCache.remove(legacy)?.close() }
.onFailure { Timber.w(it, "Failed to close legacy database %s before deletion", legacy) }
.onFailure { Logger.w(it) { "Failed to close legacy database $legacy before deletion" } }
val deleted = app.deleteDatabase(legacy)
if (deleted) {
Timber.i("Deleted legacy DB ${anonymizeDbName(legacy)}")
Logger.i { "Deleted legacy DB ${anonymizeDbName(legacy)}" }
} else {
Timber.w("Attempted to delete legacy DB %s but deleteDatabase returned false", legacy)
Logger.w { "Attempted to delete legacy DB $legacy but deleteDatabase returned false" }
}
}
prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply()

View file

@ -48,5 +48,5 @@ dependencies {
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
}

View file

@ -21,11 +21,11 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -43,9 +43,9 @@ class BootloaderWarningDataSource @Inject constructor(private val dataStore: Dat
runCatching { Json.decodeFromString<List<String>>(jsonString).toSet() }
.onFailure { e ->
if (e is IllegalArgumentException || e is SerializationException) {
Timber.w(e, "Failed to parse dismissed bootloader warning addresses, resetting preference")
Logger.w(e) { "Failed to parse dismissed bootloader warning addresses, resetting preference" }
} else {
Timber.w(e, "Unexpected error while parsing dismissed bootloader warning addresses")
Logger.w(e) { "Unexpected error while parsing dismissed bootloader warning addresses" }
}
}
.getOrDefault(emptySet())

View file

@ -18,13 +18,13 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ChannelProtos.Channel
import org.meshtastic.proto.ChannelProtos.ChannelSettings
import org.meshtastic.proto.ConfigProtos
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@ -36,7 +36,7 @@ class ChannelSetDataSource @Inject constructor(private val channelSetStore: Data
channelSetStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading DeviceConfig settings: ${exception.message}")
Logger.e { "Error reading DeviceConfig settings: ${exception.message}" }
emit(ChannelSet.getDefaultInstance())
} else {
throw exception

View file

@ -18,11 +18,11 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@ -34,7 +34,7 @@ class LocalConfigDataSource @Inject constructor(private val localConfigStore: Da
localConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading LocalConfig settings: ${exception.message}")
Logger.e { "Error reading LocalConfig settings: ${exception.message}" }
emit(LocalConfig.getDefaultInstance())
} else {
throw exception
@ -53,7 +53,7 @@ class LocalConfigDataSource @Inject constructor(private val localConfigStore: Da
if (localField != null) {
builder.setField(localField, value)
} else {
Timber.e("Error writing LocalConfig settings: ${config.payloadVariantCase}")
Logger.e { "Error writing LocalConfig settings: ${config.payloadVariantCase}" }
}
}
builder.build()

View file

@ -18,11 +18,11 @@
package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@ -34,7 +34,7 @@ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore:
moduleConfigStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading LocalModuleConfig settings: ${exception.message}")
Logger.e { "Error reading LocalModuleConfig settings: ${exception.message}" }
emit(LocalModuleConfig.getDefaultInstance())
} else {
throw exception
@ -53,7 +53,7 @@ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore:
if (localField != null) {
builder.setField(localField, value)
} else {
Timber.e("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
Logger.e { "Error writing LocalModuleConfig settings: ${config.payloadVariantCase}" }
}
}
builder.build()

View file

@ -21,6 +21,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@ -29,7 +30,6 @@ import kotlinx.serialization.json.Json
import org.json.JSONArray
import org.json.JSONObject
import org.meshtastic.core.datastore.model.RecentAddress
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -46,11 +46,11 @@ class RecentAddressesDataSource @Inject constructor(private val dataStore: DataS
try {
Json.decodeFromString<List<RecentAddress>>(jsonString)
} catch (e: IllegalArgumentException) {
Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" }
// Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString)
} catch (e: SerializationException) {
Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
Logger.w { "Could not parse recent addresses, falling back to legacy parsing: ${e.message}" }
// Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString)
}
@ -73,7 +73,7 @@ class RecentAddressesDataSource @Inject constructor(private val dataStore: DataS
}
else -> {
// Unknown format, log or handle as an error if necessary
Timber.w("Unknown item type in recent IP addresses: $item")
Logger.w { "Unknown item type in recent IP addresses: $item" }
null
}
}

View file

@ -55,7 +55,7 @@ dependencies {
implementation(libs.androidx.annotation)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
implementation(libs.kermit)
implementation(libs.zxing.android.embedded) { isTransitive = false }
implementation(libs.zxing.core)

View file

@ -2,30 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Channel.kt$0xff</ID>
<ID>MagicNumber:ChannelOption.kt$.03125f</ID>
<ID>MagicNumber:ChannelOption.kt$.0625f</ID>
<ID>MagicNumber:ChannelOption.kt$.203125f</ID>
<ID>MagicNumber:ChannelOption.kt$.40625f</ID>
<ID>MagicNumber:ChannelOption.kt$.8125f</ID>
<ID>MagicNumber:ChannelOption.kt$1.6250f</ID>
<ID>MagicNumber:ChannelOption.kt$1000f</ID>
<ID>MagicNumber:ChannelOption.kt$1600</ID>
<ID>MagicNumber:ChannelOption.kt$200</ID>
<ID>MagicNumber:ChannelOption.kt$3.25f</ID>
<ID>MagicNumber:ChannelOption.kt$31</ID>
<ID>MagicNumber:ChannelOption.kt$400</ID>
<ID>MagicNumber:ChannelOption.kt$5</ID>
<ID>MagicNumber:ChannelOption.kt$62</ID>
<ID>MagicNumber:ChannelOption.kt$800</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f</ID>
<ID>MagicNumber:ChannelSet.kt$40</ID>
<ID>MagicNumber:ChannelSet.kt$960</ID>
<ID>SwallowedException:ChannelSet.kt$ex: Throwable</ID>

View file

@ -17,7 +17,7 @@
package org.meshtastic.core.model
import timber.log.Timber
import co.touchlab.kermit.Logger
/** Provide structured access to parse and compare device version strings */
data class DeviceVersion(val asString: String) : Comparable<DeviceVersion> {
@ -28,7 +28,7 @@ data class DeviceVersion(val asString: String) : Comparable<DeviceVersion> {
try {
verStringToInt(asString)
} catch (e: Exception) {
Timber.w("Exception while parsing version '$asString', assuming version 0")
Logger.w { "Exception while parsing version '$asString', assuming version 0" }
0
}

View file

@ -20,14 +20,13 @@ package org.meshtastic.core.model.util
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
import co.touchlab.kermit.Logger
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
import org.meshtastic.core.model.Channel
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import timber.log.Timber
import java.net.MalformedURLException
import kotlin.jvm.Throws
private const val MESHTASTIC_HOST = "meshtastic.org"
private const val CHANNEL_PATH = "/e/"
@ -86,6 +85,6 @@ fun ChannelSet.qrCode(shouldAdd: Boolean): Bitmap? = try {
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: Throwable) {
Timber.e("URL was too complex to render as barcode")
Logger.e { "URL was too complex to render as barcode" }
null
}

View file

@ -46,5 +46,5 @@ dependencies {
implementation(projects.core.proto)
implementation(libs.javax.inject)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.timber)
implementation(libs.kermit)
}

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.service
import co.touchlab.kermit.Logger
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -25,7 +26,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.MeshProtos.MeshPacket
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -66,7 +66,7 @@ class ServiceRepository @Inject constructor() {
get() = _clientNotification
fun setClientNotification(notification: MeshProtos.ClientNotification?) {
Timber.e(notification?.message.orEmpty())
Logger.e { notification?.message.orEmpty() }
_clientNotification.value = notification
}
@ -80,7 +80,7 @@ class ServiceRepository @Inject constructor() {
get() = _errorMessage
fun setErrorMessage(text: String) {
Timber.e(text)
Logger.e { text }
_errorMessage.value = text
}

View file

@ -62,7 +62,7 @@ dependencies {
implementation(libs.guava)
implementation(libs.zxing.core)
implementation(libs.zxing.android.embedded)
implementation(libs.timber)
implementation(libs.kermit)
debugImplementation(libs.androidx.compose.ui.test.manifest)

View file

@ -2,18 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComposableParamOrder:AlertDialogs.kt$SimpleAlertDialog</ID>
<ID>ComposableParamOrder:EditBase64Preference.kt$EditBase64Preference</ID>
<ID>ComposableParamOrder:EditTextPreference.kt$EditTextPreference</ID>
<ID>ComposableParamOrder:MainAppBar.kt$MainAppBar</ID>
<ID>ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo</ID>
<ID>ComposableParamOrder:NodeChip.kt$NodeChip</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
<ID>ComposableParamOrder:SignalInfo.kt$SignalInfo</ID>
<ID>ComposableParamOrder:SwitchPreference.kt$SwitchPreference</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt$second</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
<ID>MagicNumber:EditIPv4Preference.kt$0xff</ID>
<ID>MagicNumber:EditIPv4Preference.kt$16</ID>
<ID>MagicNumber:EditIPv4Preference.kt$24</ID>
@ -22,47 +10,5 @@
<ID>MagicNumber:EditListPreference.kt$12345</ID>
<ID>MagicNumber:EditListPreference.kt$67890</ID>
<ID>MagicNumber:LazyColumnDragAndDropDemo.kt$50</ID>
<ID>ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane</ID>
<ID>ModifierMissing:ChannelItem.kt$ChannelItem</ID>
<ID>ModifierMissing:ChannelSelection.kt$ChannelSelection</ID>
<ID>ModifierMissing:ContactSharing.kt$SharedContactDialog</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPicker</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPickerDialog</ID>
<ID>ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$LoraSignalIndicator</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Rssi</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Snr</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi</ID>
<ID>ModifierMissing:PreferenceDivider.kt$PreferenceDivider</ID>
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog</ID>
<ID>ModifierMissing:SlidingSelector.kt$OptionLabel</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().padding(all = 16.dp)</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)</ID>
<ID>ModifierReused:PreferenceCategory.kt$Card(modifier = modifier.padding(bottom = 8.dp)) { Column( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { ProvideTextStyle(MaterialTheme.typography.bodyLarge) { content() } } }</ID>
<ID>ModifierReused:PreferenceCategory.kt$Text( text, modifier = modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp, end = 16.dp), style = MaterialTheme.typography.titleLarge, )</ID>
<ID>ModifierReused:TextDividerPreference.kt$Card(modifier = modifier.fillMaxWidth()) { Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } } }</ID>
<ID>ModifierReused:TextDividerPreference.kt$Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End))</ID>
<ID>ModifierReused:TextDividerPreference.kt$Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } }</ID>
<ID>MultipleEmitters:PreferenceCategory.kt$PreferenceCategory</ID>
<ID>ParameterNaming:BitwisePreference.kt$onItemSelected</ID>
<ID>ParameterNaming:ChannelSelection.kt$onSelected</ID>
<ID>ParameterNaming:ContactSharing.kt$onSharedContactRequested</ID>
<ID>ParameterNaming:DropDownPreference.kt$onItemSelected</ID>
<ID>ParameterNaming:EditIPv4Preference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditListPreference.kt$onValuesChanged</ID>
<ID>ParameterNaming:EditPasswordPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditTextPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:PositionPrecisionPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onNegativeClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onPositiveClicked</ID>
<ID>ParameterNaming:SlidingSelector.kt$onOptionSelected</ID>
<ID>PreviewPublic:IndoorAirQuality.kt$IAQScalePreview</ID>
<ID>PreviewPublic:LazyColumnDragAndDropDemo.kt$LazyColumnDragAndDropDemo</ID>
<ID>PreviewPublic:MaterialBatteryInfo.kt$MaterialBatteryInfoPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSelfPreview</ID>
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -43,6 +43,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@ -65,7 +66,6 @@ import org.meshtastic.core.ui.R
import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import java.net.MalformedURLException
/**
@ -90,7 +90,7 @@ fun AddContactFAB(
try {
uri.toSharedContact()
} catch (ex: MalformedURLException) {
Timber.e("URL was malformed: ${ex.message}")
Logger.e { "URL was malformed: ${ex.message}" }
null
}
if (sharedContact != null) {
@ -102,7 +102,7 @@ fun AddContactFAB(
sharedContact?.let { SharedContactDialog(sharedContact = it, onDismiss = { onSharedContactRequested(null) }) }
fun zxingScan() {
Timber.d("Starting zxing QR code scanner")
Logger.d { "Starting zxing QR code scanner" }
val zxingScan = ScanOptions()
zxingScan.setCameraId(CAMERA_ID)
zxingScan.setPrompt("")
@ -115,9 +115,9 @@ fun AddContactFAB(
LaunchedEffect(cameraPermissionState.status) {
if (cameraPermissionState.status.isGranted) {
Timber.d("Camera permission granted")
Logger.d { "Camera permission granted" }
} else {
Timber.d("Camera permission denied")
Logger.d { "Camera permission denied" }
}
}
@ -193,7 +193,7 @@ val Uri.qrCode: Bitmap?
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: WriterException) {
Timber.e("URL was too complex to render as barcode: ${ex.message}")
Logger.e { "URL was too complex to render as barcode: ${ex.message}" }
null
}

View file

@ -20,6 +20,7 @@ package org.meshtastic.core.ui.qr
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.RadioConfigRepository
@ -32,7 +33,6 @@ import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.config
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@ -61,7 +61,7 @@ constructor(
try {
serviceRepository.meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set channel error")
Logger.e(ex) { "Set channel error" }
}
}
@ -70,7 +70,7 @@ constructor(
try {
serviceRepository.meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set config error")
Logger.e(ex) { "Set config error" }
}
}
}