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

@ -76,6 +76,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist
import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -156,7 +157,6 @@ import org.osmdroid.views.overlay.Polygon
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import timber.log.Timber
import java.io.File
import java.text.DateFormat
import kotlin.math.abs
@ -170,7 +170,7 @@ private fun MapView.updateMarkers(
waypointMarkers: List<MarkerWithLabel>,
nodeClusterer: RadiusMarkerClusterer,
) {
Timber.d("Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints")
Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
overlays.removeAll { it is MarkerWithLabel }
// overlays.addAll(nodeMarkers + waypointMarkers)
overlays.addAll(waypointMarkers)
@ -271,7 +271,7 @@ fun MapView(
fun loadOnlineTileSourceBase(): ITileSource {
val id = mapViewModel.mapStyleId
Timber.d("mapStyleId from prefs: $id")
Logger.d { "mapStyleId from prefs: $id" }
return CustomTileSource.getTileSource(id).also {
zoomLevelMax = it.maximumZoomLevel.toDouble()
showDownloadButton = if (it is OnlineTileSourceBase) it.tileSourcePolicy.acceptsBulkDownload() else false
@ -295,11 +295,11 @@ fun MapView(
fun MapView.toggleMyLocation() {
if (context.gpsDisabled()) {
Timber.d("Telling user we need location turned on for MyLocationNewOverlay")
Logger.d { "Telling user we need location turned on for MyLocationNewOverlay" }
scope.launch { context.showToast(Res.string.location_disabled) }
return
}
Timber.d("user clicked MyLocationNewOverlay ${myLocationOverlay == null}")
Logger.d { "user clicked MyLocationNewOverlay ${myLocationOverlay == null}" }
if (myLocationOverlay == null) {
myLocationOverlay =
MyLocationNewOverlay(this).apply {
@ -454,15 +454,15 @@ fun MapView(
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(com.meshtastic.core.strings.getString(Res.string.waypoint_delete))
builder.setNeutralButton(com.meshtastic.core.strings.getString(Res.string.cancel)) { _, _ ->
Timber.d("User canceled marker delete dialog")
Logger.d { "User canceled marker delete dialog" }
}
builder.setNegativeButton(com.meshtastic.core.strings.getString(Res.string.delete_for_me)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for me")
Logger.d { "User deleted waypoint ${waypoint.id} for me" }
mapViewModel.deleteWaypoint(waypoint.id)
}
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
builder.setPositiveButton(com.meshtastic.core.strings.getString(Res.string.delete_for_everyone)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for everyone")
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
mapViewModel.sendWaypoint(waypoint.copy { expire = 1 })
mapViewModel.deleteWaypoint(waypoint.id)
}
@ -485,7 +485,7 @@ fun MapView(
fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback()
Timber.d("marker long pressed id=$id")
Logger.d { "marker long pressed id=$id" }
val waypoint = waypoints[id]?.data?.waypoint ?: return
// edit only when unlocked or lockedTo myNodeNum
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
@ -691,9 +691,9 @@ fun MapView(
),
)
} catch (ex: TileSourcePolicyException) {
Timber.d("Tile source does not allow archiving: ${ex.message}")
Logger.d { "Tile source does not allow archiving: ${ex.message}" }
} catch (ex: Exception) {
Timber.d("Tile source exception: ${ex.message}")
Logger.d { "Tile source exception: ${ex.message}" }
}
}
@ -897,7 +897,7 @@ fun MapView(
EditWaypointDialog(
waypoint = showEditWaypointDialog ?: return, // Safe call
onSendClicked = { waypoint ->
Timber.d("User clicked send waypoint ${waypoint.id}")
Logger.d { "User clicked send waypoint ${waypoint.id}" }
showEditWaypointDialog = null
mapViewModel.sendWaypoint(
waypoint.copy {
@ -910,12 +910,12 @@ fun MapView(
)
},
onDeleteClicked = { waypoint ->
Timber.d("User clicked delete waypoint ${waypoint.id}")
Logger.d { "User clicked delete waypoint ${waypoint.id}" }
showEditWaypointDialog = null
showDeleteMarkerDialog(waypoint)
},
onDismissRequest = {
Timber.d("User clicked cancel marker edit dialog")
Logger.d { "User clicked cancel marker edit dialog" }
showEditWaypointDialog = null
},
)

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import co.touchlab.kermit.Logger
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@ -40,7 +41,6 @@ import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import timber.log.Timber
@SuppressLint("WakelockTimeout")
private fun PowerManager.WakeLock.safeAcquire() {
@ -48,9 +48,9 @@ private fun PowerManager.WakeLock.safeAcquire() {
try {
acquire()
} catch (e: SecurityException) {
Timber.e("WakeLock permission exception: ${e.message}")
Logger.e { "WakeLock permission exception: ${e.message}" }
} catch (e: IllegalStateException) {
Timber.e("WakeLock acquire() exception: ${e.message}")
Logger.e { "WakeLock acquire() exception: ${e.message}" }
}
}
}
@ -60,7 +60,7 @@ private fun PowerManager.WakeLock.safeRelease() {
try {
release()
} catch (e: IllegalStateException) {
Timber.e("WakeLock release() exception: ${e.message}")
Logger.e { "WakeLock release() exception: ${e.message}" }
}
}
}

View file

@ -32,12 +32,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import co.touchlab.kermit.Logger
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.gms.location.Priority
import timber.log.Timber
private const val INTERVAL_MILLIS = 10000L
@ -66,11 +66,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
val locationSettingsLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
Timber.d("Location settings changed by user.")
Logger.d { "Location settings changed by user." }
// User has enabled location services or improved accuracy.
onPermissionResult(true) // Settings are now adequate, and permission was already granted.
} else {
Timber.d("Location settings change cancelled by user.")
Logger.d { "Location settings change cancelled by user." }
// User chose not to change settings. The permission itself is still granted,
// but the experience might be degraded. For the purpose of enabling map features,
// we consider this as success if the core permission is there.
@ -111,7 +111,7 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
val task = client.checkLocationSettings(builder.build())
task.addOnSuccessListener {
Timber.d("Location settings are satisfied.")
Logger.d { "Location settings are satisfied." }
onPermissionResult(true) // Permission granted and settings are good
}
@ -122,11 +122,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
locationSettingsLauncher.launch(intentSenderRequest)
// Result of this launch will be handled by locationSettingsLauncher's callback
} catch (sendEx: ActivityNotFoundException) {
Timber.d("Error launching location settings resolution ${sendEx.message}.")
Logger.d { "Error launching location settings resolution ${sendEx.message}." }
onPermissionResult(true) // Permission is granted, but settings dialog failed. Proceed.
}
} else {
Timber.d("Location settings are not satisfiable.${exception.message}")
Logger.d { "Location settings are not satisfiable.${exception.message}" }
onPermissionResult(true) // Permission is granted, but settings not ideal. Proceed.
}
}

View file

@ -62,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
@ -124,7 +125,6 @@ import org.meshtastic.proto.MeshProtos.Position
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
import timber.log.Timber
import java.text.DateFormat
import kotlin.math.abs
import kotlin.math.max
@ -219,7 +219,7 @@ fun MapView(
try {
cameraPositionState.animate(cameraUpdate)
} catch (e: IllegalStateException) {
Timber.d("Error animating camera to location: ${e.message}")
Logger.d { "Error animating camera to location: ${e.message}" }
}
}
}
@ -239,14 +239,14 @@ fun MapView(
try {
@Suppress("MissingPermission")
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
Timber.d("Started location tracking")
Logger.d { "Started location tracking" }
} catch (e: SecurityException) {
Timber.d("Location permission not available: ${e.message}")
Logger.d { "Location permission not available: ${e.message}" }
isLocationTrackingEnabled = false
}
} else {
fusedLocationClient.removeLocationUpdates(locationCallback)
Timber.d("Stopped location tracking")
Logger.d { "Stopped location tracking" }
}
}
@ -412,7 +412,7 @@ fun MapView(
cameraPositionState.animate(cameraUpdate)
hasCenteredTraceroute = true
} catch (e: IllegalStateException) {
Timber.d("Error centering traceroute overlay: ${e.message}")
Logger.d { "Error centering traceroute overlay: ${e.message}" }
}
}
}
@ -548,7 +548,7 @@ fun MapView(
CameraUpdateFactory.newLatLngBounds(bounds.build(), 100),
)
}
Timber.d("Cluster clicked! $cluster")
Logger.d { "Cluster clicked! $cluster" }
}
true
},
@ -679,9 +679,9 @@ fun MapView(
val currentPosition = cameraPositionState.position
val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build()
cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
Timber.d("Oriented map to north")
Logger.d { "Oriented map to north" }
} catch (e: IllegalStateException) {
Timber.d("Error orienting map to north: ${e.message}")
Logger.d { "Error orienting map to north: ${e.message}" }
}
}
}
@ -715,7 +715,7 @@ fun MapView(
internal fun convertIntToEmoji(unicodeCodePoint: Int): String = try {
String(Character.toChars(unicodeCodePoint))
} catch (e: IllegalArgumentException) {
Timber.w(e, "Invalid unicode code point: $unicodeCodePoint")
Logger.w(e) { "Invalid unicode code point: $unicodeCodePoint" }
"\uD83D\uDCCD"
}

View file

@ -21,6 +21,7 @@ import android.app.Application
import android.net.Uri
import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
@ -56,7 +57,6 @@ import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ConfigProtos
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -200,7 +200,7 @@ constructor(
fun selectCustomTileProvider(config: CustomTileProviderConfig?) {
if (config != null) {
if (!isValidTileUrlTemplate(config.urlTemplate)) {
Timber.tag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
Logger.withTag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
_selectedCustomTileProviderUrl.value = null
googleMapsPrefs.selectedCustomTileUrl = null
return
@ -224,7 +224,8 @@ constructor(
fun createUrlTileProvider(urlString: String): TileProvider? {
if (!isValidTileUrlTemplate(urlString)) {
Timber.tag("MapViewModel").e("Tile URL does not contain valid {x}, {y}, and {z} placeholders: $urlString")
Logger.withTag("MapViewModel")
.e("Tile URL does not contain valid {x}, {y}, and {z} placeholders: $urlString")
return null
}
return object : UrlTileProvider(TILE_SIZE, TILE_SIZE) {
@ -237,7 +238,7 @@ constructor(
return try {
URL(formattedUrl)
} catch (e: MalformedURLException) {
Timber.tag("MapViewModel").e(e, "Malformed URL: $formattedUrl")
Logger.withTag("MapViewModel").e(e) { "Malformed URL: $formattedUrl" }
null
}
}
@ -290,7 +291,7 @@ constructor(
try {
_selectedGoogleMapType.value = MapType.valueOf(savedGoogleMapTypeName ?: MapType.NORMAL.name)
} catch (e: IllegalArgumentException) {
Timber.e(e, "Invalid saved Google Map type: $savedGoogleMapTypeName")
Logger.e(e) { "Invalid saved Google Map type: $savedGoogleMapTypeName" }
_selectedGoogleMapType.value = MapType.NORMAL // Fallback in case of invalid stored name
googleMapsPrefs.selectedGoogleMapType = null
}
@ -335,14 +336,14 @@ constructor(
}
_mapLayers.value = loadedItems
if (loadedItems.isNotEmpty()) {
Timber.tag("MapViewModel").i("Loaded ${loadedItems.size} persisted map layers.")
Logger.withTag("MapViewModel").i("Loaded ${loadedItems.size} persisted map layers.")
}
}
} else {
Timber.tag("MapViewModel").i("Map layers directory does not exist. No layers loaded.")
Logger.withTag("MapViewModel").i("Map layers directory does not exist. No layers loaded.")
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error loading persisted map layers")
Logger.withTag("MapViewModel").e(e) { "Error loading persisted map layers" }
_mapLayers.value = emptyList()
}
}
@ -367,7 +368,7 @@ constructor(
}
if (layerType == null) {
Timber.tag("MapViewModel").e("Unsupported map layer file type: $extension")
Logger.withTag("MapViewModel").e("Unsupported map layer file type: $extension")
return@launch
}
@ -384,7 +385,7 @@ constructor(
val newItem = MapLayerItem(name = layerName, uri = localFileUri, layerType = layerType)
_mapLayers.value = _mapLayers.value + newItem
} else {
Timber.tag("MapViewModel").e("Failed to copy file to internal storage.")
Logger.withTag("MapViewModel").e("Failed to copy file to internal storage.")
}
}
}
@ -402,7 +403,7 @@ constructor(
inputStream?.use { input -> outputStream.use { output -> input.copyTo(output) } }
Uri.fromFile(outputFile)
} catch (e: IOException) {
Timber.tag("MapViewModel").e(e, "Error copying file to internal storage")
Logger.withTag("MapViewModel").e(e) { "Error copying file to internal storage" }
null
}
}
@ -453,7 +454,7 @@ constructor(
file.delete()
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error deleting file from internal storage")
Logger.withTag("MapViewModel").e(e) { "Error deleting file from internal storage" }
}
}
}
@ -465,7 +466,7 @@ constructor(
try {
application.contentResolver.openInputStream(uriToLoad)
} catch (_: Exception) {
Timber.d("MapViewModel: Error opening InputStream from URI: $uriToLoad")
Logger.d { "MapViewModel: Error opening InputStream from URI: $uriToLoad" }
null
}
}
@ -480,7 +481,7 @@ constructor(
LayerType.GEOJSON -> loadGeoJsonLayerIfNeeded(layerItem, map)
}
} catch (e: Exception) {
Timber.tag("MapViewModel").e(e, "Error loading map layer for ${layerItem.uri}")
Logger.withTag("MapViewModel").e(e) { "Error loading map layer for ${layerItem.uri}" }
}
}

View file

@ -20,6 +20,7 @@ package org.meshtastic.feature.map
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -44,7 +45,6 @@ import org.meshtastic.core.strings.two_days
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.MeshProtos
import timber.log.Timber
import java.util.concurrent.TimeUnit
@Suppress("MagicNumber")
@ -150,7 +150,7 @@ abstract class BaseMapViewModel(
return try {
serviceRepository.meshService?.packetId
} catch (ex: RemoteException) {
Timber.e("RemoteException: ${ex.message}")
Logger.e { "RemoteException: ${ex.message}" }
return null
}
}
@ -170,7 +170,7 @@ abstract class BaseMapViewModel(
try {
serviceRepository.meshService?.send(p)
} catch (ex: RemoteException) {
Timber.e("Send DataPacket error: ${ex.message}")
Logger.e { "Send DataPacket error: ${ex.message}" }
}
}