diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 282939bd3..890ea104b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -179,6 +179,7 @@ androidComponents {
project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defaultConfig.versionCode}") }
dependencies {
+ implementation(projects.core.analytics)
implementation(projects.core.data)
implementation(projects.core.database)
implementation(projects.core.datastore)
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index e18674796..9ac0d78c3 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -5,11 +5,6 @@
CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down
CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back
CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib
- CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work
- CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work
- CommentSpacing:Exceptions.kt$/// Convert any exceptions in this service call into a RemoteException that the client can
- CommentSpacing:Exceptions.kt$/// then handle
- CommentSpacing:Exceptions.kt$Exceptions$/// Set in Application.onCreate
CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */
ComposableNaming:NodeDetail.kt$notesSection
ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList
@@ -67,12 +62,10 @@
FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt
FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
- FinalNewline:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt
FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt
FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
- FinalNewline:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt
FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt
FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt
FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt
@@ -82,10 +75,8 @@
FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt
FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt
FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt
- FinalNewline:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt
FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt
FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt
- FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt
FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE
LambdaParameterEventTrailing:Channel.kt$onConfirm
@@ -97,7 +88,7 @@
LambdaParameterEventTrailing:NodeDetail.kt$onSaveNotes
LambdaParameterInRestartableEffect:Channel.kt$onConfirm
LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged
- LargeClass:MeshService.kt$MeshService : ServiceLogging
+ LargeClass:MeshService.kt$MeshService : Service
LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
@@ -166,12 +157,8 @@
MagicNumber:TCPInterface.kt$TCPInterface$180
MagicNumber:TCPInterface.kt$TCPInterface$500
MagicNumber:UIState.kt$4
- MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider
MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker
MaxLineLength:BluetoothInterface.kt$/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface. MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there. FIXME - make sure this protocol is guaranteed robust and won't drop packets "According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)). In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes." MAXPACKET is 256? look into what the lora lib uses. FIXME Characteristics: UUID properties description 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 read fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet). After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this mailbox. f75c76d2-129e-4dad-a1dd-7866124401e7 write toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len) ed9da18c-a800-4f66-a670-aa7547e34453 read|notify|write fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages until it catches up with this number. The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32 callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio. When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio. Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted. Re: queue management Not all messages are kept in the fromradio queue (filtered based on SubPacket): * only the most recent Position and User messages for a particular node are kept * all Data SubPackets are kept * No WantNodeNum / DenyNodeNum messages are kept A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging) */
- MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
- MaxLineLength:ServiceClient.kt$ServiceClient$// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
- MaxLineLength:ServiceClient.kt$ServiceClient.<no name provided>$// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)
ModifierMissing:BLEDevices.kt$BLEDevices
ModifierMissing:Channel.kt$ChannelScreen
@@ -256,7 +243,6 @@
ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.SNR.color, minValue = Metric.SNR.min, maxValue = Metric.SNR.max, )
ModifierWithoutDefault:CommonCharts.kt$modifier
ModifierWithoutDefault:EnvironmentCharts.kt$modifier
- MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex)
MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview
MultipleEmitters:CommonCharts.kt$LegendLabel
MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart
@@ -275,12 +261,10 @@
NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt
NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt
NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt
- NewLineAtEndOfFile:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt
NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt
NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt
NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt
NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt
- NewLineAtEndOfFile:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt
NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt
NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt
NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt
@@ -290,22 +274,16 @@
NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt
NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt
NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt
- NewLineAtEndOfFile:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt
NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt
NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt
- NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt
NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt
NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$
NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$
- NoConsecutiveBlankLines:BootCompleteReceiver.kt$
NoConsecutiveBlankLines:Constants.kt$
NoConsecutiveBlankLines:DebugLogFile.kt$
- NoConsecutiveBlankLines:DeferredExecution.kt$
- NoConsecutiveBlankLines:Exceptions.kt$
NoConsecutiveBlankLines:IRadioInterface.kt$
NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }
NoSemicolons:DateUtils.kt$DateUtils$;
- NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.*
OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract
ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked
ParameterNaming:ChannelSettingsItemList.kt$onSelected
@@ -345,8 +323,6 @@
PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview
RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex
ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
- SpacingAroundKeyword:Exceptions.kt$if
- SpacingAroundKeyword:Exceptions.kt$when
SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException
SwallowedException:Exceptions.kt$ex: Throwable
SwallowedException:MeshService.kt$MeshService$ex: BLEException
@@ -376,16 +352,16 @@
TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")
TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")
TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")
- TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterfaceLogging
- TooManyFunctions:MeshService.kt$MeshService : ServiceLogging
+ TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterface
+ TooManyFunctions:MeshService.kt$MeshService : Service
TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub
TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel
TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt
TooManyFunctions:NodesViewModel.kt$NodesViewModel : ViewModel
- TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModelLogging
- TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService : Logging
- TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable
- TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging
+ TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel
+ TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService
+ TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : Closeable
+ TooManyFunctions:UIState.kt$UIViewModel : ViewModel
TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"
UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit
UnusedParameter:ChannelSettingsItemList.kt$title: String
@@ -394,12 +370,6 @@
ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)
ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)
ViewModelInjection:DebugSearch.kt$viewModel
- WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*
Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }
- Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(
- Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(port, object : SerialInputOutputManager.Listener { override fun onNewData(data: ByteArray) { listener.onDataReceived(data) } override fun onRunError(e: Exception?) { closed.set(true) ignoreException { port.dtr = false port.rts = false port.close() } closedLatch.countDown() listener.onDisconnected(e) } })
- Wrapping:SerialInterface.kt$SerialInterface$(
- Wrapping:SerialInterface.kt$SerialInterface$(device, object : SerialConnectionListener { override fun onMissingPermission() { errormsg("Need permissions for port") } override fun onConnected() { onConnect.invoke() } override fun onDataReceived(bytes: ByteArray) { debug("Received ${bytes.size} byte(s)") bytes.forEach(::readChar) } override fun onDisconnected(thrown: Exception?) { thrown?.let { e -> errormsg("Serial error: $e") } debug("$device disconnected") onDeviceDisconnect(false) } })
- Wrapping:ServiceClient.kt$ServiceClient$Closeable, Logging
diff --git a/app/src/fdroid/java/com/geeksville/mesh/analytics/NopAnalytics.kt b/app/src/fdroid/java/com/geeksville/mesh/analytics/NopAnalytics.kt
deleted file mode 100644
index 7171fe405..000000000
--- a/app/src/fdroid/java/com/geeksville/mesh/analytics/NopAnalytics.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.analytics
-
-import android.content.Context
-import com.geeksville.mesh.android.Logging
-
-class DataPair(val name: String, valueIn: Any?) {
- val value = valueIn ?: "null"
-
- // / An accumulating firebase event - only one allowed per event
- constructor(d: Double) : this("BOGUS", d)
-
- constructor(d: Int) : this("BOGUS", d)
-}
-
-/** Implement our analytics API using Firebase Analytics */
-@Suppress("UNUSED_PARAMETER", "EmptyFunctionBlock", "EmptyInitBlock")
-class NopAnalytics(context: Context) :
- AnalyticsProvider,
- Logging {
-
- init {}
-
- override fun setEnabled(on: Boolean) {}
-
- override fun endSession() {}
-
- override fun trackLowValue(event: String, vararg properties: DataPair) {}
-
- override fun track(event: String, vararg properties: DataPair) {}
-
- override fun startSession() {}
-
- override fun setUserInfo(vararg p: DataPair) {}
-
- override fun increment(name: String, amount: Double) {}
-
- /** Send a google analytics screen view event */
- override fun sendScreenView(name: String) {}
-
- override fun endScreenView() {}
-}
diff --git a/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
deleted file mode 100644
index 32ed72d5f..000000000
--- a/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.android
-
-import android.app.Application
-import android.content.Context
-import android.provider.Settings
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.runtime.Composable
-import androidx.navigation.NavHostController
-import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.analytics.AnalyticsProvider
-import com.geeksville.mesh.analytics.NopAnalytics
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.BuildUtils.info
-import org.meshtastic.core.model.DeviceHardware
-import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
-import timber.log.Timber
-
-abstract class GeeksvilleApplication :
- Application(),
- Logging {
-
- companion object {
- lateinit var analytics: AnalyticsProvider
- }
-
- // / Are we running inside the testlab?
- val isInTestLab: Boolean
- get() {
- val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
- if (testLabSetting != null) {
- info("Testlab is $testLabSetting")
- }
- return "true" == testLabSetting
- }
-
- abstract val analyticsPrefs: AnalyticsPrefs
-
- @Suppress("EmptyFunctionBlock", "UnusedParameter")
- fun askToRate(application: AppCompatActivity) {}
-
- override fun onCreate() {
- super.onCreate()
-
- if (BuildConfig.DEBUG) {
- Timber.plant(Timber.DebugTree())
- }
-
- val nopAnalytics = NopAnalytics(this)
- analytics = nopAnalytics
- }
-}
-
-@Suppress("UnusedParameter")
-fun setAttributes(deviceVersion: String, deviceHardware: DeviceHardware) {
- // No-op for F-Droid version
- info("Setting attributes: deviceVersion=$deviceVersion, deviceHardware=$deviceHardware")
-}
-
-@Composable
-fun AddNavigationTracking(navController: NavHostController) {
- // No-op for F-Droid version
- navController.addOnDestinationChangedListener { _, destination, _ ->
- debug("Navigation changed to: ${destination.route}")
- }
-}
-
-val Context.isAnalyticsAvailable: Boolean
- get() = false
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt
index 8b1de274f..499508626 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -63,7 +63,6 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos.Waypoint
-import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.android.hasGps
import com.geeksville.mesh.copy
@@ -107,6 +106,7 @@ import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon
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
@@ -116,7 +116,7 @@ private fun MapView.UpdateMarkers(
waypointMarkers: List,
nodeClusterer: RadiusMarkerClusterer,
) {
- debug("Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints")
+ Timber.d("Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints")
overlays.removeAll { it is MarkerWithLabel }
// overlays.addAll(nodeMarkers + waypointMarkers)
overlays.addAll(waypointMarkers)
@@ -242,7 +242,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
fun loadOnlineTileSourceBase(): ITileSource {
val id = mapViewModel.mapStyleId
- debug("mapStyleId from prefs: $id")
+ Timber.d("mapStyleId from prefs: $id")
return CustomTileSource.getTileSource(id).also {
zoomLevelMax = it.maximumZoomLevel.toDouble()
showDownloadButton = if (it is OnlineTileSourceBase) it.tileSourcePolicy.acceptsBulkDownload() else false
@@ -261,11 +261,11 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
fun MapView.toggleMyLocation() {
if (context.gpsDisabled()) {
- debug("Telling user we need location turned on for MyLocationNewOverlay")
+ Timber.d("Telling user we need location turned on for MyLocationNewOverlay")
Toast.makeText(context, R.string.location_disabled, Toast.LENGTH_SHORT).show()
return
}
- debug("user clicked MyLocationNewOverlay ${myLocationOverlay == null}")
+ Timber.d("user clicked MyLocationNewOverlay ${myLocationOverlay == null}")
if (myLocationOverlay == null) {
myLocationOverlay =
MyLocationNewOverlay(this).apply {
@@ -352,14 +352,14 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
fun showDeleteMarkerDialog(waypoint: Waypoint) {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(R.string.waypoint_delete)
- builder.setNeutralButton(R.string.cancel) { _, _ -> debug("User canceled marker delete dialog") }
+ builder.setNeutralButton(R.string.cancel) { _, _ -> Timber.d("User canceled marker delete dialog") }
builder.setNegativeButton(R.string.delete_for_me) { _, _ ->
- debug("User deleted waypoint ${waypoint.id} for me")
+ Timber.d("User deleted waypoint ${waypoint.id} for me")
mapViewModel.deleteWaypoint(waypoint.id)
}
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
builder.setPositiveButton(R.string.delete_for_everyone) { _, _ ->
- debug("User deleted waypoint ${waypoint.id} for everyone")
+ Timber.d("User deleted waypoint ${waypoint.id} for everyone")
mapViewModel.sendWaypoint(waypoint.copy { expire = 1 })
mapViewModel.deleteWaypoint(waypoint.id)
}
@@ -382,7 +382,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
fun showMarkerLongPressDialog(id: Int) {
performHapticFeedback()
- debug("marker long pressed id=$id")
+ Timber.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) {
@@ -570,9 +570,9 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
),
)
} catch (ex: TileSourcePolicyException) {
- debug("Tile source does not allow archiving: ${ex.message}")
+ Timber.d("Tile source does not allow archiving: ${ex.message}")
} catch (ex: Exception) {
- debug("Tile source exception: ${ex.message}")
+ Timber.d("Tile source exception: ${ex.message}")
}
}
@@ -582,7 +582,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
val mapStyleInt = mapViewModel.mapStyleId
builder.setSingleChoiceItems(mapStyles, mapStyleInt) { dialog, which ->
- debug("Set mapStyleId pref to $which")
+ Timber.d("Set mapStyleId pref to $which")
mapViewModel.mapStyleId = which
dialog.dismiss()
map.setTileSource(loadOnlineTileSourceBase())
@@ -768,7 +768,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
EditWaypointDialog(
waypoint = showEditWaypointDialog ?: return, // Safe call
onSendClicked = { waypoint ->
- debug("User clicked send waypoint ${waypoint.id}")
+ Timber.d("User clicked send waypoint ${waypoint.id}")
showEditWaypointDialog = null
mapViewModel.sendWaypoint(
waypoint.copy {
@@ -781,12 +781,12 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
)
},
onDeleteClicked = { waypoint ->
- debug("User clicked delete waypoint ${waypoint.id}")
+ Timber.d("User clicked delete waypoint ${waypoint.id}")
showEditWaypointDialog = null
showDeleteMarkerDialog(waypoint)
},
onDismissRequest = {
- debug("User clicked cancel marker edit dialog")
+ Timber.d("User clicked cancel marker edit dialog")
showEditWaypointDialog = null
},
)
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt
index 0e400aa2d..d7b27aa54 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewWithLifecycle.kt
@@ -34,7 +34,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.android.BuildUtils.errormsg
import org.meshtastic.feature.map.requiredZoomLevel
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource
@@ -43,6 +42,7 @@ 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() {
@@ -50,9 +50,9 @@ private fun PowerManager.WakeLock.safeAcquire() {
try {
acquire()
} catch (e: SecurityException) {
- errormsg("WakeLock permission exception: ${e.message}")
+ Timber.e("WakeLock permission exception: ${e.message}")
} catch (e: IllegalStateException) {
- errormsg("WakeLock acquire() exception: ${e.message}")
+ Timber.e("WakeLock acquire() exception: ${e.message}")
}
}
}
@@ -62,7 +62,7 @@ private fun PowerManager.WakeLock.safeRelease() {
try {
release()
} catch (e: IllegalStateException) {
- errormsg("WakeLock release() exception: ${e.message}")
+ Timber.e("WakeLock release() exception: ${e.message}")
}
}
}
diff --git a/app/src/google/java/com/geeksville/mesh/analytics/FirebaseAnalytics.kt b/app/src/google/java/com/geeksville/mesh/analytics/FirebaseAnalytics.kt
deleted file mode 100644
index f8976f556..000000000
--- a/app/src/google/java/com/geeksville/mesh/analytics/FirebaseAnalytics.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.analytics
-
-import android.os.Bundle
-import com.geeksville.mesh.android.Logging
-import com.google.firebase.Firebase
-import com.google.firebase.analytics.FirebaseAnalytics
-import com.google.firebase.analytics.analytics
-import com.google.firebase.analytics.logEvent
-
-class DataPair(val name: String, valueIn: Any?) {
- val value = valueIn ?: "null"
-
- // / An accumulating firebase event - only one allowed per event
- constructor(d: Double) : this(FirebaseAnalytics.Param.VALUE, d)
-
- constructor(d: Int) : this(FirebaseAnalytics.Param.VALUE, d)
-}
-
-/** Implement our analytics API using Firebase Analytics */
-class FirebaseAnalytics(installId: String) :
- AnalyticsProvider,
- Logging {
-
- val t = Firebase.analytics.apply { setUserId(installId) }
-
- override fun setEnabled(on: Boolean) {
- t.setAnalyticsCollectionEnabled(on)
- }
-
- override fun endSession() {
- track("End Session")
- // Mint.flush() // Send results now
- }
-
- override fun trackLowValue(event: String, vararg properties: DataPair) {
- track(event, *properties)
- }
-
- override fun track(event: String, vararg properties: DataPair) {
- debug("Analytics: track $event")
-
- val bundle = Bundle()
- properties.forEach {
- when (it.value) {
- is Double -> bundle.putDouble(it.name, it.value)
- is Int -> bundle.putLong(it.name, it.value.toLong())
- is Long -> bundle.putLong(it.name, it.value)
- is Float -> bundle.putDouble(it.name, it.value.toDouble())
- else -> bundle.putString(it.name, it.value.toString())
- }
- }
- t.logEvent(event, bundle)
- }
-
- override fun startSession() {
- debug("Analytics: start session")
- // automatic with firebase
- }
-
- override fun setUserInfo(vararg p: DataPair) {
- p.forEach { t.setUserProperty(it.name, it.value.toString()) }
- }
-
- override fun increment(name: String, amount: Double) {
- // Mint.logEvent("$name increment")
- }
-
- /** Send a google analytics screen view event */
- override fun sendScreenView(name: String) {
- debug("Analytics: start screen $name")
- t.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
- param(FirebaseAnalytics.Param.SCREEN_NAME, name)
- param(FirebaseAnalytics.Param.SCREEN_CLASS, "MainActivity")
- }
- }
-
- override fun endScreenView() {
- // debug("Analytics: end screen")
- }
-}
diff --git a/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
deleted file mode 100644
index d89f97be2..000000000
--- a/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.android
-
-import android.app.Application
-import android.content.Context
-import android.content.SharedPreferences
-import android.provider.Settings
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.runtime.Composable
-import androidx.navigation.NavHostController
-import com.datadog.android.Datadog
-import com.datadog.android.DatadogSite
-import com.datadog.android.compose.ExperimentalTrackingApi
-import com.datadog.android.compose.NavigationViewTrackingEffect
-import com.datadog.android.compose.enableComposeActionTracking
-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.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
-import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.analytics.AnalyticsProvider
-import com.geeksville.mesh.analytics.FirebaseAnalytics
-import com.geeksville.mesh.util.exceptionReporter
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.GoogleApiAvailabilityLight
-import com.google.firebase.Firebase
-import com.google.firebase.analytics.analytics
-import com.google.firebase.crashlytics.crashlytics
-import com.google.firebase.crashlytics.setCustomKeys
-import com.google.firebase.initialize
-import com.suddenh4x.ratingdialog.AppRating
-import io.opentelemetry.api.GlobalOpenTelemetry
-import org.meshtastic.core.model.DeviceHardware
-import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
-import timber.log.Timber
-
-abstract class GeeksvilleApplication :
- Application(),
- Logging {
-
- companion object {
- lateinit var analytics: AnalyticsProvider
- }
-
- // / Are we running inside the testlab?
- val isInTestLab: Boolean
- get() {
- val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab")
- if (testLabSetting != null) {
- info("Testlab is $testLabSetting")
- }
- return "true" == testLabSetting
- }
-
- abstract val analyticsPrefs: AnalyticsPrefs
-
- private val minimumLaunchTimes: Int = 10
- private val minimumDays: Int = 10
- private val minimumLaunchTimesToShowAgain: Int = 5
- private val minimumDaysToShowAgain: Int = 14
-
- /** Ask user to rate in play store */
- @Suppress("MagicNumber")
- fun askToRate(activity: AppCompatActivity) {
- if (!isGooglePlayAvailable) return
-
- @Suppress("MaxLineLength")
- exceptionReporter {
- // we don't want to crash our app because of bugs in this optional feature
- AppRating.Builder(activity)
- .setMinimumLaunchTimes(minimumLaunchTimes) // default is 5, 3 means app is launched 3 or more times
- .setMinimumDays(
- minimumDays,
- ) // default is 5, 0 means install day, 10 means app is launched 10 or more days
- // later than installation
- .setMinimumLaunchTimesToShowAgain(
- minimumLaunchTimesToShowAgain,
- ) // default is 5, 1 means app is launched 1 or more times after neutral button
- // clicked
- .setMinimumDaysToShowAgain(
- minimumDaysToShowAgain,
- ) // default is 14, 1 means app is launched 1 or more days after neutral button
- // clicked
- .showIfMeetsConditions()
- }
- }
-
- lateinit var analyticsPrefsChangedListener: SharedPreferences.OnSharedPreferenceChangeListener
-
- override fun onCreate() {
- super.onCreate()
- initDatadog()
- initCrashlytics()
- updateAnalyticsConsent()
- // listen for changes to analytics prefs
- analyticsPrefsChangedListener =
- SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
- if (key == "allowed") {
- updateAnalyticsConsent()
- }
- }
- getSharedPreferences("analytics-prefs", MODE_PRIVATE)
- .registerOnSharedPreferenceChangeListener(analyticsPrefsChangedListener)
- }
-
- private val sampleRate = 100f
-
- private fun initCrashlytics() {
- analytics = FirebaseAnalytics(analyticsPrefs.installId)
- Firebase.initialize(this)
- Firebase.crashlytics.setUserId(analyticsPrefs.installId)
- Timber.plant(CrashlyticsTree())
- }
-
- private fun updateAnalyticsConsent() {
- if (!isAnalyticsAvailable || isInTestLab) {
- info("Analytics not available")
- return
- }
- val isAnalyticsAllowed = analyticsPrefs.analyticsAllowed
- info(if (isAnalyticsAllowed) "Analytics enabled" else "Analytics disabled")
- Datadog.setTrackingConsent(if (isAnalyticsAllowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
-
- analytics.setEnabled(isAnalyticsAllowed)
- Firebase.crashlytics.isCrashlyticsCollectionEnabled = isAnalyticsAllowed
- Firebase.analytics.setAnalyticsCollectionEnabled(isAnalyticsAllowed)
- Firebase.crashlytics.sendUnsentReports()
- }
-
- private class CrashlyticsTree : Timber.Tree() {
-
- 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?) {
- Firebase.crashlytics.setCustomKeys {
- key(KEY_PRIORITY, priority)
- key(KEY_TAG, tag ?: "No Tag")
- key(KEY_MESSAGE, message)
- }
-
- if (t == null) {
- Firebase.crashlytics.recordException(Exception(message))
- } else {
- Firebase.crashlytics.recordException(t)
- }
- }
- }
-
- private fun initDatadog() {
- val logger =
- Logger.Builder()
- .setNetworkInfoEnabled(true)
- .setRemoteSampleRate(sampleRate)
- .setBundleWithTraceEnabled(true)
- .setBundleWithRumEnabled(true)
- .build()
- 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()
- val consent = TrackingConsent.PENDING
- Datadog.initialize(this, configuration, consent)
- Datadog.setUserInfo(analyticsPrefs.installId)
-
- val rumConfiguration =
- RumConfiguration.Builder(BuildConfig.datadogApplicationId)
- .trackAnonymousUser(true)
- .trackBackgroundEvents(true)
- .trackFrustrations(true)
- .trackLongTasks()
- .trackNonFatalAnrs(true)
- .trackUserInteractions()
- .enableComposeActionTracking()
- .build()
- Rum.enable(rumConfiguration)
-
- val logsConfig = LogsConfiguration.Builder().build()
- Logs.enable(logsConfig)
-
- val traceConfig = TraceConfiguration.Builder().build()
- Trace.enable(traceConfig)
-
- GlobalOpenTelemetry.set(DatadogOpenTelemetry(BuildConfig.APPLICATION_ID))
-
- val sessionReplayConfig =
- SessionReplayConfiguration.Builder(sampleRate = 20.0f)
- // in case you need Jetpack Compose support
- .addExtensionSupport(ComposeExtensionSupport())
- .build()
-
- SessionReplay.enable(sessionReplayConfig)
-
- Timber.plant(Timber.DebugTree(), DatadogTree(logger))
- }
-}
-
-fun setAttributes(firmwareVersion: String, deviceHardware: DeviceHardware) {
- GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion.extractSemanticVersion())
- GlobalRumMonitor.get().addAttribute("device_hardware", deviceHardware.hwModelSlug)
-}
-
-private val Context.isGooglePlayAvailable: Boolean
- get() =
- GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(this).let {
- it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
- }
-
-private val isDatadogAvailable: Boolean = Datadog.isInitialized()
-
-val Context.isAnalyticsAvailable: Boolean
- get() = isDatadogAvailable && isGooglePlayAvailable
-
-@OptIn(ExperimentalTrackingApi::class)
-@Composable
-fun AddNavigationTracking(navController: NavHostController) {
- NavigationViewTrackingEffect(
- navController = navController,
- trackArguments = true,
- destinationPredicate = AcceptAllNavDestinations(),
- )
-}
-
-fun String.extractSemanticVersion(): String {
- // Regex to capture up to three numeric parts separated by dots
- val regex = """^(\d+)(?:\.(\d+))?(?:\.(\d+))?""".toRegex()
- val matchResult = regex.find(this)
- return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".")
- ?: this // Fallback to original if no match
-}
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/LocationHandler.kt b/app/src/google/java/com/geeksville/mesh/ui/map/LocationHandler.kt
index 28accd205..b340105f3 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/LocationHandler.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/LocationHandler.kt
@@ -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 com.geeksville.mesh.android.BuildUtils.debug
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) {
- debug("Location settings changed by user.")
+ Timber.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 {
- debug("Location settings change cancelled by user.")
+ Timber.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 {
- debug("Location settings are satisfied.")
+ Timber.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) {
- debug("Error launching location settings resolution ${sendEx.message}.")
+ Timber.d("Error launching location settings resolution ${sendEx.message}.")
onPermissionResult(true) // Permission is granted, but settings dialog failed. Proceed.
}
} else {
- debug("Location settings are not satisfiable.${exception.message}")
+ Timber.d("Location settings are not satisfiable.${exception.message}")
onPermissionResult(true) // Permission is granted, but settings not ideal. Proceed.
}
}
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
index 9adebaa20..8ef529906 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt
@@ -66,8 +66,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.MeshProtos.Waypoint
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog
import com.geeksville.mesh.ui.map.components.CustomMapLayersSheet
@@ -205,7 +203,7 @@ fun MapView(
try {
cameraPositionState.animate(cameraUpdate)
} catch (e: IllegalStateException) {
- debug("Error animating camera to location: ${e.message}")
+ Timber.d("Error animating camera to location: ${e.message}")
}
}
}
@@ -224,14 +222,14 @@ fun MapView(
try {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
- debug("Started location tracking")
+ Timber.d("Started location tracking")
} catch (e: SecurityException) {
- debug("Location permission not available: ${e.message}")
+ Timber.d("Location permission not available: ${e.message}")
isLocationTrackingEnabled = false
}
} else {
fusedLocationClient.removeLocationUpdates(locationCallback)
- debug("Stopped location tracking")
+ Timber.d("Stopped location tracking")
}
}
@@ -374,7 +372,7 @@ fun MapView(
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
}
} catch (e: IllegalStateException) {
- warn("MapView Could not animate to bounds: ${e.message}")
+ Timber.w("MapView Could not animate to bounds: ${e.message}")
}
}
},
@@ -462,7 +460,7 @@ fun MapView(
CameraUpdateFactory.newLatLngBounds(bounds.build(), 100),
)
}
- debug("Cluster clicked! $cluster")
+ Timber.d("Cluster clicked! $cluster")
}
true
},
@@ -574,9 +572,9 @@ fun MapView(
val currentPosition = cameraPositionState.position
val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build()
cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
- debug("Oriented map to north")
+ Timber.d("Oriented map to north")
} catch (e: IllegalStateException) {
- debug("Error orienting map to north: ${e.message}")
+ Timber.d("Error orienting map to north: ${e.message}")
}
}
}
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
index d9be78a2b..e3efdd395 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
@@ -22,7 +22,6 @@ import android.net.Uri
import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.ConfigProtos
-import com.geeksville.mesh.android.BuildUtils.debug
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
@@ -442,7 +441,7 @@ constructor(
try {
application.contentResolver.openInputStream(uriToLoad)
} catch (_: Exception) {
- debug("MapViewModel: Error opening InputStream from URI: $uriToLoad")
+ Timber.d("MapViewModel: Error opening InputStream from URI: $uriToLoad")
null
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index a0e8cf0cf..c1ffaaed5 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -39,25 +39,20 @@ import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
+import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
-class MainActivity :
- AppCompatActivity(),
- Logging {
+class MainActivity : AppCompatActivity() {
private val model: UIViewModel by viewModels()
// This is aware of the Activity lifecycle and handles binding to the mesh service.
@@ -78,15 +73,6 @@ class MainActivity :
super.onCreate(savedInstanceState)
- if (savedInstanceState == null) {
- lifecycleScope.launch {
- val appIntroCompleted = uiPreferencesDataSource.appIntroCompleted.value
- if (appIntroCompleted) {
- (application as GeeksvilleApplication).askToRate(this@MainActivity)
- }
- }
- }
-
setContent {
val theme by model.theme.collectAsState()
val dynamic = theme == MODE_DYNAMIC
@@ -108,12 +94,7 @@ class MainActivity :
if (appIntroCompleted) {
MainScreen(uIViewModel = model)
} else {
- AppIntroductionScreen(
- onDone = {
- model.onAppIntroCompleted()
- (application as GeeksvilleApplication).askToRate(this@MainActivity)
- },
- )
+ AppIntroductionScreen(onDone = { model.onAppIntroCompleted() })
}
}
}
@@ -132,22 +113,22 @@ class MainActivity :
when (appLinkAction) {
Intent.ACTION_VIEW -> {
appLinkData?.let {
- debug("App link data: $it")
+ Timber.d("App link data: $it")
if (it.path?.startsWith("/e/") == true || it.path?.startsWith("/E/") == true) {
- debug("App link data is a channel set")
+ Timber.d("App link data is a channel set")
model.requestChannelUrl(it)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
val sharedContact = it.toSharedContact()
- debug("App link data is a shared contact: ${sharedContact.user.longName}")
+ Timber.d("App link data is a shared contact: ${sharedContact.user.longName}")
model.setSharedContactRequested(sharedContact)
} else {
- debug("App link data is not a channel set")
+ Timber.d("App link data is not a channel set")
}
}
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
- debug("USB device attached")
+ Timber.d("USB device attached")
showSettingsPage()
}
@@ -161,7 +142,7 @@ class MainActivity :
}
else -> {
- warn("Unexpected action $appLinkAction")
+ Timber.w("Unexpected action $appLinkAction")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
index e444b1c1a..f20aaf816 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
@@ -32,6 +32,7 @@ import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
+import timber.log.Timber
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@@ -56,7 +57,7 @@ constructor(
private var serviceSetupJob: Job? = null
init {
- debug("Adding self as LifecycleObserver for $lifecycleOwner")
+ Timber.d("Adding self as LifecycleObserver for $lifecycleOwner")
lifecycleOwner.lifecycle.addObserver(this)
}
@@ -67,7 +68,7 @@ constructor(
serviceSetupJob =
lifecycleOwner.lifecycleScope.handledLaunch {
serviceRepository.setMeshService(service)
- debug("connected to mesh service, connectionState=${serviceRepository.connectionState.value}")
+ Timber.d("connected to mesh service, connectionState=${serviceRepository.connectionState.value}")
}
}
@@ -82,32 +83,32 @@ constructor(
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
- debug("Lifecycle: ON_START")
+ Timber.d("Lifecycle: ON_START")
try {
bindMeshService()
} catch (ex: BindFailedException) {
- errormsg("Bind of MeshService failed: ${ex.message}")
+ Timber.e("Bind of MeshService failed: ${ex.message}")
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
- debug("Lifecycle: ON_DESTROY")
+ Timber.d("Lifecycle: ON_DESTROY")
owner.lifecycle.removeObserver(this)
- debug("Removed self as LifecycleObserver to $lifecycleOwner")
+ Timber.d("Removed self as LifecycleObserver to $lifecycleOwner")
}
// endregion
@Suppress("TooGenericExceptionCaught")
private fun bindMeshService() {
- debug("Binding to mesh service!")
+ Timber.d("Binding to mesh service!")
try {
MeshService.startService(activity)
} catch (ex: Exception) {
- errormsg("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
+ Timber.e("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
}
connect(activity, MeshService.createIntent(), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT)
diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
new file mode 100644
index 000000000..4aece39f0
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import org.meshtastic.core.analytics.platform.PlatformAnalytics
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * The main application class for Meshtastic.
+ *
+ * This class is annotated with [HiltAndroidApp] to enable Hilt for dependency injection. It initializes core
+ * application components, including analytics and platform-specific helpers, and manages analytics consent based on
+ * user preferences.
+ */
+@HiltAndroidApp
+class MeshUtilApplication : Application() {
+
+ @Inject lateinit var platformAnalytics: PlatformAnalytics
+
+ companion object {
+ /**
+ * Provides access to the platform-specific analytics provider. Initialized via the injected [PlatformAnalytics]
+ * during [onCreate].
+ */
+ lateinit var analytics: PlatformAnalytics
+ private set
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ // Initialize platform-specific features (analytics, crash reporting, etc.)
+ analytics = platformAnalytics
+ }
+}
+
+fun logAssert(executeReliableWrite: Boolean) {
+ if (!executeReliableWrite) {
+ val ex = AssertionError("Assertion failed")
+ Timber.e(ex)
+ throw ex
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/analytics/AnalyticsClient.kt b/app/src/main/java/com/geeksville/mesh/analytics/AnalyticsClient.kt
deleted file mode 100644
index e57054f5c..000000000
--- a/app/src/main/java/com/geeksville/mesh/analytics/AnalyticsClient.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.analytics
-
-/**
- * Created by kevinh on 12/24/14.
- */
-interface AnalyticsProvider {
-
- // Turn analytics logging on/off
- fun setEnabled(on: Boolean)
-
- /**
- * Store an event
- */
- fun track(event: String, vararg properties: DataPair)
-
- /**
- * Only track this event if using a cheap provider (like google)
- */
- fun trackLowValue(event: String, vararg properties: DataPair)
-
- fun endSession()
- fun startSession()
-
- /**
- * Set persistent ID info about this user, as a key value pair
- */
- fun setUserInfo(vararg p: DataPair)
-
- /**
- * Increment some sort of analytics counter
- */
- fun increment(name: String, amount: Double = 1.0)
-
- fun sendScreenView(name: String)
- fun endScreenView()
-}
diff --git a/app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt b/app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt
index dd43fb431..cc8c78b24 100644
--- a/app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt
+++ b/app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt
@@ -19,13 +19,12 @@ package com.geeksville.mesh.android
import android.os.Build
-/**
- * Created by kevinh on 1/14/16.
- */
-object BuildUtils : Logging {
+/** Created by kevinh on 1/14/16. */
+object BuildUtils {
// Are we running on the emulator?
val isEmulator
- get() = Build.FINGERPRINT.startsWith("generic") ||
+ get() =
+ Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.FINGERPRINT.contains("emulator") ||
setOf(Build.MODEL, Build.PRODUCT).contains("google_sdk") ||
diff --git a/app/src/main/java/com/geeksville/mesh/android/Logging.kt b/app/src/main/java/com/geeksville/mesh/android/Logging.kt
deleted file mode 100644
index 7e83a88ba..000000000
--- a/app/src/main/java/com/geeksville/mesh/android/Logging.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.android
-
-import timber.log.Timber
-
-interface Logging {
-
- private fun tag(): String = this.javaClass.name
-
- fun info(msg: String) = Timber.tag(tag()).i(msg)
-
- fun debug(msg: String) = Timber.tag(tag()).d(msg)
-
- fun warn(msg: String) = Timber.tag(tag()).w(msg)
-
- /**
- * Log an error message, note - we call this errormsg rather than error because error() is a stdlib function in
- * kotlin in the global namespace and we don't want users to accidentally call that.
- */
- fun errormsg(msg: String, ex: Throwable? = null) {
- if (ex?.message != null) {
- Timber.tag(tag()).e(ex, msg)
- } else {
- Timber.tag(tag()).e(msg)
- }
- }
-
- // / Kotlin assertions are disabled on android, so instead we use this assert helper
- fun logAssert(f: Boolean) {
- if (!f) {
- val ex = AssertionError("Assertion failed")
-
- // if(!Debug.isDebuggerConnected())
- throw ex
- }
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt b/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt
index 0c5e1f5d9..f10baf5bf 100644
--- a/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt
+++ b/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt
@@ -24,6 +24,7 @@ import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import com.geeksville.mesh.util.exceptionReporter
+import timber.log.Timber
import java.io.Closeable
import java.lang.IllegalArgumentException
import java.util.concurrent.locks.ReentrantLock
@@ -31,11 +32,8 @@ import kotlin.concurrent.withLock
class BindFailedException : Exception("bindService failed")
-/**
- * A wrapper that cleans up the service binding process
- */
-open class ServiceClient(private val stubFactory: (IBinder) -> T) : Closeable,
- Logging {
+/** A wrapper that cleans up the service binding process */
+open class ServiceClient(private val stubFactory: (IBinder) -> T) : Closeable {
var serviceP: T? = null
@@ -72,17 +70,17 @@ open class ServiceClient(private val stubFactory: (IBinder) -> T
if (isClosed) {
isClosed = false
if (!c.bindService(intent, connection, flags)) {
-
- // Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
+ // Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false.
+ // Try
// a short sleep to see if that helps
- errormsg("Needed to use the second bind attempt hack")
+ Timber.e("Needed to use the second bind attempt hack")
Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1
if (!c.bindService(intent, connection, flags)) {
throw BindFailedException()
}
}
} else {
- warn("Ignoring rebind attempt for service")
+ Timber.w("Ignoring rebind attempt for service")
}
}
@@ -92,41 +90,39 @@ open class ServiceClient(private val stubFactory: (IBinder) -> T
context?.unbindService(connection)
} catch (ex: IllegalArgumentException) {
// Autobugs show this can generate an illegal arg exception for "service not registered" during reinstall?
- warn("Ignoring error in ServiceClient.close, probably harmless")
+ Timber.w("Ignoring error in ServiceClient.close, probably harmless")
}
serviceP = null
context = null
}
// Called when we become connected
- open fun onConnected(service: T) {
- }
+ open fun onConnected(service: T) {}
// called on loss of connection
- open fun onDisconnected() {
- }
+ open fun onDisconnected() {}
- private val connection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
- if (!isClosed) {
- val s = stubFactory(binder)
- serviceP = s
- onConnected(s)
+ private val connection =
+ object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
+ if (!isClosed) {
+ val s = stubFactory(binder)
+ serviceP = s
+ onConnected(s)
- // after calling our handler, tell anyone who was waiting for this connection to complete
- lock.withLock {
- condition.signalAll()
+ // after calling our handler, tell anyone who was waiting for this connection to complete
+ lock.withLock { condition.signalAll() }
+ } else {
+ // If we start to close a service, it seems that there is a possibility a onServiceConnected event
+ // is the queue
+ // for us. Be careful not to process that stale event
+ Timber.w("A service connected while we were closing it, ignoring")
}
- } else {
- // If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
- // for us. Be careful not to process that stale event
- warn("A service connected while we were closing it, ignoring")
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
+ serviceP = null
+ onDisconnected()
}
}
-
- override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
- serviceP = null
- onDisconnected()
- }
- }
}
diff --git a/app/src/main/java/com/geeksville/mesh/concurrent/DeferredExecution.kt b/app/src/main/java/com/geeksville/mesh/concurrent/DeferredExecution.kt
index 59d893d80..934d8910b 100644
--- a/app/src/main/java/com/geeksville/mesh/concurrent/DeferredExecution.kt
+++ b/app/src/main/java/com/geeksville/mesh/concurrent/DeferredExecution.kt
@@ -17,31 +17,27 @@
package com.geeksville.mesh.concurrent
-import com.geeksville.mesh.android.Logging
-
+import timber.log.Timber
/**
- * Sometimes when starting services we face situations where messages come in that require computation
- * but we can't do that computation yet because we are still waiting for some long running init to
- * complete.
+ * Sometimes when starting services we face situations where messages come in that require computation but we can't do
+ * that computation yet because we are still waiting for some long running init to complete.
*
- * This class lets you queue up closures to run at a later date and later on you can call run() to
- * run all the previously queued work.
+ * This class lets you queue up closures to run at a later date and later on you can call run() to run all the
+ * previously queued work.
*/
-class DeferredExecution : Logging {
+class DeferredExecution {
private val queue = mutableListOf<() -> Unit>()
- /// Queue some new work
+ // / Queue some new work
fun add(fn: () -> Unit) {
queue.add(fn)
}
- /// run all work in the queue and clear it to be ready to accept new work
+ // / run all work in the queue and clear it to be ready to accept new work
fun run() {
- debug("Running deferred execution numjobs=${queue.size}")
- queue.forEach {
- it()
- }
+ Timber.d("Running deferred execution numjobs=${queue.size}")
+ queue.forEach { it() }
queue.clear()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/geeksville/mesh/concurrent/SyncContinuation.kt b/app/src/main/java/com/geeksville/mesh/concurrent/SyncContinuation.kt
index 1fd6462d9..6e7d56ce8 100644
--- a/app/src/main/java/com/geeksville/mesh/concurrent/SyncContinuation.kt
+++ b/app/src/main/java/com/geeksville/mesh/concurrent/SyncContinuation.kt
@@ -16,29 +16,23 @@
*/
package com.geeksville.mesh.concurrent
-
-import com.geeksville.mesh.android.Logging
-
-/**
- * A deferred execution object (with various possible implementations)
- */
-interface Continuation : Logging {
+/** A deferred execution object (with various possible implementations) */
+interface Continuation {
abstract fun resume(res: Result)
// syntactic sugar
fun resumeSuccess(res: T) = resume(Result.success(res))
+
fun resumeWithException(ex: Throwable) = try {
resume(Result.failure(ex))
} catch (ex: Throwable) {
- // errormsg("Ignoring $ex while resuming, because we are the ones who threw it")
+ // Timber.e("Ignoring $ex while resuming, because we are the ones who threw it")
throw ex
}
}
-/**
- * An async continuation that just calls a callback when the result is available
- */
+/** An async continuation that just calls a callback when the result is available */
class CallbackContinuation(private val cb: (Result) -> Unit) : Continuation {
override fun resume(res: Result) = cb(res)
}
@@ -46,8 +40,8 @@ class CallbackContinuation(private val cb: (Result) -> Unit) : Continua
/**
* This is a blocking/threaded version of coroutine Continuation
*
- * A little bit ugly, but the coroutine version has a nasty internal bug that showed up
- * in my SyncBluetoothDevice so I needed a quick workaround.
+ * A little bit ugly, but the coroutine version has a nasty internal bug that showed up in my SyncBluetoothDevice so I
+ * needed a quick workaround.
*/
class SyncContinuation : Continuation {
@@ -84,8 +78,8 @@ class SyncContinuation : Continuation {
}
/**
- * Calls an init function which is responsible for saving our continuation so that some
- * other thread can call resume or resume with exception.
+ * Calls an init function which is responsible for saving our continuation so that some other thread can call resume or
+ * resume with exception.
*
* Essentially this is a blocking version of the (buggy) coroutine suspendCoroutine
*/
diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
index 0c937263b..ad71ca28b 100644
--- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
@@ -26,7 +26,6 @@ import android.os.RemoteException
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
@@ -53,6 +52,7 @@ import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
+import timber.log.Timber
import javax.inject.Inject
/**
@@ -108,8 +108,7 @@ constructor(
private val networkRepository: NetworkRepository,
private val radioInterfaceService: RadioInterfaceService,
private val recentAddressesDataSource: RecentAddressesDataSource,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
private val context: Context
get() = application.applicationContext
@@ -199,12 +198,12 @@ constructor(
init {
serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope)
- debug("BTScanModel created")
+ Timber.d("BTScanModel created")
}
override fun onCleared() {
super.onCleared()
- debug("BTScanModel cleared")
+ Timber.d("BTScanModel cleared")
}
fun setErrorText(text: String) {
@@ -233,11 +232,11 @@ constructor(
fun stopScan() {
if (scanJob != null) {
- debug("stopping scan")
+ Timber.d("stopping scan")
try {
scanJob?.cancel()
} catch (ex: Throwable) {
- warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
+ Timber.w("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
} finally {
scanJob = null
}
@@ -252,7 +251,7 @@ constructor(
@SuppressLint("MissingPermission")
fun startScan() {
- debug("starting classic scan")
+ Timber.d("starting classic scan")
_spinner.value = true
scanJob =
@@ -281,7 +280,7 @@ constructor(
try {
serviceRepository.meshService?.let { service -> MeshService.changeDeviceAddress(context, service, address) }
} catch (ex: RemoteException) {
- errormsg("changeDeviceSelection failed, probably it is shutting down", ex)
+ Timber.e(ex, "changeDeviceSelection failed, probably it is shutting down")
// ignore the failure and the GUI won't be updating anyways
}
}
@@ -289,14 +288,14 @@ constructor(
@SuppressLint("MissingPermission")
private fun requestBonding(it: DeviceListEntry) {
val device = bluetoothRepository.getRemoteDevice(it.address) ?: return
- info("Starting bonding for ${device.anonymize}")
+ Timber.i("Starting bonding for ${device.anonymize}")
bluetoothRepository
.createBond(device)
.onEach { state ->
- debug("Received bond state changed $state")
+ Timber.d("Received bond state changed $state")
if (state != BluetoothDevice.BOND_BONDING) {
- debug("Bonding completed, state=$state")
+ Timber.d("Bonding completed, state=$state")
if (state == BluetoothDevice.BOND_BONDED) {
setErrorText(context.getString(R.string.pairing_completed))
changeDeviceAddress("x${device.address}")
@@ -307,7 +306,7 @@ constructor(
}
.catch { ex ->
// We ignore missing BT adapters, because it lets us run on the emulator
- warn("Failed creating Bluetooth bond: ${ex.message}")
+ Timber.w("Failed creating Bluetooth bond: ${ex.message}")
}
.launchIn(viewModelScope)
}
@@ -317,10 +316,10 @@ constructor(
.requestPermission(it.driver.device)
.onEach { granted ->
if (granted) {
- info("User approved USB access")
+ Timber.i("User approved USB access")
changeDeviceAddress(it.fullAddress)
} else {
- errormsg("USB permission denied for device ${it.address}")
+ Timber.e("USB permission denied for device ${it.address}")
}
}
.launchIn(viewModelScope)
diff --git a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt
index e9548228e..a26588d90 100644
--- a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt
@@ -26,7 +26,6 @@ import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ui.debug.FilterMode
import com.google.protobuf.InvalidProtocolBufferException
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -45,6 +44,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
+import timber.log.Timber
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@@ -203,8 +203,7 @@ class DebugViewModel
constructor(
private val meshLogRepository: MeshLogRepository,
private val nodeRepository: NodeRepository,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
val meshLog: StateFlow> =
meshLogRepository
@@ -240,7 +239,7 @@ constructor(
}
init {
- debug("DebugViewModel created")
+ Timber.d("DebugViewModel created")
viewModelScope.launch {
combine(searchManager.searchText, filterManager.filteredLogs) { searchText, logs ->
searchManager.findSearchMatches(searchText, logs)
@@ -253,7 +252,7 @@ constructor(
override fun onCleared() {
super.onCleared()
- debug("DebugViewModel cleared")
+ Timber.d("DebugViewModel cleared")
}
private fun toUiState(databaseLogs: List) = databaseLogs
diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
index f31196933..bad83f8d2 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -34,7 +34,6 @@ import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -67,6 +66,7 @@ import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import org.meshtastic.feature.map.model.CustomTileSource
+import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@@ -211,8 +211,7 @@ constructor(
private val deviceHardwareRepository: DeviceHardwareRepository,
private val firmwareReleaseRepository: FirmwareReleaseRepository,
private val mapPrefs: MapPrefs,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
private val destNum = savedStateHandle.toRoute().destNum
private fun MeshLog.hasValidTraceroute(): Boolean =
@@ -376,15 +375,15 @@ constructor(
.onEach { firmwareEdition -> _state.update { state -> state.copy(firmwareEdition = firmwareEdition) } }
.launchIn(viewModelScope)
- debug("MetricsViewModel created")
+ Timber.d("MetricsViewModel created")
} else {
- debug("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
+ Timber.d("MetricsViewModel: destNum is null, skipping metrics flows initialization.")
}
}
override fun onCleared() {
super.onCleared()
- debug("MetricsViewModel cleared")
+ Timber.d("MetricsViewModel cleared")
}
fun setTimeFrame(timeFrame: TimeFrame) {
@@ -427,7 +426,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
- errormsg("Can't write file error: ${ex.message}")
+ Timber.e(ex, "Can't write file error")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index baec8f3ae..e5a482bb4 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -35,7 +35,6 @@ import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.channel
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.channelSettings
@@ -44,7 +43,6 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
-import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -71,11 +69,11 @@ import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource
-import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
+import timber.log.Timber
import javax.inject.Inject
// Given a human name, strip out the first letter of the first three words and return that as the
@@ -165,24 +163,12 @@ constructor(
firmwareReleaseRepository: FirmwareReleaseRepository,
private val uiPreferencesDataSource: UiPreferencesDataSource,
private val meshServiceNotifications: MeshServiceNotifications,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
val theme: StateFlow = uiPreferencesDataSource.theme
- val firmwareVersion = myNodeInfo.mapNotNull { nodeInfo -> nodeInfo?.firmwareVersion }
-
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
- val deviceHardware: StateFlow =
- ourNodeInfo
- .mapNotNull { nodeInfo ->
- nodeInfo?.user?.hwModel?.let { hwModel ->
- deviceHardwareRepository.getDeviceHardwareByModel(hwModel.safeNumber()).getOrNull()
- }
- }
- .stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null)
-
val clientNotification: StateFlow = serviceRepository.clientNotification
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
@@ -306,7 +292,7 @@ constructor(
.onEach { channelSet -> _channels.value = channelSet }
.launchIn(viewModelScope)
- debug("ViewModel created")
+ Timber.d("ViewModel created")
}
private val _sharedContactRequested: MutableStateFlow = MutableStateFlow(null)
@@ -332,7 +318,7 @@ constructor(
fun requestChannelUrl(url: Uri) = runCatching { _requestChannelSet.value = url.toChannelSet() }
.onFailure { ex ->
- errormsg("Channel url error: ${ex.message}")
+ Timber.e(ex, "Channel url error")
showSnackBar(R.string.channel_invalid)
}
@@ -361,7 +347,7 @@ constructor(
override fun onCleared() {
super.onCleared()
- debug("ViewModel cleared")
+ Timber.d("ViewModel cleared")
}
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
@@ -374,7 +360,7 @@ constructor(
try {
meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
- errormsg("Set config error:", ex)
+ Timber.e(ex, "Set config error")
}
}
@@ -382,7 +368,7 @@ constructor(
try {
meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
- errormsg("Set channel error:", ex)
+ Timber.e(ex, "Set channel error")
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
index 817865e2f..fa8272547 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt
@@ -28,7 +28,6 @@ import androidx.annotation.RequiresPermission
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasBluetoothPermission
import com.geeksville.mesh.util.registerReceiverCompat
import kotlinx.coroutines.flow.Flow
@@ -38,6 +37,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -51,7 +51,7 @@ constructor(
private val bluetoothBroadcastReceiverLazy: dagger.Lazy,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
-) : Logging {
+) {
private val _state =
MutableStateFlow(
BluetoothState(
@@ -126,7 +126,7 @@ constructor(
} ?: BluetoothState()
_state.emit(newState)
- debug("Detected our bluetooth access=$newState")
+ Timber.d("Detected our bluetooth access=$newState")
}
companion object {
diff --git a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt
index 4d9b791d6..18359f8e6 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt
@@ -27,46 +27,47 @@ import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
+import com.geeksville.mesh.MeshUtilApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class LocationRepository @Inject constructor(
+class LocationRepository
+@Inject
+constructor(
private val context: Application,
private val locationManager: dagger.Lazy,
-) : Logging {
+) {
- /**
- * Status of whether the app is actively subscribed to location changes.
- */
+ /** Status of whether the app is actively subscribed to location changes. */
private val _receivingLocationUpdates: MutableStateFlow = MutableStateFlow(false)
- val receivingLocationUpdates: StateFlow get() = _receivingLocationUpdates
+ val receivingLocationUpdates: StateFlow
+ get() = _receivingLocationUpdates
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
private fun LocationManager.requestLocationUpdates() = callbackFlow {
-
val intervalMs = 30 * 1000L // 30 seconds
val minDistanceM = 0f
- val locationRequest = LocationRequestCompat.Builder(intervalMs)
- .setMinUpdateDistanceMeters(minDistanceM)
- .setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
- .build()
+ val locationRequest =
+ LocationRequestCompat.Builder(intervalMs)
+ .setMinUpdateDistanceMeters(minDistanceM)
+ .setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
+ .build()
val locationListener = LocationListenerCompat { location ->
if (location.hasAltitude() && !LocationCompat.hasMslAltitude(location)) {
try {
AltitudeConverterCompat.addMslAltitudeToLocation(context, location)
} catch (e: Exception) {
- errormsg("addMslAltitudeToLocation() failed", e)
+ Timber.e(e, "addMslAltitudeToLocation() failed")
}
}
// info("New location: $location")
@@ -83,9 +84,11 @@ class LocationRepository @Inject constructor(
}
}
- info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
+ Timber.i(
+ "Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m",
+ )
_receivingLocationUpdates.value = true
- GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
+ MeshUtilApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
try {
providerList.forEach { provider ->
@@ -102,17 +105,15 @@ class LocationRepository @Inject constructor(
}
awaitClose {
- info("Stopping location requests")
+ Timber.i("Stopping location requests")
_receivingLocationUpdates.value = false
- GeeksvilleApplication.analytics.track("location_stop")
+ MeshUtilApplication.analytics.track("location_stop")
LocationManagerCompat.removeUpdates(this@requestLocationUpdates, locationListener)
}
}
- /**
- * Observable flow for location updates
- */
+ /** Observable flow for location updates */
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
fun getLocations() = locationManager.get().requestLocationUpdates()
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt
index 129dd6ebe..2e0425550 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt
@@ -18,7 +18,6 @@
package com.geeksville.mesh.repository.network
import com.geeksville.mesh.MeshProtos.MqttClientProxyMessage
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.mqttClientProxyMessage
import com.geeksville.mesh.util.ignoreException
import com.google.protobuf.ByteString
@@ -37,6 +36,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.util.subscribeList
+import timber.log.Timber
import java.net.URI
import java.security.SecureRandom
import javax.inject.Inject
@@ -50,7 +50,7 @@ class MQTTRepository
constructor(
private val radioConfigRepository: RadioConfigRepository,
private val nodeRepository: NodeRepository,
-) : Logging {
+) {
companion object {
/**
@@ -70,7 +70,7 @@ constructor(
private var mqttClient: MqttAsyncClient? = null
fun disconnect() {
- info("MQTT Disconnected")
+ Timber.i("MQTT Disconnected")
mqttClient?.apply {
ignoreException { disconnect() }
close(true)
@@ -110,7 +110,7 @@ constructor(
val callback =
object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
- info("MQTT connectComplete: $serverURI reconnect: $reconnect")
+ Timber.i("MQTT connectComplete: $serverURI reconnect: $reconnect")
channelSet.subscribeList
.ifEmpty {
return
@@ -123,7 +123,7 @@ constructor(
}
override fun connectionLost(cause: Throwable) {
- info("MQTT connectionLost cause: $cause")
+ Timber.i("MQTT connectionLost cause: $cause")
if (cause is IllegalArgumentException) close(cause)
}
@@ -138,7 +138,7 @@ constructor(
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {
- info("MQTT deliveryComplete messageId: ${token?.messageId}")
+ Timber.i("MQTT deliveryComplete messageId: ${token?.messageId}")
}
}
@@ -161,15 +161,15 @@ constructor(
private fun subscribe(topic: String) {
mqttClient?.subscribe(topic, DEFAULT_QOS)
- info("MQTT Subscribed to topic: $topic")
+ Timber.i("MQTT Subscribed to topic: $topic")
}
fun publish(topic: String, data: ByteArray, retained: Boolean) {
try {
val token = mqttClient?.publish(topic, data, DEFAULT_QOS, retained)
- info("MQTT Publish messageId: ${token?.messageId}")
+ Timber.i("MQTT Publish messageId: ${token?.messageId}")
} catch (ex: Exception) {
- errormsg("MQTT Publish error: ${ex.message}")
+ Timber.e("MQTT Publish error: ${ex.message}")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/NetworkRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/network/NetworkRepository.kt
index 13dced15a..a7bb09006 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/network/NetworkRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/network/NetworkRepository.kt
@@ -21,7 +21,6 @@ import android.net.ConnectivityManager
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import com.geeksville.mesh.CoroutineDispatchers
-import com.geeksville.mesh.android.Logging
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
@@ -29,21 +28,19 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class NetworkRepository @Inject constructor(
+class NetworkRepository
+@Inject
+constructor(
private val nsdManagerLazy: dagger.Lazy,
private val connectivityManager: dagger.Lazy,
private val dispatchers: CoroutineDispatchers,
-) : Logging {
+) {
val networkAvailable: Flow
- get() = connectivityManager.get().networkAvailable()
- .flowOn(dispatchers.io)
- .conflate()
+ get() = connectivityManager.get().networkAvailable().flowOn(dispatchers.io).conflate()
val resolvedList: Flow>
- get() = nsdManagerLazy.get().serviceList(SERVICE_TYPE)
- .flowOn(dispatchers.io)
- .conflate()
+ get() = nsdManagerLazy.get().serviceList(SERVICE_TYPE).flowOn(dispatchers.io).conflate()
companion object {
internal const val SERVICE_PORT = 4403
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
index d489d5f32..feb2e9c1b 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
@@ -21,7 +21,6 @@ import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.service.BLECharacteristicNotFoundException
@@ -39,6 +38,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.util.anonymize
+import timber.log.Timber
import java.lang.reflect.Method
import java.util.UUID
@@ -105,8 +105,7 @@ constructor(
bluetoothRepository: BluetoothRepository,
private val service: RadioInterfaceService,
@Assisted val address: String,
-) : IRadioInterface,
- Logging {
+) : IRadioInterface {
companion object {
// this service UUID is publicly visible for scanning
@@ -162,7 +161,7 @@ constructor(
} catch (ex: CancellationException) {
break
} catch (ex: Exception) {
- debug("RSSI polling error: ${ex.message}")
+ Timber.d("RSSI polling error: ${ex.message}")
}
}
}
@@ -193,7 +192,7 @@ constructor(
// device is off/not connected)
val device = bluetoothRepository.getRemoteDevice(address)
if (device != null) {
- info("Creating radio interface service. device=${address.anonymize}")
+ Timber.i("Creating radio interface service. device=${address.anonymize}")
// Note this constructor also does no comm
val s = SafeBluetooth(context, device)
@@ -201,7 +200,7 @@ constructor(
startConnect()
} else {
- errormsg("Bluetooth adapter not found, assuming running on the emulator!")
+ Timber.e("Bluetooth adapter not found, assuming running on the emulator!")
}
}
@@ -210,7 +209,7 @@ constructor(
try {
safe?.let { s ->
val uuid = BTM_TORADIO_CHARACTER
- debug("queuing ${p.size} bytes to $uuid")
+ Timber.d("queuing ${p.size} bytes to $uuid")
// Note: we generate a new characteristic each time, because we are about to
// change the data and we want the data stored in the closure
@@ -219,7 +218,7 @@ constructor(
s.asyncWriteCharacteristic(toRadio, p) { r ->
try {
r.getOrThrow()
- debug("write of ${p.size} bytes to $uuid completed")
+ Timber.d("write of ${p.size} bytes to $uuid completed")
if (isFirstSend) {
isFirstSend = false
@@ -241,10 +240,10 @@ constructor(
private fun scheduleReconnect(reason: String) {
stopRssiPolling()
if (reconnectJob == null) {
- warn("Scheduling reconnect because $reason")
+ Timber.w("Scheduling reconnect because $reason")
reconnectJob = service.serviceScope.handledLaunch { retryDueToException() }
} else {
- warn("Skipping reconnect for $reason")
+ Timber.w("Skipping reconnect for $reason")
}
}
@@ -260,13 +259,13 @@ constructor(
.clone() // We clone the array just in case, I'm not sure if they keep reusing the array
if (b.isNotEmpty()) {
- debug("Received ${b.size} bytes from radio")
+ Timber.d("Received ${b.size} bytes from radio")
service.handleFromRadio(b)
// Queue up another read, until we run out of packets
doReadFromRadio(firstRead)
} else {
- debug("Done reading from radio, fromradio is empty")
+ Timber.d("Done reading from radio, fromradio is empty")
if (firstRead) {
// If we just finished our initial download, now we want to start listening for notifies
startWatchingFromNum()
@@ -287,7 +286,7 @@ constructor(
exceptionReporter {
// If the gatt has been destroyed, skip the refresh attempt
safe?.gatt?.let { gatt ->
- debug("DOING FORCE REFRESH")
+ Timber.d("DOING FORCE REFRESH")
val refresh: Method = gatt.javaClass.getMethod("refresh")
refresh.invoke(gatt)
}
@@ -309,12 +308,12 @@ constructor(
try {
if (fromNumChanged) {
fromNumChanged = false
- debug("fromNum changed, so we are reading new messages")
+ Timber.d("fromNum changed, so we are reading new messages")
doReadFromRadio(false)
}
} catch (e: RadioNotConnectedException) {
// Don't report autobugs for this, getting an exception here is expected behavior
- errormsg("Ending FromNum read, radio not connected", e)
+ Timber.e(e, "Ending FromNum read, radio not connected")
}
}
}
@@ -334,7 +333,7 @@ constructor(
val backoffMillis = (1000 * (1 shl reconnectAttempts.coerceAtMost(maxReconnectionAttempts))).toLong()
// Exponential backoff, capped at 64s
reconnectAttempts++
- warn(
+ Timber.w(
"Forcing disconnect and hopefully device will comeback" +
" (disabling forced refresh). Reconnect attempt $reconnectAttempts," +
" waiting ${backoffMillis}ms.",
@@ -350,18 +349,18 @@ constructor(
service.onDisconnect(false) // assume we will fail
delay(backoffMillis) // Give some nasty time for buggy BLE stacks to shutdown
reconnectJob = null // Any new reconnect requests after this will be allowed to run
- warn("Attempting reconnect")
+ Timber.w("Attempting reconnect")
if (safe != null) {
// check again, because we just slept, and someone might have closed our interface
startConnect()
} else {
- warn("Not connecting, because safe==null, someone must have closed us")
+ Timber.w("Not connecting, because safe==null, someone must have closed us")
}
} else {
- warn("Abandoning reconnect because safe==null, someone must have closed the device")
+ Timber.w("Abandoning reconnect because safe==null, someone must have closed the device")
}
} catch (ex: CancellationException) {
- warn("retryDueToException was cancelled")
+ Timber.w("retryDueToException was cancelled")
} finally {
reconnectJob = null
}
@@ -377,7 +376,7 @@ constructor(
private fun doDiscoverServicesAndInit() {
val s = safe
if (s == null) {
- warn("Interface is shutting down, so skipping discover")
+ Timber.w("Interface is shutting down, so skipping discover")
} else {
s.asyncDiscoverServices { discRes ->
try {
@@ -385,7 +384,7 @@ constructor(
service.serviceScope.handledLaunch {
try {
- debug("Discovered services!")
+ Timber.d("Discovered services!")
delay(
1000,
) // android BLE is buggy and needs a 1000ms sleep before calling getChracteristic, or you
@@ -412,7 +411,7 @@ constructor(
}
} catch (ex: BLEException) {
if (s.gatt == null) {
- warn("GATT was closed while discovering, assume we are shutting down")
+ Timber.w("GATT was closed while discovering, assume we are shutting down")
} else {
scheduleReconnect("Unexpected error discovering services, forcing disconnect $ex")
}
@@ -429,7 +428,7 @@ constructor(
reconnectAttempts = 0 // Reset backoff on successful connection
service.serviceScope.handledLaunch {
- info("Connected to radio!")
+ Timber.i("Connected to radio!")
startRssiPolling()
if (
@@ -453,7 +452,7 @@ constructor(
safe?.asyncRequestMtu(512) { mtuRes ->
try {
mtuRes.getOrThrow()
- debug("MTU change attempted")
+ Timber.d("MTU change attempted")
// throw BLEException("Test MTU set failed")
@@ -474,7 +473,7 @@ constructor(
stopRssiPolling()
if (safe != null) {
- info("Closing BluetoothInterface")
+ Timber.i("Closing BluetoothInterface")
val s = safe
safe = null // We do this first, because if we throw we still want to mark that we no longer have a valid
// connection
@@ -482,10 +481,10 @@ constructor(
try {
s?.close()
} catch (_: BLEConnectionClosing) {
- warn("Ignoring BLE errors while closing")
+ Timber.w("Ignoring BLE errors while closing")
}
} else {
- debug("Radio was not connected, skipping disable")
+ Timber.d("Radio was not connected, skipping disable")
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt
index 03cf81231..ab6022e26 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt
@@ -17,9 +17,9 @@
package com.geeksville.mesh.repository.radio
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import org.meshtastic.core.model.util.anonymize
+import timber.log.Timber
import javax.inject.Inject
/** Bluetooth backend implementation. */
@@ -28,15 +28,14 @@ class BluetoothInterfaceSpec
constructor(
private val factory: BluetoothInterfaceFactory,
private val bluetoothRepository: BluetoothRepository,
-) : InterfaceSpec,
- Logging {
+) : InterfaceSpec {
override fun createInterface(rest: String): BluetoothInterface = factory.create(rest)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean {
val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
- warn("Ignoring stale bond to ${rest.anonymize}")
+ Timber.w("Ignoring stale bond to ${rest.anonymize}")
false
} else {
true
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
index cf9644f9d..28bb560bb 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
@@ -24,7 +24,6 @@ import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.TelemetryProtos
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.channel
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.config
@@ -39,6 +38,7 @@ import kotlinx.coroutines.delay
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Position
+import timber.log.Timber
import kotlin.random.Random
private val defaultLoRaConfig =
@@ -59,8 +59,7 @@ class MockInterface
constructor(
private val service: RadioInterfaceService,
@Assisted val address: String,
-) : IRadioInterface,
- Logging {
+) : IRadioInterface {
companion object {
private const val MY_NODE = 0x42424242
@@ -72,7 +71,7 @@ constructor(
private val packetIdSequence = generateSequence { currentPacketId++ }.iterator()
init {
- info("Starting the mock interface")
+ Timber.i("Starting the mock interface")
service.onConnect() // Tell clients they can use the API
}
@@ -87,7 +86,7 @@ constructor(
data != null && data.portnum == Portnums.PortNum.ADMIN_APP ->
handleAdminPacket(pr, AdminProtos.AdminMessage.parseFrom(data.payload))
pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr)
- else -> info("Ignoring data sent to mock interface $pr")
+ else -> Timber.i("Ignoring data sent to mock interface $pr")
}
}
@@ -109,12 +108,12 @@ constructor(
}
}
- else -> info("Ignoring admin sent to mock interface $d")
+ else -> Timber.i("Ignoring admin sent to mock interface $d")
}
}
override fun close() {
- info("Closing the mock interface")
+ Timber.i("Closing the mock interface")
}
// / Generate a fake text message from a node
@@ -298,7 +297,7 @@ constructor(
}
private fun sendConfigResponse(configId: Int) {
- debug("Sending mock config response")
+ Timber.d("Sending mock config response")
// / Generate a fake node info entry
@Suppress("MagicNumber")
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
index 428f2e05e..94dd06dd5 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
@@ -18,15 +18,15 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
+import android.provider.Settings
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos
+import com.geeksville.mesh.MeshUtilApplication
import com.geeksville.mesh.android.BinaryLogFile
import com.geeksville.mesh.android.BuildUtils
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
@@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.service.ConnectionState
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -73,7 +74,7 @@ constructor(
private val processLifecycle: Lifecycle,
private val radioPrefs: RadioPrefs,
private val interfaceFactory: InterfaceFactory,
-) : Logging {
+) {
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow = _connectionState.asStateFlow()
@@ -138,7 +139,7 @@ constructor(
fun keepAlive(now: Long = System.currentTimeMillis()) {
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
- info("Sending ToRadio heartbeat")
+ Timber.i("Sending ToRadio heartbeat")
val heartbeat =
MeshProtos.ToRadio.newBuilder().setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
handleSendToRadio(heartbeat.toByteArray())
@@ -150,7 +151,8 @@ constructor(
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
interfaceFactory.toInterfaceAddress(interfaceId, rest)
- fun isMockInterface(): Boolean = BuildConfig.DEBUG || (context as GeeksvilleApplication).isInTestLab
+ 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
@@ -198,7 +200,7 @@ constructor(
}
private fun broadcastConnectionChanged(newState: ConnectionState) {
- debug("Broadcasting connection state change to $newState")
+ Timber.d("Broadcasting connection state change to $newState")
processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionState.emit(newState) }
}
@@ -219,7 +221,7 @@ constructor(
keepAlive(System.currentTimeMillis())
}
- // ignoreException { debug("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
+ // ignoreException { Timber.d("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
emitReceiveActivity()
@@ -241,13 +243,13 @@ constructor(
/** Start our configured interface (if it isn't already running) */
private fun startInterface() {
if (radioIf !is NopInterface) {
- warn("Can't start interface - $radioIf is already running")
+ Timber.w("Can't start interface - $radioIf is already running")
} else {
val address = getBondedDeviceAddress()
if (address == null) {
- warn("No bonded mesh radio, can't start interface")
+ Timber.w("No bonded mesh radio, can't start interface")
} else {
- info("Starting radio ${address.anonymize}")
+ Timber.i("Starting radio ${address.anonymize}")
isStarted = true
if (logSends) {
@@ -271,7 +273,7 @@ constructor(
private fun stopInterface() {
val r = radioIf
- info("stopping interface $r")
+ Timber.i("stopping interface $r")
isStarted = false
radioIf = interfaceFactory.nopInterface
r.close()
@@ -301,18 +303,18 @@ constructor(
*/
private fun setBondedDeviceAddress(address: String?): Boolean =
if (getBondedDeviceAddress() == address && isStarted) {
- warn("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
+ Timber.w("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
false
} else {
// Record that this use has configured a new radio
- GeeksvilleApplication.analytics.track("mesh_bond")
+ MeshUtilApplication.analytics.track("mesh_bond")
// Ignore any errors that happen while closing old device
ignoreException { stopInterface() }
// The device address "n" can be used to mean none
- debug("Setting bonded device to ${address.anonymize}")
+ Timber.d("Setting bonded device to ${address.anonymize}")
// Stores the address if non-null, otherwise removes the pref
radioPrefs.devAddr = address
@@ -353,14 +355,14 @@ constructor(
// Use tryEmit for SharedFlow as it's non-blocking
val emitted = _meshActivity.tryEmit(MeshActivity.Send)
if (!emitted) {
- debug("MeshActivity.Send event was not emitted due to buffer overflow or no collectors")
+ Timber.d("MeshActivity.Send event was not emitted due to buffer overflow or no collectors")
}
}
private fun emitReceiveActivity() {
val emitted = _meshActivity.tryEmit(MeshActivity.Receive)
if (!emitted) {
- debug("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
+ Timber.d("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
index 7b1904de6..90ecfa946 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
@@ -17,23 +17,23 @@
package com.geeksville.mesh.repository.radio
-import com.geeksville.mesh.android.Logging
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 timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
-/**
- * An interface that assumes we are talking to a meshtastic device via USB serial
- */
-class SerialInterface @AssistedInject constructor(
+/** An interface that assumes we are talking to a meshtastic device via USB serial */
+class SerialInterface
+@AssistedInject
+constructor(
service: RadioInterfaceService,
private val serialInterfaceSpec: SerialInterfaceSpec,
private val usbRepository: UsbRepository,
@Assisted private val address: String,
-) : StreamInterface(service), Logging {
+) : StreamInterface(service) {
private var connRef = AtomicReference()
init {
@@ -48,39 +48,42 @@ class SerialInterface @AssistedInject constructor(
override fun connect() {
val device = serialInterfaceSpec.findSerial(address)
if (device == null) {
- errormsg("Can't find device")
+ Timber.e("Can't find device")
} else {
- info("Opening $device")
+ Timber.i("Opening $device")
val onConnect: () -> Unit = { super.connect() }
- usbRepository.createSerialConnection(device, object : SerialConnectionListener {
- override fun onMissingPermission() {
- errormsg("Need permissions for port")
- }
+ usbRepository
+ .createSerialConnection(
+ device,
+ object : SerialConnectionListener {
+ override fun onMissingPermission() {
+ Timber.e("Need permissions for port")
+ }
- override fun onConnected() {
- onConnect.invoke()
- }
+ override fun onConnected() {
+ onConnect.invoke()
+ }
- override fun onDataReceived(bytes: ByteArray) {
- debug("Received ${bytes.size} byte(s)")
- bytes.forEach(::readChar)
- }
+ override fun onDataReceived(bytes: ByteArray) {
+ Timber.d("Received ${bytes.size} byte(s)")
+ bytes.forEach(::readChar)
+ }
- override fun onDisconnected(thrown: Exception?) {
- thrown?.let { e ->
- errormsg("Serial error: $e")
- }
- debug("$device disconnected")
- onDeviceDisconnect(false)
+ override fun onDisconnected(thrown: Exception?) {
+ thrown?.let { e -> Timber.e("Serial error: $e") }
+ Timber.d("$device disconnected")
+ onDeviceDisconnect(false)
+ }
+ },
+ )
+ .also { conn ->
+ connRef.set(conn)
+ conn.connect()
}
- }).also { conn ->
- connRef.set(conn)
- conn.connect()
- }
}
}
override fun sendBytes(p: ByteArray) {
connRef.get()?.sendBytes(p)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
index 1afeeb1cd..1134c6c02 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
@@ -17,16 +17,14 @@
package com.geeksville.mesh.repository.radio
-import com.geeksville.mesh.android.Logging
+import timber.log.Timber
/**
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP
* probably)
*/
-abstract class StreamInterface(protected val service: RadioInterfaceService) :
- Logging,
- IRadioInterface {
- companion object : Logging {
+abstract class StreamInterface(protected val service: RadioInterfaceService) : IRadioInterface {
+ companion object {
private const val START1 = 0x94.toByte()
private const val START2 = 0xc3.toByte()
private const val MAX_TO_FROM_RADIO_SIZE = 512
@@ -43,7 +41,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
private var packetLen = 0
override fun close() {
- debug("Closing stream for good")
+ Timber.d("Closing stream for good")
onDeviceDisconnect(true)
}
@@ -92,7 +90,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
when (val c = b.toChar()) {
'\r' -> {} // ignore
'\n' -> {
- debug("DeviceLog: $debugLineBuf")
+ Timber.d("DeviceLog: $debugLineBuf")
debugLineBuf.clear()
}
else -> debugLineBuf.append(c)
@@ -106,7 +104,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) :
var nextPtr = ptr + 1
fun lostSync() {
- errormsg("Lost protocol sync")
+ Timber.e("Lost protocol sync")
nextPtr = 0
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
index be621e1db..66d91cf43 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
@@ -17,7 +17,6 @@
package com.geeksville.mesh.repository.radio
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.util.Exceptions
@@ -26,6 +25,7 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
+import timber.log.Timber
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
@@ -34,10 +34,8 @@ import java.net.InetAddress
import java.net.Socket
import java.net.SocketTimeoutException
-class TCPInterface @AssistedInject constructor(
- service: RadioInterfaceService,
- @Assisted private val address: String,
-) : StreamInterface(service), Logging {
+class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @Assisted private val address: String) :
+ StreamInterface(service) {
companion object {
const val MAX_RETRIES_ALLOWED = Int.MAX_VALUE
@@ -67,7 +65,7 @@ class TCPInterface @AssistedInject constructor(
override fun onDeviceDisconnect(waitForStopped: Boolean) {
val s = socket
if (s != null) {
- debug("Closing TCP socket")
+ Timber.d("Closing TCP socket")
s.close()
socket = null
}
@@ -80,7 +78,7 @@ class TCPInterface @AssistedInject constructor(
try {
startConnect()
} catch (ex: IOException) {
- errormsg("IOException in TCP reader: $ex")
+ Timber.e("IOException in TCP reader: $ex")
onDeviceDisconnect(false)
} catch (ex: Throwable) {
Exceptions.report(ex, "Exception in TCP reader")
@@ -89,22 +87,22 @@ class TCPInterface @AssistedInject constructor(
if (retryCount > MAX_RETRIES_ALLOWED) break
- debug("Reconnect attempt $retryCount in ${backoffDelay / 1000}s")
+ Timber.d("Reconnect attempt $retryCount in ${backoffDelay / 1000}s")
delay(backoffDelay)
retryCount++
backoffDelay = minOf(backoffDelay * 2, MAX_BACKOFF_MILLIS)
}
- debug("Exiting TCP reader")
+ Timber.d("Exiting TCP reader")
}
}
// Create a socket to make the connection with the server
private suspend fun startConnect() = withContext(Dispatchers.IO) {
- debug("TCP connecting to $address")
+ Timber.d("TCP connecting to $address")
- val (host, port) = address.split(":", limit = 2)
- .let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
+ val (host, port) =
+ address.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
Socket(InetAddress.getByName(host), port).use { socket ->
socket.tcpNoDelay = true
@@ -121,18 +119,20 @@ class TCPInterface @AssistedInject constructor(
backoffDelay = MIN_BACKOFF_MILLIS
var timeoutCount = 0
- while (timeoutCount < 180) try { // close after 90s of inactivity
- val c = inputStream.read()
- if (c == -1) {
- warn("Got EOF on TCP stream")
- break
- } else {
- timeoutCount = 0
- readChar(c.toByte())
+ while (timeoutCount < 180) {
+ try { // close after 90s of inactivity
+ val c = inputStream.read()
+ if (c == -1) {
+ Timber.w("Got EOF on TCP stream")
+ break
+ } else {
+ timeoutCount = 0
+ readChar(c.toByte())
+ }
+ } catch (ex: SocketTimeoutException) {
+ timeoutCount++
+ // Ignore and start another read
}
- } catch (ex: SocketTimeoutException) {
- timeoutCount++
- // Ignore and start another read
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt
index 99260c59a..ad9c3bc47 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt
@@ -18,11 +18,11 @@
package com.geeksville.mesh.repository.usb
import android.hardware.usb.UsbManager
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.ignoreException
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.util.SerialInputOutputManager
+import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@@ -31,8 +31,8 @@ import java.util.concurrent.atomic.AtomicReference
internal class SerialConnectionImpl(
private val usbManagerLazy: dagger.Lazy,
private val device: UsbSerialDriver,
- private val listener: SerialConnectionListener
-) : SerialConnection, Logging {
+ private val listener: SerialConnectionListener,
+) : SerialConnection {
private val port = device.ports[0] // Most devices have just one port (port 0)
private val closedLatch = CountDownLatch(1)
private val closed = AtomicBoolean(false)
@@ -40,7 +40,7 @@ internal class SerialConnectionImpl(
override fun sendBytes(bytes: ByteArray) {
ioRef.get()?.let {
- debug("writing ${bytes.size} byte(s)")
+ Timber.d("writing ${bytes.size} byte(s)")
it.writeAsync(bytes)
}
}
@@ -54,7 +54,7 @@ internal class SerialConnectionImpl(
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
if (waitForStopped) {
- debug("Waiting for USB manager to stop...")
+ Timber.d("Waiting for USB manager to stop...")
closedLatch.await(1, TimeUnit.SECONDS)
}
}
@@ -80,26 +80,31 @@ internal class SerialConnectionImpl(
port.dtr = true
port.rts = true
- debug("Starting serial reader thread")
- val io = SerialInputOutputManager(port, object : SerialInputOutputManager.Listener {
- override fun onNewData(data: ByteArray) {
- listener.onDataReceived(data)
- }
+ Timber.d("Starting serial reader thread")
+ val io =
+ SerialInputOutputManager(
+ port,
+ object : SerialInputOutputManager.Listener {
+ override fun onNewData(data: ByteArray) {
+ listener.onDataReceived(data)
+ }
- override fun onRunError(e: Exception?) {
- closed.set(true)
- ignoreException {
- port.dtr = false
- port.rts = false
- port.close()
+ override fun onRunError(e: Exception?) {
+ closed.set(true)
+ ignoreException {
+ port.dtr = false
+ port.rts = false
+ port.close()
+ }
+ closedLatch.countDown()
+ listener.onDisconnected(e)
+ }
+ },
+ )
+ .apply {
+ readTimeout = 200 // To save battery we only timeout ever so often
+ ioRef.set(this)
}
- closedLatch.countDown()
- listener.onDisconnected(e)
- }
- }).apply {
- readTimeout = 200 // To save battery we only timeout ever so often
- ioRef.set(this)
- }
io.start()
listener.onConnected()
diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbBroadcastReceiver.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbBroadcastReceiver.kt
index 510c3a2a9..e7f60855a 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbBroadcastReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbBroadcastReceiver.kt
@@ -23,23 +23,20 @@ import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.exceptionReporter
import com.geeksville.mesh.util.getParcelableExtraCompat
+import timber.log.Timber
import javax.inject.Inject
-/**
- * A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are
- * changed.
- */
-class UsbBroadcastReceiver @Inject constructor(
- private val usbRepository: UsbRepository
-) : BroadcastReceiver(), Logging {
+/** A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are changed. */
+class UsbBroadcastReceiver @Inject constructor(private val usbRepository: UsbRepository) : BroadcastReceiver() {
// Can be used for registering
- internal val intentFilter get() = IntentFilter().apply {
- addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
- addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
- }
+ internal val intentFilter
+ get() =
+ IntentFilter().apply {
+ addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
+ addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
+ }
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
val device: UsbDevice? = intent.getParcelableExtraCompat(UsbManager.EXTRA_DEVICE)
@@ -47,17 +44,17 @@ class UsbBroadcastReceiver @Inject constructor(
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
- debug("USB device '$deviceName' was detached")
+ Timber.d("USB device '$deviceName' was detached")
usbRepository.refreshState()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
- debug("USB device '$deviceName' was attached")
+ Timber.d("USB device '$deviceName' was attached")
usbRepository.refreshState()
}
UsbManager.EXTRA_PERMISSION_GRANTED -> {
- debug("USB device '$deviceName' was granted permission")
+ Timber.d("USB device '$deviceName' was granted permission")
usbRepository.refreshState()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt
index f34a9e9e9..f60ec4778 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt
@@ -22,59 +22,61 @@ import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.util.registerReceiverCompat
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialProber
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
-/**
- * Repository responsible for maintaining and updating the state of USB connectivity.
- */
+/** Repository responsible for maintaining and updating the state of USB connectivity. */
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
-class UsbRepository @Inject constructor(
+class UsbRepository
+@Inject
+constructor(
private val application: Application,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
private val usbBroadcastReceiverLazy: dagger.Lazy,
private val usbManagerLazy: dagger.Lazy,
- private val usbSerialProberLazy: dagger.Lazy
-) : Logging {
+ private val usbSerialProberLazy: dagger.Lazy,
+) {
private val _serialDevices = MutableStateFlow(emptyMap())
@Suppress("unused") // Retained as public API
- val serialDevices = _serialDevices
- .asStateFlow()
+ val serialDevices = _serialDevices.asStateFlow()
@Suppress("unused") // Retained as public API
- val serialDevicesWithDrivers = _serialDevices
- .mapLatest { serialDevices ->
- val serialProber = usbSerialProberLazy.get()
- buildMap {
- serialDevices.forEach { (k, v) ->
- serialProber.probeDevice(v)?.let { driver ->
- put(k, driver)
- }
+ val serialDevicesWithDrivers =
+ _serialDevices
+ .mapLatest { serialDevices ->
+ val serialProber = usbSerialProberLazy.get()
+ buildMap {
+ serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
}
}
- }.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
+ .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
@Suppress("unused") // Retained as public API
- val serialDevicesWithPermission = _serialDevices
- .mapLatest { serialDevices ->
- usbManagerLazy.get()?.let { usbManager ->
- serialDevices.filterValues { device ->
- usbManager.hasPermission(device)
- }
- } ?: emptyMap()
- }.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
+ val serialDevicesWithPermission =
+ _serialDevices
+ .mapLatest { serialDevices ->
+ usbManagerLazy.get()?.let { usbManager ->
+ serialDevices.filterValues { device -> usbManager.hasPermission(device) }
+ } ?: emptyMap()
+ }
+ .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
@@ -86,23 +88,19 @@ class UsbRepository @Inject constructor(
}
/**
- * Creates a USB serial connection to the specified USB device. State changes and data arrival
- * result in async callbacks on the supplied listener.
+ * Creates a USB serial connection to the specified USB device. State changes and data arrival result in async
+ * callbacks on the supplied listener.
*/
- fun createSerialConnection(device: UsbSerialDriver, listener: SerialConnectionListener): SerialConnection {
- return SerialConnectionImpl(usbManagerLazy, device, listener)
- }
+ fun createSerialConnection(device: UsbSerialDriver, listener: SerialConnectionListener): SerialConnection =
+ SerialConnectionImpl(usbManagerLazy, device, listener)
fun requestPermission(device: UsbDevice): Flow =
usbManagerLazy.get()?.requestPermission(application, device) ?: emptyFlow()
fun refreshState() {
- processLifecycle.coroutineScope.launch(dispatchers.default) {
- refreshStateInternal()
- }
+ processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
}
- private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
- _serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap())
- }
+ private suspend fun refreshStateInternal() =
+ withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap()) }
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/BootCompleteReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/BootCompleteReceiver.kt
index 2d977edf8..120a1f0d6 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BootCompleteReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BootCompleteReceiver.kt
@@ -20,10 +20,8 @@ package com.geeksville.mesh.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import com.geeksville.mesh.android.Logging
-
-class BootCompleteReceiver : BroadcastReceiver(), Logging {
+class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(mContext: Context, intent: Intent) {
// Verify the intent action
if (Intent.ACTION_BOOT_COMPLETED != intent.action) {
@@ -32,4 +30,4 @@ class BootCompleteReceiver : BroadcastReceiver(), Logging {
// start listening for bluetooth messages from our device
MeshService.startServiceLater(mContext)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index b94d4afa5..31f0f5436 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -40,6 +40,8 @@ import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.FromRadio.PayloadVariantCase
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
+import com.geeksville.mesh.MeshUtilApplication
+import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums
@@ -47,9 +49,6 @@ import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.TelemetryProtos.LocalStats
import com.geeksville.mesh.XmodemProtos
-import com.geeksville.mesh.analytics.DataPair
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.copy
@@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
@@ -121,9 +121,7 @@ import kotlin.math.absoluteValue
* infinite recursion on some androids (because contextWrapper.getResources calls to string
*/
@AndroidEntryPoint
-class MeshService :
- Service(),
- Logging {
+class MeshService : Service() {
@Inject lateinit var dispatchers: CoroutineDispatchers
@Inject lateinit var packetRepository: Lazy
@@ -156,7 +154,7 @@ class MeshService :
private val tracerouteStartTimes = ConcurrentHashMap()
- companion object : Logging {
+ companion object {
// Intents broadcast by MeshService
@@ -277,7 +275,7 @@ class MeshService :
private fun stopLocationRequests() {
if (locationFlow?.isActive == true) {
- info("Stopping location requests")
+ Timber.i("Stopping location requests")
locationFlow?.cancel()
locationFlow = null
}
@@ -311,7 +309,7 @@ class MeshService :
override fun onCreate() {
super.onCreate()
- info("Creating mesh service")
+ Timber.i("Creating mesh service")
serviceNotifications.initChannels()
// Switch to the IO thread
serviceScope.handledLaunch { radioInterfaceService.connect() }
@@ -368,7 +366,7 @@ class MeshService :
val a = radioInterfaceService.getBondedDeviceAddress()
val wantForeground = a != null && a != NO_DEVICE_SELECTED
- info("Requesting foreground service=$wantForeground")
+ Timber.i("Requesting foreground service=$wantForeground")
// We always start foreground because that's how our service is always started (if we didn't
// then android would
@@ -405,7 +403,7 @@ class MeshService :
}
override fun onDestroy() {
- info("Destroying mesh service")
+ Timber.i("Destroying mesh service")
// Make sure we aren't using the notification first
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
@@ -428,7 +426,7 @@ class MeshService :
/** discard entire node db & message state - used when downloading a new db from the device */
private fun discardNodeDB() {
- debug("Discarding NodeDB")
+ Timber.d("Discarding NodeDB")
myNodeInfo = null
nodeDBbyNodeNum.clear()
haveNodeDB = false
@@ -738,7 +736,7 @@ class MeshService :
// We ignore most messages that we sent
val fromUs = myInfo.myNodeNum == packet.from
- debug("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
+ Timber.d("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
dataPacket.status = MessageStatus.RECEIVED
@@ -751,19 +749,19 @@ class MeshService :
when (data.portnumValue) {
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
if (data.replyId != 0 && data.emoji == 0) {
- debug("Received REPLY from $fromId")
+ Timber.d("Received REPLY from $fromId")
rememberDataPacket(dataPacket)
} else if (data.replyId != 0 && data.emoji != 0) {
- debug("Received EMOJI from $fromId")
+ Timber.d("Received EMOJI from $fromId")
rememberReaction(packet)
} else {
- debug("Received CLEAR_TEXT from $fromId")
+ Timber.d("Received CLEAR_TEXT from $fromId")
rememberDataPacket(dataPacket)
}
}
Portnums.PortNum.ALERT_APP_VALUE -> {
- debug("Received ALERT_APP from $fromId")
+ Timber.d("Received ALERT_APP from $fromId")
rememberDataPacket(dataPacket)
}
@@ -776,9 +774,9 @@ class MeshService :
Portnums.PortNum.POSITION_APP_VALUE -> {
val u = MeshProtos.Position.parseFrom(data.payload)
- // debug("position_app ${packet.from} ${u.toOneLineString()}")
+ // Timber.d("position_app ${packet.from} ${u.toOneLineString()}")
if (data.wantResponse && u.latitudeI == 0 && u.longitudeI == 0) {
- debug("Ignoring nop position update from position request")
+ Timber.d("Ignoring nop position update from position request")
} else {
handleReceivedPosition(packet.from, u, dataPacket.time)
}
@@ -855,7 +853,7 @@ class MeshService :
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
- info("Traceroute $requestId complete in $seconds s")
+ Timber.i("Traceroute $requestId complete in $seconds s")
"$full\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
full
@@ -864,7 +862,7 @@ class MeshService :
}
}
- else -> debug("No custom processing needed for ${data.portnumValue}")
+ else -> Timber.d("No custom processing needed for ${data.portnumValue}")
}
// We always tell other apps when new data packets arrive
@@ -872,9 +870,9 @@ class MeshService :
serviceBroadcasts.broadcastReceivedData(dataPacket)
}
- GeeksvilleApplication.analytics.track("num_data_receive", DataPair(1))
+ MeshUtilApplication.analytics.track("num_data_receive", DataPair("num_data_receive", 1))
- GeeksvilleApplication.analytics.track(
+ MeshUtilApplication.analytics.track(
"data_receive",
DataPair("num_bytes", bytes.size),
DataPair("type", data.portnumValue),
@@ -888,7 +886,7 @@ class MeshService :
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
if (fromNodeNum == myNodeNum) {
val response = a.getConfigResponse
- debug("Admin: received config ${response.payloadVariantCase}")
+ Timber.d("Admin: received config ${response.payloadVariantCase}")
setLocalConfig(response)
}
}
@@ -898,7 +896,7 @@ class MeshService :
val mi = myNodeInfo
if (mi != null) {
val ch = a.getChannelResponse
- debug("Admin: Received channel ${ch.index}")
+ Timber.d("Admin: Received channel ${ch.index}")
if (ch.index + 1 < mi.maxChannels) {
handleChannel(ch)
@@ -908,15 +906,15 @@ class MeshService :
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
- debug("Admin: received DeviceMetadata from $fromNodeNum")
+ Timber.d("Admin: received DeviceMetadata from $fromNodeNum")
serviceScope.handledLaunch {
nodeRepository.insertMetadata(MetadataEntity(fromNodeNum, a.getDeviceMetadataResponse))
}
}
- else -> warn("No special processing needed for ${a.payloadVariantCase}")
+ else -> Timber.w("No special processing needed for ${a.payloadVariantCase}")
}
- debug("Admin: Received session_passkey from $fromNodeNum")
+ Timber.d("Admin: Received session_passkey from $fromNodeNum")
sessionPasskey = a.sessionPasskey
}
@@ -931,7 +929,7 @@ class MeshService :
p
} else {
p.copy {
- warn("Public key mismatch from $longName ($shortName)")
+ Timber.w("Public key mismatch from $longName ($shortName)")
publicKey = NodeEntity.ERROR_BYTE_STRING
}
}
@@ -962,10 +960,10 @@ class MeshService :
// (only)
// we don't record these nop position updates
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
- debug("Ignoring nop position update for the local node")
+ Timber.d("Ignoring nop position update for the local node")
} else {
updateNodeInfo(fromNum) {
- debug("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
+ Timber.d("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
it.setPosition(p, (defaultTime / 1000L).toInt())
}
}
@@ -1036,7 +1034,7 @@ class MeshService :
}
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForwardProtos.StoreAndForward) {
- debug("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
+ Timber.d("StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}")
when (s.variantCase) {
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
val u =
@@ -1093,7 +1091,7 @@ class MeshService :
)
onNodeDBChanged()
} else {
- warn("Ignoring early received packet: ${packet.toOneLineString()}")
+ Timber.w("Ignoring early received packet: ${packet.toOneLineString()}")
// earlyReceivedPackets.add(packet)
// logAssert(earlyReceivedPackets.size < 128) // The max should normally be about 32,
// but if the device is
@@ -1104,7 +1102,7 @@ class MeshService :
private fun sendNow(p: DataPacket) {
val packet = toMeshPacket(p)
p.time = System.currentTimeMillis() // update time to the actual time we started sending
- // debug("Sending to radio: ${packet.toPIIString()}")
+ // Timber.d("Sending to radio: ${packet.toPIIString()}")
packetHandler.sendToRadio(packet)
}
@@ -1115,7 +1113,7 @@ class MeshService :
sendNow(p)
sentPackets.add(p)
} catch (ex: Exception) {
- errormsg("Error sending queued message:", ex)
+ Timber.e("Error sending queued message:", ex)
}
}
offlineSentPackets.removeAll(sentPackets)
@@ -1150,7 +1148,7 @@ class MeshService :
// decided to pass through to us (except for broadcast packets)
// val toNum = packet.to
- // debug("Received: $packet")
+ // Timber.d("Received: $packet")
if (packet.hasDecoded()) {
val packetToSave =
MeshLog(
@@ -1232,19 +1230,12 @@ class MeshService :
/** Send in analytics about mesh connection */
private fun reportConnection() {
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
- GeeksvilleApplication.analytics.track(
+ MeshUtilApplication.analytics.track(
"mesh_connect",
DataPair("num_nodes", numNodes),
DataPair("num_online", numOnlineNodes),
radioModel,
)
-
- // Once someone connects to hardware start tracking the approximate number of nodes in their
- // mesh
- // this allows us to collect stats on what typical mesh size is and to tell difference
- // between users who just
- // downloaded the app, vs has connected it to some hardware.
- GeeksvilleApplication.analytics.setUserInfo(DataPair("num_nodes", numNodes), radioModel)
}
private var sleepTimeout: Job? = null
@@ -1254,7 +1245,7 @@ class MeshService :
// Called when we gain/lose connection to our radio
private fun onConnectionChanged(c: ConnectionState) {
- debug("onConnectionChanged: ${connectionStateHolder.getState()} -> $c")
+ Timber.d("onConnectionChanged: ${connectionStateHolder.getState()} -> $c")
// Perform all the steps needed once we start waiting for device sleep to complete
fun startDeviceSleep() {
@@ -1266,7 +1257,10 @@ class MeshService :
val now = System.currentTimeMillis()
connectTimeMsec = 0L
- GeeksvilleApplication.analytics.track("connected_seconds", DataPair((now - connectTimeMsec) / 1000.0))
+ MeshUtilApplication.analytics.track(
+ "connected_seconds",
+ DataPair("connected_seconds", (now - connectTimeMsec) / 1000.0),
+ )
}
// Have our timeout fire in the appropriate number of seconds
@@ -1277,12 +1271,12 @@ class MeshService :
// wait 30 seconds
val timeout = (localConfig.power?.lsSecs ?: 0) + 30
- debug("Waiting for sleeping device, timeout=$timeout secs")
+ Timber.d("Waiting for sleeping device, timeout=$timeout secs")
delay(timeout * 1000L)
- warn("Device timeout out, setting disconnected")
+ Timber.w("Device timeout out, setting disconnected")
onConnectionChanged(ConnectionState.DISCONNECTED)
} catch (ex: CancellationException) {
- debug("device sleep timeout cancelled")
+ Timber.d("device sleep timeout cancelled")
}
}
@@ -1295,12 +1289,12 @@ class MeshService :
stopLocationRequests()
stopMqttClientProxy()
- GeeksvilleApplication.analytics.track(
+ MeshUtilApplication.analytics.track(
"mesh_disconnect",
DataPair("num_nodes", numNodes),
DataPair("num_online", numOnlineNodes),
)
- GeeksvilleApplication.analytics.track("num_nodes", DataPair(numNodes))
+ MeshUtilApplication.analytics.track("num_nodes", DataPair("num_nodes", numNodes))
// broadcast an intent with our new connection state
serviceBroadcasts.broadcastConnection()
@@ -1312,12 +1306,12 @@ class MeshService :
connectTimeMsec = System.currentTimeMillis()
startConfig()
} catch (ex: InvalidProtocolBufferException) {
- errormsg("Invalid protocol buffer sent by device - update device software and try again", ex)
+ Timber.e("Invalid protocol buffer sent by device - update device software and try again", ex)
} catch (ex: RadioNotConnectedException) {
// note: no need to call startDeviceSleep(), because this exception could only have
// reached us if it was
// already called
- errormsg("Lost connection to radio during init - waiting for reconnect ${ex.message}")
+ Timber.e("Lost connection to radio during init - waiting for reconnect ${ex.message}")
} catch (ex: RemoteException) {
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms
// ish which
@@ -1437,7 +1431,7 @@ class MeshService :
// Explicitly handle default/unwanted cases to satisfy the exhaustive `when`
PayloadVariantCase.PAYLOADVARIANT_NOT_SET -> { proto ->
- errormsg("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
+ Timber.e("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
}
}
}
@@ -1452,7 +1446,7 @@ class MeshService :
val proto = MeshProtos.FromRadio.parseFrom(bytes)
proto.route()
} catch (ex: InvalidProtocolBufferException) {
- errormsg("Invalid Protobuf from radio, len=${bytes.size}", ex)
+ Timber.e("Invalid Protobuf from radio, len=${bytes.size}", ex)
}
}
@@ -1463,7 +1457,7 @@ class MeshService :
private val newNodes = mutableListOf()
private fun handleDeviceConfig(config: ConfigProtos.Config) {
- debug("Received config ${config.toOneLineString()}")
+ Timber.d("Received config ${config.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1479,7 +1473,7 @@ class MeshService :
}
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
- debug("Received moduleConfig ${config.toOneLineString()}")
+ Timber.d("Received moduleConfig ${config.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1495,7 +1489,7 @@ class MeshService :
}
private fun handleChannel(ch: ChannelProtos.Channel) {
- debug("Received channel ${ch.index}")
+ Timber.d("Received channel ${ch.index}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1553,7 +1547,7 @@ class MeshService :
}
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
- debug(
+ Timber.d(
"Received nodeinfo num=${info.num}," +
" hasUser=${info.hasUser()}," +
" hasPosition=${info.hasPosition()}," +
@@ -1616,10 +1610,7 @@ class MeshService :
val mi = myNodeInfo
if (myInfo != null && mi != null) {
// Track types of devices and firmware versions in use
- GeeksvilleApplication.analytics.setUserInfo(
- DataPair("firmware", mi.firmwareVersion),
- DataPair("hw_model", mi.model),
- )
+ analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown")
}
}
@@ -1647,7 +1638,7 @@ class MeshService :
/** Update our DeviceMetadata */
private fun handleMetadata(metadata: MeshProtos.DeviceMetadata) {
- debug("Received deviceMetadata ${metadata.toOneLineString()}")
+ Timber.d("Received deviceMetadata ${metadata.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1679,7 +1670,7 @@ class MeshService :
}
private fun handleClientNotification(notification: MeshProtos.ClientNotification) {
- debug("Received clientNotification ${notification.toOneLineString()}")
+ Timber.d("Received clientNotification ${notification.toOneLineString()}")
serviceRepository.setClientNotification(notification)
serviceNotifications.showClientNotification(notification)
// if the future for the originating request is still in the queue, complete as unsuccessful
@@ -1688,7 +1679,7 @@ class MeshService :
}
private fun handleFileInfo(fileInfo: MeshProtos.FileInfo) {
- debug("Received fileInfo ${fileInfo.toOneLineString()}")
+ Timber.d("Received fileInfo ${fileInfo.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1701,7 +1692,7 @@ class MeshService :
}
private fun handleLogReord(logRecord: MeshProtos.LogRecord) {
- debug("Received logRecord ${logRecord.toOneLineString()}")
+ Timber.d("Received logRecord ${logRecord.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1714,7 +1705,7 @@ class MeshService :
}
private fun handleRebooted(rebooted: Boolean) {
- debug("Received rebooted ${rebooted.toOneLineString()}")
+ Timber.d("Received rebooted ${rebooted.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1727,7 +1718,7 @@ class MeshService :
}
private fun handleXmodemPacket(xmodemPacket: XmodemProtos.XModem) {
- debug("Received XmodemPacket ${xmodemPacket.toOneLineString()}")
+ Timber.d("Received XmodemPacket ${xmodemPacket.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1740,7 +1731,7 @@ class MeshService :
}
private fun handleDeviceUiConfig(deviceuiConfig: DeviceUIProtos.DeviceUIConfig) {
- debug("Received deviceUIConfig ${deviceuiConfig.toOneLineString()}")
+ Timber.d("Received deviceUIConfig ${deviceuiConfig.toOneLineString()}")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1768,7 +1759,7 @@ class MeshService :
private fun stopMqttClientProxy() {
if (mqttMessageFlow?.isActive == true) {
- info("Stopping MqttClientProxy")
+ Timber.i("Stopping MqttClientProxy")
mqttMessageFlow?.cancel()
mqttMessageFlow = null
}
@@ -1785,13 +1776,13 @@ class MeshService :
private fun handleConfigComplete(configCompleteId: Int) {
if (configCompleteId == configNonce) {
- debug("Received config complete for config-only nonce $configNonce")
+ Timber.d("Received config complete for config-only nonce $configNonce")
handleConfigComplete()
}
}
private fun handleConfigComplete() {
- debug("Received config only complete for nonce $configNonce")
+ Timber.d("Received config only complete for nonce $configNonce")
val packetToSave =
MeshLog(
uuid = UUID.randomUUID().toString(),
@@ -1804,13 +1795,13 @@ class MeshService :
// This was our config request
if (newMyNodeInfo == null) {
- errormsg("Did not receive a valid config")
+ Timber.e("Did not receive a valid config")
} else {
myNodeInfo = newMyNodeInfo
}
// This was our config request
if (newNodes.isEmpty()) {
- errormsg("Did not receive a valid node info")
+ Timber.e("Did not receive a valid node info")
} else {
newNodes.forEach(::installNodeInfo)
newNodes.clear()
@@ -1829,7 +1820,7 @@ class MeshService :
newMyNodeInfo = null
newNodes.clear()
- debug("Starting config only nonce=$configNonce")
+ Timber.d("Starting config only nonce=$configNonce")
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = configNonce })
}
@@ -1840,7 +1831,7 @@ class MeshService :
val mi = myNodeInfo
if (mi != null) {
val idNum = destNum ?: mi.myNodeNum // when null we just send to the local node
- debug("Sending our position/time to=$idNum ${Position(position)}")
+ Timber.d("Sending our position/time to=$idNum ${Position(position)}")
// Also update our own map for our nodeNum, by handling the packet just like packets
// from other users
@@ -1865,7 +1856,7 @@ class MeshService :
)
}
} catch (ex: BLEException) {
- warn("Ignoring disconnected radio during gps location update")
+ Timber.w("Ignoring disconnected radio during gps location update")
}
}
@@ -1876,9 +1867,9 @@ class MeshService :
@Suppress("ComplexCondition")
if (user == old) {
- debug("Ignoring nop owner change")
+ Timber.d("Ignoring nop owner change")
} else {
- debug(
+ Timber.d(
"setOwner Id: $id longName: ${longName.anonymize}" +
" shortName: $shortName isLicensed: $isLicensed" +
" isUnmessagable: $isUnmessagable",
@@ -1942,10 +1933,10 @@ class MeshService :
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isFavorite) {
- debug("removing node ${node.num} from favorite list")
+ Timber.d("removing node ${node.num} from favorite list")
removeFavoriteNode = node.num
} else {
- debug("adding node ${node.num} to favorite list")
+ Timber.d("adding node ${node.num} to favorite list")
setFavoriteNode = node.num
}
},
@@ -1957,10 +1948,10 @@ class MeshService :
packetHandler.sendToRadio(
newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isIgnored) {
- debug("removing node ${node.num} from ignore list")
+ Timber.d("removing node ${node.num} from ignore list")
removeIgnoredNode = node.num
} else {
- debug("adding node ${node.num} to ignore list")
+ Timber.d("adding node ${node.num} to ignore list")
setIgnoredNode = node.num
}
},
@@ -1985,21 +1976,23 @@ class MeshService :
}
fun clearDatabases() = serviceScope.handledLaunch {
- debug("Clearing nodeDB")
+ Timber.d("Clearing nodeDB")
discardNodeDB()
nodeRepository.clearNodeDB()
}
private fun updateLastAddress(deviceAddr: String?) {
val currentAddr = meshPrefs.deviceAddress
- debug("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
+ Timber.d("setDeviceAddress: received request to change to: ${deviceAddr.anonymize}")
if (deviceAddr != currentAddr) {
- debug("SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}")
+ Timber.d(
+ "SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}",
+ )
meshPrefs.deviceAddress = deviceAddr
clearDatabases()
clearNotifications()
} else {
- debug("SetDeviceAddress: Device address is unchanged, ignoring.")
+ Timber.d("SetDeviceAddress: Device address is unchanged, ignoring.")
}
}
@@ -2011,7 +2004,7 @@ class MeshService :
object : IMeshService.Stub() {
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
- debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
+ Timber.d("Passing through device change to radio service: ${deviceAddr.anonymize}")
updateLastAddress(deviceAddr)
radioInterfaceService.setDeviceAddress(deviceAddr)
}
@@ -2063,7 +2056,7 @@ class MeshService :
if (p.id == 0) p.id = generatePacketId()
val bytes = p.bytes!!
- info(
+ Timber.i(
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes" +
" (connectionState=${connectionStateHolder.getState()})",
)
@@ -2083,7 +2076,7 @@ class MeshService :
try {
sendNow(p)
} catch (ex: Exception) {
- errormsg("Error sending message, so enqueueing", ex)
+ Timber.e("Error sending message, so enqueueing", ex)
enqueueForSending(p)
}
} else {
@@ -2094,13 +2087,11 @@ class MeshService :
// Keep a record of DataPackets, so GUIs can show proper chat history
rememberDataPacket(p, false)
- GeeksvilleApplication.analytics.track(
+ MeshUtilApplication.analytics.track(
"data_send",
DataPair("num_bytes", bytes.size),
DataPair("type", p.dataType),
)
-
- GeeksvilleApplication.analytics.track("num_data_sent", DataPair(1))
}
}
@@ -2114,7 +2105,7 @@ class MeshService :
}
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
- debug("Setting new radio config!")
+ Timber.d("Setting new radio config!")
val config = ConfigProtos.Config.parseFrom(payload)
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
if (num == myNodeNum) setLocalConfig(config) // Update our local copy
@@ -2134,7 +2125,7 @@ class MeshService :
/** Send our current module config to the device */
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
- debug("Setting new module config!")
+ Timber.d("Setting new module config!")
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
if (num == myNodeNum) setLocalModuleConfig(config) // Update our local copy
@@ -2203,14 +2194,14 @@ class MeshService :
override fun getNodes(): MutableList = toRemoteExceptions {
val r = nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
- info("in getOnline, count=${r.size}")
+ Timber.i("in getOnline, count=${r.size}")
// return arrayOf("+16508675309")
r
}
override fun connectionState(): String = toRemoteExceptions {
val r = connectionStateHolder.getState()
- info("in connectionState=$r")
+ Timber.i("in connectionState=$r")
r.toString()
}
@@ -2250,7 +2241,7 @@ class MeshService :
}
if (currentPosition == null) {
- debug("Position request skipped - no valid position available")
+ Timber.d("Position request skipped - no valid position available")
return@toRemoteExceptions
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
index 94fcca450..432e24d60 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
@@ -25,6 +25,7 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.service.ServiceRepository
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -49,7 +50,7 @@ constructor(
}
fun broadcastNodeChange(info: NodeInfo) {
- MeshService.debug("Broadcasting node change $info")
+ Timber.d("Broadcasting node change $info")
val intent = Intent(MeshService.ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
explicitBroadcast(intent)
}
@@ -58,10 +59,10 @@ constructor(
fun broadcastMessageStatus(id: Int, status: MessageStatus?) {
if (id == 0) {
- MeshService.debug("Ignoring anonymous packet status")
+ Timber.d("Ignoring anonymous packet status")
} else {
// Do not log, contains PII possibly
- // MeshService.debug("Broadcasting message status $p")
+ // MeshService.Timber.d("Broadcasting message status $p")
val intent =
Intent(MeshService.ACTION_MESSAGE_STATUS).apply {
putExtra(EXTRA_PACKET_ID, id)
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
index ce690b9de..1e280ba5e 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
@@ -26,6 +26,7 @@ import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.geeksville.mesh.BuildConfig
+import timber.log.Timber
import java.util.concurrent.TimeUnit
/** Helper that calls MeshService.startService() */
@@ -37,7 +38,7 @@ class ServiceStarter(appContext: Context, workerParams: WorkerParameters) : Work
// Indicate whether the task finished successfully with the Result
Result.success()
} catch (ex: Exception) {
- MeshService.errormsg("failure starting service, will retry", ex)
+ Timber.e("failure starting service, will retry", ex)
Result.retry()
}
}
@@ -48,7 +49,7 @@ class ServiceStarter(appContext: Context, workerParams: WorkerParameters) : Work
*/
fun MeshService.Companion.startServiceLater(context: Context) {
// No point in even starting the service if the user doesn't have a device bonded
- info("Received boot complete announcement, starting mesh service in two minutes")
+ Timber.i("Received boot complete announcement, starting mesh service in two minutes")
val delayRequest =
OneTimeWorkRequestBuilder()
.setInitialDelay(2, TimeUnit.MINUTES)
@@ -69,14 +70,14 @@ fun MeshService.Companion.startService(context: Context) {
// Before binding we want to explicitly create - so the service stays alive forever (so it can keep
// listening for the bluetooth packets arriving from the radio. And when they arrive forward them
// to Signal or whatever.
- info("Trying to start service debug=${BuildConfig.DEBUG}")
+ Timber.i("Trying to start service debug=${BuildConfig.DEBUG}")
val intent = createIntent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
context.startForegroundService(intent)
} catch (ex: ForegroundServiceStartNotAllowedException) {
- errormsg("Unable to start service: ${ex.message}")
+ Timber.e("Unable to start service: ${ex.message}")
}
} else {
context.startForegroundService(intent)
diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
index 7b3338601..4518a22d3 100644
--- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
@@ -20,9 +20,6 @@ package com.geeksville.mesh.service
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.BuildUtils.errormsg
-import com.geeksville.mesh.android.BuildUtils.info
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.repository.radio.RadioInterfaceService
@@ -41,6 +38,7 @@ import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ConnectionState
+import timber.log.Timber
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
@@ -71,7 +69,7 @@ constructor(
*/
fun sendToRadio(p: ToRadio.Builder) {
val built = p.build()
- debug("Sending to radio ${built.toPIIString()}")
+ Timber.d("Sending to radio ${built.toPIIString()}")
val b = built.toByteArray()
radioInterfaceService.sendToRadio(b)
@@ -103,7 +101,7 @@ constructor(
fun stopPacketQueue() {
if (queueJob?.isActive == true) {
- info("Stopping packet queueJob")
+ Timber.i("Stopping packet queueJob")
queueJob?.cancel()
queueJob = null
queuedPackets.clear()
@@ -113,7 +111,7 @@ constructor(
}
fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
- debug("queueStatus ${queueStatus.toOneLineString()}")
+ Timber.d("queueStatus ${queueStatus.toOneLineString()}")
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, meshPacketId) }
if (success && isFull) return // Queue is full, wait for free != 0
if (requestId != 0) {
@@ -132,20 +130,20 @@ constructor(
if (queueJob?.isActive == true) return
queueJob =
scope.handledLaunch {
- debug("packet queueJob started")
+ Timber.d("packet queueJob started")
while (connectionStateHolder.getState() == ConnectionState.CONNECTED) {
// take the first packet from the queue head
val packet = queuedPackets.poll() ?: break
try {
// send packet to the radio and wait for response
val response = sendPacket(packet)
- debug("queueJob packet id=${packet.id.toUInt()} waiting")
+ Timber.d("queueJob packet id=${packet.id.toUInt()} waiting")
val success = response.get(2, TimeUnit.MINUTES)
- debug("queueJob packet id=${packet.id.toUInt()} success $success")
+ Timber.d("queueJob packet id=${packet.id.toUInt()} success $success")
} catch (e: TimeoutException) {
- debug("queueJob packet id=${packet.id.toUInt()} timeout")
+ Timber.d("queueJob packet id=${packet.id.toUInt()} timeout")
} catch (e: Exception) {
- debug("queueJob packet id=${packet.id.toUInt()} failed")
+ Timber.d("queueJob packet id=${packet.id.toUInt()} failed")
}
}
}
@@ -182,7 +180,7 @@ constructor(
if (connectionStateHolder.getState() != ConnectionState.CONNECTED) throw RadioNotConnectedException()
sendToRadio(ToRadio.newBuilder().apply { this.packet = packet })
} catch (ex: Exception) {
- errormsg("sendToRadio error:", ex)
+ Timber.e("sendToRadio error:", ex)
future.complete(false)
}
return future
diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
index 969d645d0..5bf6cd962 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
@@ -31,11 +31,11 @@ import android.os.Build
import android.os.DeadObjectException
import android.os.Handler
import android.os.Looper
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
+import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.concurrent.CallbackContinuation
import com.geeksville.mesh.concurrent.Continuation
import com.geeksville.mesh.concurrent.SyncContinuation
+import com.geeksville.mesh.logAssert
import com.geeksville.mesh.util.exceptionReporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -43,6 +43,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import timber.log.Timber
import java.io.Closeable
import java.util.Random
import java.util.UUID
@@ -62,9 +63,7 @@ fun longBLEUUID(hexFour: String): UUID = UUID.fromString("0000$hexFour-0000-1000
*
* This class fixes the API by using coroutines to let you safely do a series of BTLE operations.
*/
-class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) :
- Logging,
- Closeable {
+class SafeBluetooth(private val context: Context, private val device: BluetoothDevice) : Closeable {
// / Timeout before we declare a bluetooth operation failed (used for synchronous API operations only)
var timeoutMsec = 20 * 1000L
@@ -102,11 +101,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val completion: com.geeksville.mesh.concurrent.Continuation<*>,
val timeoutMillis: Long = 0, // If we want to timeout this operation at a certain time, use a non zero value
private val startWorkFn: () -> Boolean,
- ) : Logging {
+ ) {
// / Start running a queued bit of work, return true for success or false for fatal bluetooth error
fun startWork(): Boolean {
- debug("Starting work: $tag")
+ Timber.d("Starting work: $tag")
return startWorkFn()
}
@@ -123,8 +122,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
private val mHandler: Handler = Handler(Looper.getMainLooper())
fun restartBle() {
- GeeksvilleApplication.analytics.track("ble_restart") // record # of times we needed to use this nasty hack
- errormsg("Doing emergency BLE restart")
+ analytics.track("ble_restart") // record # of times we needed to use this nasty hack
+ Timber.w("Doing emergency BLE restart")
context.bluetoothManager?.adapter?.let { adp ->
if (adp.isEnabled) {
adp.disable()
@@ -168,7 +167,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) = exceptionReporter {
- info("new bluetooth connection state $newState, status $status")
+ Timber.i("new bluetooth connection state $newState, status $status")
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
@@ -177,7 +176,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// If autoconnect is on and this connect attempt failed, hopefully some future attempt will
// succeed
if (status != BluetoothGatt.GATT_SUCCESS && autoConnect) {
- errormsg("Connect attempt failed $status, not calling connect completion handler...")
+ Timber.e("Connect attempt failed $status, not calling connect completion handler...")
} else {
completeWork(status, Unit)
}
@@ -185,9 +184,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
BluetoothProfile.STATE_DISCONNECTED -> {
if (gatt == null) {
- errormsg("No gatt: ignoring connection state $newState, status $status")
+ Timber.e("No gatt: ignoring connection state $newState, status $status")
} else if (isClosing) {
- info("Got disconnect because we are shutting down, closing gatt")
+ Timber.i("Got disconnect because we are shutting down, closing gatt")
gatt = null
g.close() // Finish closing our gatt here
} else {
@@ -195,7 +194,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val oldstate = state
state = newState
if (oldstate == BluetoothProfile.STATE_CONNECTED) {
- info("Lost connection - aborting current work: $currentWork")
+ Timber.i("Lost connection - aborting current work: $currentWork")
// If we get a disconnect, just try again otherwise fail all current operations
// Note: if no work is pending (likely) we also just totally teardown and restart the
@@ -218,12 +217,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// you will get a callback with status=133. Then call BluetoothGatt#connect()
// to initiate a background connection.
if (autoConnect) {
- warn("Failed on non-auto connect, falling back to auto connect attempt")
+ Timber.w("Failed on non-auto connect, falling back to auto connect attempt")
closeGatt() // Close the old non-auto connection
lowLevelConnect(true)
}
} else if (status == 147) {
- info("got 147, calling lostConnection()")
+ Timber.i("got 147, calling lostConnection()")
lostConnection("code 147")
}
@@ -261,7 +260,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val reliable = currentReliableWrite
if (reliable != null) {
if (!characteristic.value.contentEquals(reliable)) {
- errormsg("A reliable write failed!")
+ Timber.e("A reliable write failed!")
gatt.abortReliableWrite()
completeWork(STATUS_RELIABLE_WRITE_FAILED, characteristic) // skanky code to indicate failure
} else {
@@ -278,7 +277,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
// Alas, passing back an Int mtu isn't working and since I don't really care what MTU
// the device was willing to let us have I'm just punting and returning Unit
- if (isSettingMtu) completeWork(status, Unit) else errormsg("Ignoring bogus onMtuChanged")
+ if (isSettingMtu) completeWork(status, Unit) else Timber.e("Ignoring bogus onMtuChanged")
}
/**
@@ -290,7 +289,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val handler = notifyHandlers.get(characteristic.uuid)
if (handler == null) {
- warn("Received notification from $characteristic, but no handler registered")
+ Timber.w("Received notification from $characteristic, but no handler registered")
} else {
exceptionReporter { handler(characteristic) }
}
@@ -344,9 +343,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
if (newWork.timeoutMillis != 0L) {
activeTimeout =
serviceScope.launch {
- // debug("Starting failsafe timer ${newWork.timeoutMillis}")
+ // Timber.d("Starting failsafe timer ${newWork.timeoutMillis}")
delay(newWork.timeoutMillis)
- errormsg("Failsafe BLE timer expired!")
+ Timber.e("Failsafe BLE timer expired!")
completeWork(STATUS_TIMEOUT, Unit) // Throw an exception in that work
}
}
@@ -356,12 +355,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val failThis = simFailures && !newWork.isConnect() && failRandom.nextInt(100) < failPercent
if (failThis) {
- errormsg("Simulating random work failure!")
+ Timber.e("Simulating random work failure!")
completeWork(STATUS_SIMFAILURE, Unit)
} else {
val started = newWork.startWork()
if (!started) {
- errormsg("Failed to start work, returned error status")
+ Timber.e("Failed to start work, returned error status")
completeWork(STATUS_NOSTART, Unit) // abandon the current attempt and try for another
}
}
@@ -372,7 +371,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
val btCont = BluetoothContinuation(tag, cont, timeout, initFn)
synchronized(workQueue) {
- debug("Enqueuing work: ${btCont.tag}")
+ Timber.d("Enqueuing work: ${btCont.tag}")
workQueue.add(btCont)
// if we don't have any outstanding operations, run first item in queue
@@ -409,9 +408,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
}
if (work == null) {
- warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
+ Timber.w("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
} else {
- // debug("work ${work.tag} is completed, resuming status=$status, res=$res")
+ // Timber.d("work ${work.tag} is completed, resuming status=$status, res=$res")
if (status != 0) {
work.completion.resumeWithException(
BLEStatusException(status, "Bluetooth status=$status while doing ${work.tag}"),
@@ -426,12 +425,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
/** Something went wrong, abort all queued */
private fun failAllWork(ex: Exception) {
synchronized(workQueue) {
- warn("Failing ${workQueue.size} works, because ${ex.message}")
+ Timber.w("Failing ${workQueue.size} works, because ${ex.message}")
workQueue.forEach {
try {
it.completion.resumeWithException(ex)
} catch (ex: Exception) {
- errormsg("Mystery exception, why were we informed about our own exceptions?", ex)
+ Timber.e("Mystery exception, why were we informed about our own exceptions?", ex)
}
}
workQueue.clear()
@@ -531,7 +530,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
notifyHandlers.clear()
lostConnectCallback?.let {
- debug("calling lostConnect handler")
+ Timber.d("calling lostConnect handler")
it.invoke()
}
}
@@ -543,7 +542,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// Queue a new connection attempt
val cb = connectionCallback
if (cb != null) {
- debug("queuing a reconnection callback")
+ Timber.d("queuing a reconnection callback")
assert(currentWork == null)
if (
@@ -557,7 +556,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// need)
queueWork("reconnect", CallbackContinuation(cb), 0) { true }
} else {
- debug("No connectionCallback registered")
+ Timber.d("No connectionCallback registered")
}
}
@@ -691,7 +690,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
/** Close just the GATT device but keep our pending callbacks active */
fun closeGatt() {
gatt?.let { g ->
- info("Closing our GATT connection")
+ Timber.i("Closing our GATT connection")
isClosing = true
try {
g.disconnect()
@@ -704,7 +703,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
}
gatt?.let { g2 ->
- warn("Android onConnectionStateChange did not run, manually closing")
+ Timber.w("Android onConnectionStateChange did not run, manually closing")
gatt = null // clear gat before calling close, bcause close might throw dead object exception
g2.close()
}
@@ -712,9 +711,9 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// Attempt to invoke virtual method 'com.android.bluetooth.gatt.AdvertiseClient
// com.android.bluetooth.gatt.AdvertiseManager.getAdvertiseClient(int)' on a null object reference
// com.geeksville.mesh.service.SafeBluetooth.closeGatt
- warn("Ignoring NPE in close - probably buggy Samsung BLE")
+ Timber.w("Ignoring NPE in close - probably buggy Samsung BLE")
} catch (ex: DeadObjectException) {
- warn("Ignoring dead object exception, probably bluetooth was just disabled")
+ Timber.w("Ignoring dead object exception, probably bluetooth was just disabled")
} finally {
isClosing = false
}
@@ -748,7 +747,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
// / asyncronously turn notification on/off for a characteristic
fun setNotify(c: BluetoothGattCharacteristic, enable: Boolean, onChanged: (BluetoothGattCharacteristic) -> Unit) {
- debug("starting setNotify(${c.uuid}, $enable)")
+ Timber.d("starting setNotify(${c.uuid}, $enable)")
notifyHandlers[c.uuid] = onChanged
// c.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt!!.setCharacteristicNotification(c, enable)
@@ -765,6 +764,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
} else {
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
}
- asyncWriteDescriptor(descriptor) { debug("Notify enable=$enable completed") }
+ asyncWriteDescriptor(descriptor) { Timber.d("Notify enable=$enable completed") }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 92ccbf6b8..24df1c606 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -76,9 +76,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.android.AddNavigationTracking
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.setAttributes
+import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.channelsGraph
@@ -118,6 +116,7 @@ import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
+import timber.log.Timber
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
Conversations(R.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph),
@@ -150,14 +149,13 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
}
}
- AddNavigationTracking(navController)
-
if (connectionState == ConnectionState.CONNECTED) {
requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) }
}
- VersionChecks(uIViewModel)
+ analytics.addNavigationTrackingEffect(navController = navController)
+ VersionChecks(uIViewModel)
val alertDialogState by uIViewModel.currentAlert.collectAsStateWithLifecycle()
alertDialogState?.let { state ->
if (state.choices.isNotEmpty()) {
@@ -230,8 +228,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
val receiveColor = capturedColorScheme.StatusBlue
LaunchedEffect(uIViewModel.meshActivity, capturedColorScheme) {
uIViewModel.meshActivity.collectLatest { activity ->
- debug("MeshActivity Event: $activity, Current Alpha: ${animatedGlowAlpha.value}")
-
val newTargetColor =
when (activity) {
is MeshActivity.Send -> sendColor
@@ -416,16 +412,12 @@ private fun VersionChecks(viewModel: UIViewModel) {
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
- val currentFirmwareVersion by viewModel.firmwareVersion.collectAsStateWithLifecycle(null)
-
- val currentDeviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle(null)
-
val latestStableFirmwareRelease by
viewModel.latestStableFirmwareRelease.collectAsStateWithLifecycle(DeviceVersion("2.6.4"))
LaunchedEffect(connectionState, firmwareEdition) {
if (connectionState == ConnectionState.CONNECTED) {
firmwareEdition?.let { edition ->
- debug("FirmwareEdition: ${edition.name}")
+ Timber.d("FirmwareEdition: ${edition.name}")
when (edition) {
MeshProtos.FirmwareEdition.VANILLA -> {
// Handle any specific logic for VANILLA firmware edition if needed
@@ -439,14 +431,6 @@ private fun VersionChecks(viewModel: UIViewModel) {
}
}
- LaunchedEffect(connectionState, currentFirmwareVersion, currentDeviceHardware) {
- if (connectionState == ConnectionState.CONNECTED) {
- if (currentDeviceHardware != null && currentFirmwareVersion != null) {
- setAttributes(currentFirmwareVersion!!, currentDeviceHardware!!)
- }
- }
- }
-
// Check if the device is running an old app version or firmware version
LaunchedEffect(connectionState, myNodeInfo) {
if (connectionState == ConnectionState.CONNECTED) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
index afede99bc..4b6bbda04 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt
@@ -77,7 +77,6 @@ import androidx.compose.ui.unit.sp
import androidx.datastore.core.IOException
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import kotlinx.collections.immutable.toImmutableList
@@ -89,6 +88,7 @@ import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme
+import timber.log.Timber
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
@@ -394,7 +394,7 @@ private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: L
)
.show()
}
- warn("Error:IOException: " + e.toString())
+ Timber.w(e, "Error:IOException ")
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
index 39e10bc54..e13fd660d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
@@ -853,19 +853,19 @@ private fun MainNodeDetails(node: Node, ourNode: Node?, displayUnits: ConfigProt
icon = Icons.Default.History,
trailingText = formatAgo(node.lastHeard),
)
- val distance = ourNode?.distance(node)?.toDistanceString(displayUnits)
- if (node != ourNode && distance != null) {
+ val distance = ourNode?.distance(node)?.takeIf { it > 0 }?.toDistanceString(displayUnits)
+ if (distance != null && distance.isNotEmpty()) {
SettingsItemDetail(
text = stringResource(R.string.node_sort_distance),
icon = Icons.Default.SocialDistance,
trailingText = distance,
)
- SettingsItemDetail(
- text = stringResource(R.string.last_position_update),
- icon = Icons.Default.LocationOn,
- trailingText = formatAgo(node.position.time),
- )
}
+ SettingsItemDetail(
+ text = stringResource(R.string.last_position_update),
+ icon = Icons.Default.LocationOn,
+ trailingText = formatAgo(node.position.time),
+ )
}
@Composable
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/LinkedCoordinates.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/LinkedCoordinates.kt
index fd88b65e2..9daff5a98 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/components/LinkedCoordinates.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/LinkedCoordinates.kt
@@ -40,11 +40,11 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
-import com.geeksville.mesh.android.BuildUtils.debug
import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.HyperlinkBlue
+import timber.log.Timber
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@@ -69,7 +69,7 @@ fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude
onLongClick = {
coroutineScope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", annotatedString)))
- debug("Copied to clipboard")
+ Timber.d("Copied to clipboard")
}
},
),
@@ -106,7 +106,7 @@ private fun handleClick(context: Context, annotatedString: AnnotatedString) {
Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
}
} catch (ex: ActivityNotFoundException) {
- debug("Failed to open geo intent: $ex")
+ Timber.d("Failed to open geo intent: $ex")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
index 011780fcb..22316c9b8 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt
@@ -232,11 +232,12 @@ fun SettingsScreen(
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
+ val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
- checked = state.analyticsEnabled,
+ checked = allowed,
leadingIcon = Icons.Default.BugReport,
- onClick = { viewModel.toggleAnalytics() },
+ onClick = { viewModel.toggleAnalyticsAllowed() },
)
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
index 46cb9dcf0..78f3791d6 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
@@ -24,7 +24,6 @@ import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
-import com.geeksville.mesh.android.Logging
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -50,6 +49,7 @@ import org.meshtastic.core.model.util.positionToMeter
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
+import timber.log.Timber
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
@@ -70,8 +70,7 @@ constructor(
private val meshLogRepository: MeshLogRepository,
private val uiPrefs: UiPrefs,
private val uiPreferencesDataSource: UiPreferencesDataSource,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
val myNodeInfo: StateFlow = nodeRepository.myNodeInfo
val myNodeNum
@@ -254,7 +253,7 @@ constructor(
}
}
} catch (ex: FileNotFoundException) {
- errormsg("Can't write file error: ${ex.message}")
+ Timber.e("Can't write file error: ${ex.message}")
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
index f60fff56c..9d3ac67fc 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
@@ -40,9 +40,6 @@ import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.Portnums
-import com.geeksville.mesh.android.GeeksvilleApplication
-import com.geeksville.mesh.android.Logging
-import com.geeksville.mesh.android.isAnalyticsAvailable
import com.geeksville.mesh.config
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.getChannelList
@@ -80,6 +77,7 @@ import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
+import timber.log.Timber
import java.io.FileOutputStream
import javax.inject.Inject
@@ -113,11 +111,16 @@ constructor(
private val locationRepository: LocationRepository,
private val mapConsentPrefs: MapConsentPrefs,
private val analyticsPrefs: AnalyticsPrefs,
-) : ViewModel(),
- Logging {
+) : ViewModel() {
private val meshService: IMeshService?
get() = serviceRepository.meshService
+ var analyticsAllowedFlow = analyticsPrefs.getAnalyticsAllowedChangesFlow()
+
+ fun toggleAnalyticsAllowed() {
+ analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
+ }
+
private val destNum = savedStateHandle.toRoute().destNum
private val _destNode = MutableStateFlow(null)
val destNode: StateFlow
@@ -169,9 +172,7 @@ constructor(
}
.launchIn(viewModelScope)
- _radioConfigState.update { it.copy(analyticsAvailable = (app as GeeksvilleApplication).isAnalyticsAvailable) }
-
- debug("RadioConfigViewModel created")
+ Timber.d("RadioConfigViewModel created")
}
private val myNodeInfo: StateFlow
@@ -197,7 +198,7 @@ constructor(
override fun onCleared() {
super.onCleared()
- debug("RadioConfigViewModel cleared")
+ Timber.d("RadioConfigViewModel cleared")
}
private fun request(destNum: Int, requestAction: suspend (IMeshService, Int, Int) -> Unit, errorMessage: String) =
@@ -219,7 +220,7 @@ constructor(
}
}
} catch (ex: RemoteException) {
- errormsg("$errorMessage: ${ex.message}")
+ Timber.e("$errorMessage: ${ex.message}")
}
}
}
@@ -387,7 +388,7 @@ constructor(
try {
meshService?.setFixedPosition(destNum, position)
} catch (ex: RemoteException) {
- errormsg("Set fixed position error: ${ex.message}")
+ Timber.e("Set fixed position error: ${ex.message}")
}
}
@@ -401,7 +402,7 @@ constructor(
onResult(protobuf)
}
} catch (ex: Exception) {
- errormsg("Import DeviceProfile error: ${ex.message}")
+ Timber.e("Import DeviceProfile error: ${ex.message}")
sendError(ex.customMessage)
}
}
@@ -417,7 +418,7 @@ constructor(
}
setResponseStateSuccess()
} catch (ex: Exception) {
- errormsg("Can't write file error: ${ex.message}")
+ Timber.e("Can't write file error: ${ex.message}")
sendError(ex.customMessage)
}
}
@@ -456,7 +457,7 @@ constructor(
setResponseStateSuccess()
} catch (ex: Exception) {
val errorMessage = "Can't write security keys JSON error: ${ex.message}"
- errormsg(errorMessage)
+ Timber.e(errorMessage)
sendError(ex.customMessage)
}
}
@@ -479,7 +480,7 @@ constructor(
try {
setChannels(channelUrl)
} catch (ex: Exception) {
- errormsg("DeviceProfile channel import error", ex)
+ Timber.e(ex, "DeviceProfile channel import error")
sendError(ex.customMessage)
}
}
@@ -617,7 +618,7 @@ constructor(
if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) {
val parsed = MeshProtos.Routing.parseFrom(data.payload)
- debug(debugMsg.format(parsed.errorReason.name))
+ Timber.d(debugMsg.format(parsed.errorReason.name))
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
sendError(getStringResFrom(parsed.errorReasonValue))
} else if (packet.from == destNum && route.isEmpty()) {
@@ -631,7 +632,7 @@ constructor(
}
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
- debug(debugMsg.format(parsed.payloadVariantCase.name))
+ Timber.d(debugMsg.format(parsed.payloadVariantCase.name))
if (destNum != packet.from) {
sendError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
return
@@ -698,7 +699,7 @@ constructor(
incrementCompleted()
}
- else -> debug("No custom processing needed for ${parsed.payloadVariantCase}")
+ else -> Timber.d("No custom processing needed for ${parsed.payloadVariantCase}")
}
if (AdminRoute.entries.any { it.name == route }) {
@@ -707,9 +708,4 @@ constructor(
requestIds.update { it.apply { remove(data.requestId) } }
}
}
-
- fun toggleAnalytics() {
- analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
- _radioConfigState.update { it.copy(analyticsEnabled = analyticsPrefs.analyticsAllowed) }
- }
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index 29e3f553b..5bcb40402 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -89,10 +89,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigProtos
-import com.geeksville.mesh.analytics.DataPair
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.BuildUtils.errormsg
-import com.geeksville.mesh.android.GeeksvilleApplication
+import com.geeksville.mesh.MeshUtilApplication.Companion.analytics
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
@@ -107,6 +104,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import kotlinx.coroutines.launch
+import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.qrCode
@@ -116,6 +114,7 @@ import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane
import org.meshtastic.core.ui.component.PreferenceFooter
+import timber.log.Timber
/**
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
@@ -184,7 +183,7 @@ fun ChannelScreen(
}
fun zxingScan() {
- debug("Starting zxing QR code scanner")
+ Timber.d("Starting zxing QR code scanner")
val zxingScan = ScanOptions()
zxingScan.setCameraId(0)
zxingScan.setPrompt("")
@@ -211,7 +210,7 @@ fun ChannelScreen(
viewModel.setChannels(newChannelSet)
// Since we are writing to DeviceConfig, that will trigger the rest of the GUI update (QR code etc)
} catch (ex: RemoteException) {
- errormsg("ignoring channel problem", ex)
+ Timber.e("ignoring channel problem", ex)
channelSet = channels // Throw away user edits
@@ -239,7 +238,7 @@ fun ChannelScreen(
confirmButton = {
TextButton(
onClick = {
- debug("Switching back to default channel")
+ Timber.d("Switching back to default channel")
installSettings(
Channel.default.settings,
Channel.default.loraConfig.copy {
@@ -383,7 +382,7 @@ private fun EditChannelUrl(enabled: Boolean, channelUrl: Uri, modifier: Modifier
else -> {
// track how many times users share channels
- GeeksvilleApplication.analytics.track("share", DataPair("content_type", "channel"))
+ analytics.track("share", DataPair("content_type", "channel"))
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(ClipData.newPlainText(label, valueState.toString())),
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
index e89ad7304..8d9c8eae0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt
@@ -48,8 +48,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.android.BuildUtils.errormsg
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -94,7 +92,7 @@ fun AddContactFAB(
try {
uri.toSharedContact()
} catch (ex: MalformedURLException) {
- errormsg("URL was malformed: ${ex.message}")
+ Timber.e("URL was malformed: ${ex.message}")
null
}
if (sharedContact != null) {
@@ -136,7 +134,7 @@ fun AddContactFAB(
}
fun zxingScan() {
- debug("Starting zxing QR code scanner")
+ Timber.d("Starting zxing QR code scanner")
val zxingScan = ScanOptions()
zxingScan.setCameraId(CAMERA_ID)
zxingScan.setPrompt("")
@@ -229,7 +227,7 @@ val Uri.qrCode: Bitmap?
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: WriterException) {
- errormsg("URL was too complex to render as barcode: ${ex.message}")
+ Timber.e("URL was too complex to render as barcode: ${ex.message}")
null
}
diff --git a/app/src/main/java/com/geeksville/mesh/util/Exceptions.kt b/app/src/main/java/com/geeksville/mesh/util/Exceptions.kt
index 92514016f..108a07cf9 100644
--- a/app/src/main/java/com/geeksville/mesh/util/Exceptions.kt
+++ b/app/src/main/java/com/geeksville/mesh/util/Exceptions.kt
@@ -19,13 +19,10 @@ package com.geeksville.mesh.util
import android.os.RemoteException
import android.util.Log
-import android.view.View
-import com.geeksville.mesh.android.Logging
-import com.google.android.material.snackbar.Snackbar
+import timber.log.Timber
-
-object Exceptions : Logging {
- /// Set in Application.onCreate
+object Exceptions {
+ // / Set in Application.onCreate
var reporter: ((Throwable, String?, String?) -> Unit)? = null
/**
@@ -34,19 +31,17 @@ object Exceptions : Logging {
* After reporting return
*/
fun report(exception: Throwable, tag: String? = null, message: String? = null) {
- errormsg(
+ Timber.e(
+ exception,
"Exceptions.report: $tag $message",
- exception
) // print the message to the log _before_ telling the crash reporter
- reporter?.let { r ->
- r(exception, tag, message)
- }
+ reporter?.let { r -> r(exception, tag, message) }
}
}
/**
- * This wraps (and discards) exceptions, but first it reports them to our bug tracking system and prints
- * a message to the log.
+ * This wraps (and discards) exceptions, but first it reports them to our bug tracking system and prints a message to
+ * the log.
*/
fun exceptionReporter(inner: () -> Unit) {
try {
@@ -57,40 +52,24 @@ fun exceptionReporter(inner: () -> Unit) {
}
}
-/**
- * If an exception occurs, show the message in a snackbar and continue
- */
-fun exceptionToSnackbar(view: View, inner: () -> Unit) {
- try {
- inner()
- } catch (ex: Throwable) {
- Snackbar.make(view, ex.message ?: "An exception occurred", Snackbar.LENGTH_LONG).show()
- }
-}
-
-
-/**
- * This wraps (and discards) exceptions, but it does output a log message
- */
+/** This wraps (and discards) exceptions, but it does output a log message */
fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
try {
inner()
} catch (ex: Throwable) {
// DO NOT THROW users expect we have fully handled/discarded the exception
- if(!silent)
- Exceptions.errormsg("ignoring exception", ex)
+ if (!silent) Timber.e("ignoring exception", ex)
}
}
-/// Convert any exceptions in this service call into a RemoteException that the client can
-/// then handle
+// / Convert any exceptions in this service call into a RemoteException that the client can
+// / then handle
fun toRemoteExceptions(inner: () -> T): T = try {
inner()
} catch (ex: Throwable) {
Log.e("toRemoteExceptions", "Uncaught exception, returning to remote client", ex)
- when(ex) { // don't double wrap remote exceptions
+ when (ex) { // don't double wrap remote exceptions
is RemoteException -> throw ex
else -> throw RemoteException(ex.message)
}
}
-
diff --git a/app/src/main/java/com/geeksville/mesh/util/HardwareModelExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/HardwareModelExtensions.kt
index be66eaaf2..f93be1581 100644
--- a/app/src/main/java/com/geeksville/mesh/util/HardwareModelExtensions.kt
+++ b/app/src/main/java/com/geeksville/mesh/util/HardwareModelExtensions.kt
@@ -18,7 +18,7 @@
package com.geeksville.mesh.util
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.android.BuildUtils.warn
+import timber.log.Timber
/**
* Safely extracts the hardware model number from a HardwareModel enum.
@@ -34,7 +34,7 @@ import com.geeksville.mesh.android.BuildUtils.warn
fun MeshProtos.HardwareModel.safeNumber(fallbackValue: Int = -1): Int = try {
this.number
} catch (e: IllegalArgumentException) {
- warn("Unknown hardware model enum value: $this, using fallback value: $fallbackValue")
+ Timber.w("Unknown hardware model enum value: $this, using fallback value: $fallbackValue")
fallbackValue
}
diff --git a/app/src/main/java/com/geeksville/mesh/util/LanguageUtils.kt b/app/src/main/java/com/geeksville/mesh/util/LanguageUtils.kt
index 1af60d741..98919d104 100644
--- a/app/src/main/java/com/geeksville/mesh/util/LanguageUtils.kt
+++ b/app/src/main/java/com/geeksville/mesh/util/LanguageUtils.kt
@@ -20,12 +20,12 @@ package com.geeksville.mesh.util
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
-import com.geeksville.mesh.android.Logging
import org.meshtastic.core.strings.R
import org.xmlpull.v1.XmlPullParser
+import timber.log.Timber
import java.util.Locale
-object LanguageUtils : Logging {
+object LanguageUtils {
const val SYSTEM_DEFAULT = "zz"
@@ -57,7 +57,7 @@ object LanguageUtils : Logging {
}
}
} catch (e: Exception) {
- errormsg("Error parsing locale_config.xml: ${e.message}")
+ Timber.e("Error parsing locale_config.xml: ${e.message}")
}
}
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 1a9fe8c44..84daa30c6 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -39,16 +39,21 @@ kotlin {
}
dependencies {
- compileOnly(libs.android.gradleApiPlugin)
- compileOnly(libs.android.tools.common)
- compileOnly(libs.compose.gradlePlugin)
- compileOnly(libs.detekt.gradle)
- compileOnly(libs.firebase.crashlytics.gradlePlugin)
- compileOnly(libs.firebase.performance.gradlePlugin)
- compileOnly(libs.kotlin.gradlePlugin)
- compileOnly(libs.ksp.gradlePlugin)
- compileOnly(libs.room.gradlePlugin)
- compileOnly(libs.spotless.gradlePlugin)
+ implementation(libs.android.gradleApiPlugin)
+ implementation(libs.serialization.gradlePlugin)
+ implementation(libs.android.tools.common)
+ implementation(libs.compose.gradlePlugin)
+ implementation(libs.datadog.gradlePlugin)
+ implementation(libs.detekt.gradlePlugin)
+ implementation(libs.firebase.crashlytics.gradlePlugin)
+ implementation(libs.firebase.performance.gradlePlugin)
+ implementation(libs.google.services.gradlePlugin)
+ implementation(libs.hilt.gradlePlugin)
+ implementation(libs.kotlin.gradlePlugin)
+ implementation(libs.ksp.gradlePlugin)
+ implementation(libs.room.gradlePlugin)
+ implementation(libs.secrets.gradlePlugin)
+ implementation(libs.spotless.gradlePlugin)
implementation(libs.truth)
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
index b1a4471de..cbccc4b2d 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -17,7 +17,6 @@
import com.android.build.api.dsl.ApplicationExtension
import com.geeksville.mesh.buildlogic.libs
-import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
@@ -36,12 +35,8 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin {
val bom = libs.findLibrary("firebase-bom").get()
"googleImplementation"(platform(bom))
"googleImplementation"(libs.findBundle("firebase").get()) {
- /*
- Exclusion of protobuf / protolite dependencies is necessary as the
- datastore-proto brings in protobuf dependencies. These are the source of truth
- for Now in Android.
- That's why the duplicate classes from below dependencies are excluded.
- */
+ // Exclusion of protobuf / protolite dependencies is necessary as we depend
+ // on different versions than those included.
exclude(group = "com.google.protobuf", module = "protobuf-java")
exclude(group = "com.google.protobuf", module = "protobuf-kotlin")
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
@@ -52,17 +47,6 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin {
}
}
}
-
- extensions.configure {
- buildTypes.configureEach {
- // Disable the Crashlytics mapping file upload. This feature should only be
- // enabled if a Firebase backend is available and configured in
- // google-services.json.
- configure {
- mappingFileUploadEnabled = false
- }
- }
- }
}
}
}
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 2833b7534..bbab8cffc 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -26,6 +26,7 @@ pluginManagement {
dependencyResolutionManagement {
repositories {
+ gradlePluginPortal()
google {
content {
includeGroupByRegex("com\\.android.*")
diff --git a/build.gradle.kts b/build.gradle.kts
index 988de5ba6..0e7c45a87 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,26 +20,14 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
- alias(libs.plugins.compose) apply false
- alias(libs.plugins.datadog) apply false
alias(libs.plugins.devtools.ksp) apply false
- alias(libs.plugins.firebase.crashlytics) apply false
- alias(libs.plugins.firebase.perf) apply false
- alias(libs.plugins.google.services) apply false
alias(libs.plugins.hilt) apply false
- alias(libs.plugins.room) apply false
- alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.parcelize) apply false
- alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ktorfit) apply false
alias(libs.plugins.protobuf) apply false
- alias(libs.plugins.secrets) apply false
alias(libs.plugins.dependency.analysis)
- alias(libs.plugins.detekt) apply false
- alias(libs.plugins.meshtastic.detekt) apply false
alias(libs.plugins.kover)
- alias(libs.plugins.spotless) apply false
}
@@ -79,6 +67,7 @@ dependencies {
kover(projects.app)
kover(projects.meshServiceExample)
+ kover(projects.core.analytics)
kover(projects.core.data)
kover(projects.core.datastore)
kover(projects.core.model)
diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts
new file mode 100644
index 000000000..3dd0696be
--- /dev/null
+++ b/core/analytics/build.gradle.kts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+plugins {
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.hilt)
+ alias(libs.plugins.secrets)
+ alias(libs.plugins.kover)
+}
+
+dependencies {
+ implementation(project(":core:prefs"))
+ implementation(project(":core:model"))
+ implementation(libs.timber)
+ implementation(libs.appcompat)
+ implementation(libs.lifecycle.process)
+ googleImplementation(platform(libs.firebase.bom))
+ googleImplementation(libs.bundles.firebase) {
+ /*
+ Exclusion of protobuf / protolite dependencies is necessary as the
+ datastore-proto brings in protobuf dependencies. These are the source of truth
+ for Now in Android.
+ That's why the duplicate classes from below dependencies are excluded.
+ */
+ exclude(group = "com.google.protobuf", module = "protobuf-java")
+ exclude(group = "com.google.protobuf", module = "protobuf-kotlin")
+ exclude(group = "com.google.protobuf", module = "protobuf-javalite")
+ exclude(group = "com.google.firebase", module = "protolite-well-known-types")
+ }
+ googleImplementation(libs.bundles.datadog)
+}
+
+android {
+ buildFeatures { buildConfig = true }
+ namespace = "org.meshtastic.core.analytics"
+}
+
+secrets {
+ defaultPropertiesFileName = "secrets.defaults.properties"
+ propertiesFileName = "secrets.properties"
+}
diff --git a/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/di/FdroidPlatformAnalyticsModule.kt b/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/di/FdroidPlatformAnalyticsModule.kt
new file mode 100644
index 000000000..9b0bd4492
--- /dev/null
+++ b/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/di/FdroidPlatformAnalyticsModule.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.analytics.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.meshtastic.core.analytics.platform.FdroidPlatformAnalytics
+import org.meshtastic.core.analytics.platform.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
+}
diff --git a/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/platform/FdroidPlatformAnalytics.kt b/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/platform/FdroidPlatformAnalytics.kt
new file mode 100644
index 000000000..2b3ab4dc1
--- /dev/null
+++ b/core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/platform/FdroidPlatformAnalytics.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.analytics.platform
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavHostController
+import org.meshtastic.core.analytics.BuildConfig
+import org.meshtastic.core.analytics.DataPair
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * F-Droid specific implementation of [org.meshtastic.analytics.platform.PlatformAnalytics]. This provides no-op
+ * implementations for analytics and other platform services.
+ */
+class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
+ init {
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ }
+ Timber.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")
+ }
+
+ @Composable
+ override fun addNavigationTrackingEffect(navController: NavHostController) = {
+ // 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}")
+ }
+ }
+ }
+
+ override val isPlatformServicesAvailable: Boolean
+ get() = false
+
+ override fun track(event: String, vararg properties: DataPair) {
+ Timber.d("Track called: event=$event, properties=${properties.toList()}")
+ }
+}
diff --git a/app/src/fdroid/java/com/geeksville/mesh/MeshUtilApplication.kt b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/di/GooglePlatformAnalyticsModule.kt
similarity index 52%
rename from app/src/fdroid/java/com/geeksville/mesh/MeshUtilApplication.kt
rename to core/analytics/src/google/kotlin/org/meshtastic/core/analytics/di/GooglePlatformAnalyticsModule.kt
index 5782d5ce0..4281c2f0e 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/MeshUtilApplication.kt
+++ b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/di/GooglePlatformAnalyticsModule.kt
@@ -15,19 +15,21 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh
+package org.meshtastic.core.analytics.di
-import com.geeksville.mesh.android.GeeksvilleApplication
-import dagger.hilt.android.HiltAndroidApp
-import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
-import javax.inject.Inject
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.meshtastic.core.analytics.platform.GooglePlatformAnalytics
+import org.meshtastic.core.analytics.platform.PlatformAnalytics
+import javax.inject.Singleton
-@HiltAndroidApp
-class MeshUtilApplication : GeeksvilleApplication() {
+/** Hilt module to provide the [GooglePlatformAnalytics] for the google flavor. */
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class GooglePlatformAnalyticsModule {
- @Inject override lateinit var analyticsPrefs: AnalyticsPrefs
-
- override fun onCreate() {
- super.onCreate()
- }
+ @Binds @Singleton
+ abstract fun bindPlatformHelper(googlePlatformHelper: GooglePlatformAnalytics): PlatformAnalytics
}
diff --git a/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt
new file mode 100644
index 000000000..7c36c6b21
--- /dev/null
+++ b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.analytics.platform
+
+import android.app.Application
+import android.content.Context
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log.WARN
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavHostController
+import com.datadog.android.Datadog
+import com.datadog.android.DatadogSite
+import com.datadog.android.compose.ExperimentalTrackingApi
+import com.datadog.android.compose.NavigationViewTrackingEffect
+import com.datadog.android.compose.enableComposeActionTracking
+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.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
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailabilityLight
+import com.google.firebase.Firebase
+import com.google.firebase.analytics.analytics
+import com.google.firebase.crashlytics.crashlytics
+import com.google.firebase.crashlytics.setCustomKeys
+import com.google.firebase.initialize
+import com.google.firebase.perf.performance
+import dagger.hilt.android.qualifiers.ApplicationContext
+import io.opentelemetry.api.GlobalOpenTelemetry
+import kotlinx.coroutines.flow.launchIn
+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 javax.inject.Inject
+
+/**
+ * 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.
+ */
+class GooglePlatformAnalytics
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+ analyticsPrefs: AnalyticsPrefs,
+) : PlatformAnalytics {
+
+ private val sampleRate = 10f // For Datadog remote sample rate
+
+ 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"
+ }
+
+ init {
+ initDatadog(context as Application, analyticsPrefs)
+ initCrashlytics(context, analyticsPrefs)
+ Timber.plant(Timber.DebugTree()) // Always plant DebugTree
+
+ if (isPlatformServicesAvailable) {
+ val datadogLogger =
+ Logger.Builder()
+ .setService(SERVICE_NAME)
+ .setNetworkInfoEnabled(true)
+ .setRemoteSampleRate(sampleRate)
+ .setBundleWithTraceEnabled(true)
+ .setBundleWithRumEnabled(true)
+ .build()
+ Timber.plant(DatadogTree(datadogLogger))
+ Timber.plant(CrashlyticsTree())
+ }
+ // Initial consent state
+ updateAnalyticsConsent(analyticsPrefs.analyticsAllowed)
+
+ // Subscribe to analytics preference changes
+ analyticsPrefs
+ .getAnalyticsAllowedChangesFlow()
+ .onEach { allowed -> updateAnalyticsConsent(allowed) }
+ .launchIn(ProcessLifecycleOwner.get().lifecycleScope)
+ }
+
+ private fun initDatadog(application: Application, analyticsPrefs: AnalyticsPrefs) {
+ 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.setUserInfo(analyticsPrefs.installId)
+ Datadog.setVerbosity(WARN)
+
+ val rumConfiguration =
+ RumConfiguration.Builder(BuildConfig.datadogApplicationId)
+ .trackAnonymousUser(true)
+ .trackBackgroundEvents(true)
+ .trackFrustrations(true)
+ .trackLongTasks()
+ .trackNonFatalAnrs(true)
+ .trackUserInteractions()
+ .enableComposeActionTracking()
+ .build()
+ Rum.enable(rumConfiguration)
+
+ val logsConfig = LogsConfiguration.Builder().build()
+ Logs.enable(logsConfig)
+
+ val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(true).build()
+ Trace.enable(traceConfig)
+
+ GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME))
+
+ val sessionReplayConfig =
+ SessionReplayConfiguration.Builder(sampleRate = sampleRate)
+ .addExtensionSupport(ComposeExtensionSupport())
+ .build()
+ SessionReplay.enable(sessionReplayConfig)
+ }
+
+ private fun initCrashlytics(application: Application, analyticsPrefs: AnalyticsPrefs) {
+ Firebase.initialize(application)
+ Firebase.crashlytics.setUserId(analyticsPrefs.installId)
+ }
+
+ /**
+ * 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 (!isPlatformServicesAvailable || isInTestLab) {
+ Timber.i("Analytics not available or in test lab, consent update skipped.")
+ return
+ }
+ Timber.i(if (allowed) "Analytics enabled" else "Analytics disabled")
+
+ Datadog.setTrackingConsent(if (allowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
+ Firebase.crashlytics.isCrashlyticsCollectionEnabled = allowed
+ Firebase.analytics.setAnalyticsCollectionEnabled(allowed)
+ Firebase.performance.isPerformanceCollectionEnabled = allowed
+
+ if (allowed) {
+ Firebase.crashlytics.sendUnsentReports()
+ }
+ }
+
+ 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)
+ }
+
+ @OptIn(ExperimentalTrackingApi::class)
+ @Composable
+ override fun addNavigationTrackingEffect(navController: NavHostController) = {
+ if (Datadog.isInitialized()) {
+ NavigationViewTrackingEffect(
+ navController = navController,
+ trackArguments = true,
+ destinationPredicate = AcceptAllNavDestinations(),
+ )
+ }
+ }
+
+ private val isGooglePlayAvailable: Boolean
+ get() =
+ GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(context).let {
+ it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
+ }
+
+ private val isDatadogAvailable: Boolean
+ get() = Datadog.isInitialized()
+
+ override val isPlatformServicesAvailable: Boolean
+ get() = isGooglePlayAvailable && isDatadogAvailable
+
+ private class CrashlyticsTree : Timber.Tree() {
+ 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?) {
+ if (!Firebase.crashlytics.isCrashlyticsCollectionEnabled) return
+
+ Firebase.crashlytics.setCustomKeys {
+ key(KEY_PRIORITY, priority)
+ key(KEY_TAG, tag ?: "No Tag")
+ key(KEY_MESSAGE, message)
+ }
+
+ if (t == null) {
+ Firebase.crashlytics.recordException(Exception(message))
+ } else {
+ Firebase.crashlytics.recordException(t)
+ }
+ }
+ }
+
+ 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) {
+ val bundle = Bundle()
+ properties.forEach {
+ when (it.value) {
+ is Double -> bundle.putDouble(it.name, it.value)
+ is Int ->
+ bundle.putLong(it.name, it.value.toLong()) // Firebase expects Long for integer values in bundles
+ is Long -> bundle.putLong(it.name, it.value)
+ is Float -> bundle.putDouble(it.name, it.value.toDouble())
+ 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})")
+ }
+ Firebase.analytics.logEvent(event, bundle)
+ }
+}
diff --git a/app/src/google/java/com/geeksville/mesh/MeshUtilApplication.kt b/core/analytics/src/main/kotlin/org/meshtastic/core/analytics/DataPair.kt
similarity index 62%
rename from app/src/google/java/com/geeksville/mesh/MeshUtilApplication.kt
rename to core/analytics/src/main/kotlin/org/meshtastic/core/analytics/DataPair.kt
index 30e8ff1d6..1822c417f 100644
--- a/app/src/google/java/com/geeksville/mesh/MeshUtilApplication.kt
+++ b/core/analytics/src/main/kotlin/org/meshtastic/core/analytics/DataPair.kt
@@ -15,18 +15,14 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh
+package org.meshtastic.core.analytics
-import com.geeksville.mesh.android.GeeksvilleApplication
-import dagger.hilt.android.HiltAndroidApp
-import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
-import javax.inject.Inject
-
-@HiltAndroidApp
-class MeshUtilApplication : GeeksvilleApplication() {
- @Inject override lateinit var analyticsPrefs: AnalyticsPrefs
-
- override fun onCreate() {
- super.onCreate()
- }
+/**
+ * A key-value pair for sending properties with analytics events.
+ *
+ * @param name The name (key) of the property.
+ * @param valueIn The raw value of the property; converted to the string "null" if null.
+ */
+class DataPair(val name: String, val valueIn: Any?) {
+ val value: Any = valueIn ?: "null"
}
diff --git a/core/analytics/src/main/kotlin/org/meshtastic/core/analytics/platform/PlatformAnalytics.kt b/core/analytics/src/main/kotlin/org/meshtastic/core/analytics/platform/PlatformAnalytics.kt
new file mode 100644
index 000000000..e18d3e87e
--- /dev/null
+++ b/core/analytics/src/main/kotlin/org/meshtastic/core/analytics/platform/PlatformAnalytics.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.analytics.platform
+
+import androidx.navigation.NavHostController
+import org.meshtastic.core.analytics.DataPair
+
+/**
+ * Interface to abstract platform-specific functionalities, primarily for analytics and related services that differ
+ * between product flavors.
+ */
+interface PlatformAnalytics {
+
+ fun track(event: String, vararg properties: DataPair)
+
+ /**
+ * Sets device-specific attributes (e.g., firmware version, hardware model) for analytics.
+ *
+ * @param firmwareVersion The firmware version of the connected device.
+ * @param model The hardware model of the connected device.
+ */
+ fun setDeviceAttributes(firmwareVersion: String, model: String)
+
+ /**
+ * A Composable function to set up navigation tracking for the current platform.
+ *
+ * @param navController The [NavHostController] to track.
+ */
+ fun addNavigationTrackingEffect(navController: NavHostController): () -> Unit
+
+ /**
+ * Indicates whether platform-specific services (like Google Play Services or Datadog) are available and
+ * initialized.
+ */
+ val isPlatformServicesAvailable: Boolean
+}
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
index b889ecc02..5fbcca789 100644
--- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt
@@ -35,11 +35,8 @@ class FirmwareReleaseLocalDataSource @Inject constructor(private val firmwareRel
firmwareReleases: List,
releaseType: FirmwareReleaseType,
) = withContext(Dispatchers.IO) {
- if (firmwareReleases.isNotEmpty()) {
- firmwareReleaseDao.deleteAll()
- firmwareReleases.forEach { firmwareRelease ->
- firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType))
- }
+ firmwareReleases.forEach { firmwareRelease ->
+ firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType))
}
}
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceVersion.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceVersion.kt
index 6aeffb8a3..5d07720a6 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceVersion.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceVersion.kt
@@ -40,7 +40,14 @@ data class DeviceVersion(val asString: String) : Comparable {
@Suppress("TooGenericExceptionThrown", "MagicNumber")
private fun verStringToInt(s: String): Int {
// Allow 1 to two digits per match
- val match = Regex("(\\d{1,2}).(\\d{1,2}).(\\d{1,2})").find(s) ?: throw Exception("Can't parse version $s")
+ val versionString =
+ if (s.split(".").size == 2) {
+ "$s.0"
+ } else {
+ s
+ }
+ val match =
+ Regex("(\\d{1,2}).(\\d{1,2}).(\\d{1,2})").find(versionString) ?: throw Exception("Can't parse version $s")
val (major, minor, build) = match.destructured
return major.toInt() * 10000 + minor.toInt() * 100 + build.toInt()
}
diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt
index 89ab8b721..4dd42dec7 100644
--- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt
+++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt
@@ -18,6 +18,9 @@
package org.meshtastic.core.prefs.analytics
import android.content.SharedPreferences
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import org.meshtastic.core.prefs.NullableStringPrefDelegate
import org.meshtastic.core.prefs.PrefDelegate
import org.meshtastic.core.prefs.di.AnalyticsSharedPreferences
@@ -26,23 +29,55 @@ import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
+/** Interface for managing analytics-related preferences. */
interface AnalyticsPrefs {
+ /** Preference for whether analytics collection is allowed by the user. */
var analyticsAllowed: Boolean
+
+ /**
+ * Provides a [Flow] that emits the current state of [analyticsAllowed] and subsequent changes.
+ *
+ * @return A [Flow] of [Boolean] indicating if analytics are allowed.
+ */
+ fun getAnalyticsAllowedChangesFlow(): Flow
+
+ /** Unique installation ID for analytics purposes. */
val installId: String
+
+ companion object {
+ /** Key for the analyticsAllowed preference. */
+ const val KEY_ANALYTICS_ALLOWED = "allowed"
+
+ /** Name of the SharedPreferences file where analytics preferences are stored. */
+ const val ANALYTICS_PREFS_NAME = "analytics-prefs"
+ }
}
-// Having an additional app prefs store is maintaining the existing behavior.
@Singleton
class AnalyticsPrefsImpl
@Inject
constructor(
- @AnalyticsSharedPreferences analyticsPrefs: SharedPreferences,
+ @AnalyticsSharedPreferences private val analyticsSharedPreferences: SharedPreferences,
@AppSharedPreferences appPrefs: SharedPreferences,
) : AnalyticsPrefs {
- override var analyticsAllowed: Boolean by PrefDelegate(analyticsPrefs, "allowed", true)
+ override var analyticsAllowed: Boolean by
+ PrefDelegate(analyticsSharedPreferences, AnalyticsPrefs.KEY_ANALYTICS_ALLOWED, true)
private var _installId: String? by NullableStringPrefDelegate(appPrefs, "appPrefs_install_id", null)
override val installId: String
get() = _installId ?: UUID.randomUUID().toString().also { _installId = it }
+
+ override fun getAnalyticsAllowedChangesFlow(): Flow = callbackFlow {
+ val listener =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == AnalyticsPrefs.KEY_ANALYTICS_ALLOWED) {
+ trySend(analyticsAllowed)
+ }
+ }
+ // Emit the initial value
+ trySend(analyticsAllowed)
+ analyticsSharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+ awaitClose { analyticsSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
+ }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 365c39a92..d3cd2dc73 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -162,15 +162,20 @@ zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", ve
# Build Logic
android-gradleApiPlugin = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
android-tools-common = { module = "com.android.tools:common", version = "31.13.0" }
+serialization-gradlePlugin = { module = "org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin", version.ref = "kotlinx-serialization" }
androidx-lint-gradle = { module = "androidx.lint:lint-gradle", version = "1.0.0-alpha05" }
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
+datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version = "1.20.0" }
+detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.4.27" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
-detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" }
firebase-performance-gradlePlugin = { module = "com.google.firebase:perf-plugin", version = "2.0.1" }
+google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version = "4.4.3" }
+hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" }
room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" }
+secrets-gradlePlugin = {module = "com.google.android.secrets-gradle-plugin:com.google.android.secrets-gradle-plugin.gradle.plugin", version = "1.1.0"}
spotless-gradlePlugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "8.0.0" }
[bundles]
@@ -189,7 +194,7 @@ coroutines = ["kotlinx-coroutines-android", "kotlinx-coroutines-guava"]
hilt = ["hilt-android", "hilt-navigation-compose"]
# Google
-firebase = ["firebase-analytics", "firebase-crashlytics"]
+firebase = ["firebase-analytics", "firebase-crashlytics", "firebase-performance"]
maps-compose = ["location-services", "maps-compose", "maps-compose-utils", "maps-compose-widgets"]
# Networking
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0ca6613e7..cacdfb536 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,5 +1,3 @@
-import org.gradle.kotlin.dsl.maven
-
/*
* Copyright (c) 2025 Meshtastic LLC
*
@@ -19,6 +17,7 @@ import org.gradle.kotlin.dsl.maven
include(
":app",
+ ":core:analytics",
":core:data",
":core:database",
":core:datastore",