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",