refactor(analytics)!: modularize analytics - remove Logging (#3256)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
pull/3259/head
James Rich 2025-09-30 18:22:22 -05:00 zatwierdzone przez GitHub
rodzic 9aa0cf9335
commit cad88d277b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
72 zmienionych plików z 1219 dodań i 1426 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -5,11 +5,6 @@
<ID>CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down</ID>
<ID>CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back</ID>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work</ID>
<ID>CommentSpacing:Exceptions.kt$/// Convert any exceptions in this service call into a RemoteException that the client can</ID>
<ID>CommentSpacing:Exceptions.kt$/// then handle</ID>
<ID>CommentSpacing:Exceptions.kt$Exceptions$/// Set in Application.onCreate</ID>
<ID>CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */</ID>
<ID>ComposableNaming:NodeDetail.kt$notesSection</ID>
<ID>ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList</ID>
@ -67,12 +62,10 @@
<ID>FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
<ID>FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
<ID>FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
<ID>FinalNewline:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt</ID>
<ID>FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>FinalNewline:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt</ID>
<ID>FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
@ -82,10 +75,8 @@
<ID>FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>FinalNewline:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt</ID>
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
@ -97,7 +88,7 @@
<ID>LambdaParameterEventTrailing:NodeDetail.kt$onSaveNotes</ID>
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>LargeClass:MeshService.kt$MeshService : ServiceLogging</ID>
<ID>LargeClass:MeshService.kt$MeshService : Service</ID>
<ID>LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
@ -166,12 +157,8 @@
<ID>MagicNumber:TCPInterface.kt$TCPInterface$180</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$500</ID>
<ID>MagicNumber:UIState.kt$4</ID>
<ID>MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider</ID>
<ID>MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker</ID>
<ID>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 &gt;= 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) */</ID>
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
<ID>MaxLineLength:ServiceClient.kt$ServiceClient$// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try</ID>
<ID>MaxLineLength:ServiceClient.kt$ServiceClient.&lt;no name provided&gt;$// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue</ID>
<ID>ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)</ID>
<ID>ModifierMissing:BLEDevices.kt$BLEDevices</ID>
<ID>ModifierMissing:Channel.kt$ChannelScreen</ID>
@ -256,7 +243,6 @@
<ID>ModifierReused:SignalMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Metric.SNR.color, minValue = Metric.SNR.min, maxValue = Metric.SNR.max, )</ID>
<ID>ModifierWithoutDefault:CommonCharts.kt$modifier</ID>
<ID>ModifierWithoutDefault:EnvironmentCharts.kt$modifier</ID>
<ID>MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex)</ID>
<ID>MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview</ID>
<ID>MultipleEmitters:CommonCharts.kt$LegendLabel</ID>
<ID>MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart</ID>
@ -275,12 +261,10 @@
<ID>NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
<ID>NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
<ID>NewLineAtEndOfFile:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt</ID>
<ID>NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>NewLineAtEndOfFile:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
@ -290,22 +274,16 @@
<ID>NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ </ID>
<ID>NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ </ID>
<ID>NoConsecutiveBlankLines:BootCompleteReceiver.kt$ </ID>
<ID>NoConsecutiveBlankLines:Constants.kt$ </ID>
<ID>NoConsecutiveBlankLines:DebugLogFile.kt$ </ID>
<ID>NoConsecutiveBlankLines:DeferredExecution.kt$ </ID>
<ID>NoConsecutiveBlankLines:Exceptions.kt$ </ID>
<ID>NoConsecutiveBlankLines:IRadioInterface.kt$ </ID>
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked</ID>
<ID>ParameterNaming:ChannelSettingsItemList.kt$onSelected</ID>
@ -345,8 +323,6 @@
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>SpacingAroundKeyword:Exceptions.kt$if</ID>
<ID>SpacingAroundKeyword:Exceptions.kt$when</ID>
<ID>SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>SwallowedException:MeshService.kt$MeshService$ex: BLEException</ID>
@ -376,16 +352,16 @@
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")</ID>
<ID>TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterfaceLogging</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService : ServiceLogging</ID>
<ID>TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterface</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService : Service</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService$&lt;no name provided&gt; : Stub</ID>
<ID>TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel</ID>
<ID>TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt</ID>
<ID>TooManyFunctions:NodesViewModel.kt$NodesViewModel : ViewModel</ID>
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModelLogging</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService : Logging</ID>
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging</ID>
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : Closeable</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$onBack: () -&gt; Unit</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$title: String</ID>
@ -394,12 +370,6 @@
<ID>ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)</ID>
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
<ID>ViewModelInjection:DebugSearch.kt$viewModel</ID>
<ID>WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -&gt; navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
<ID>Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(</ID>
<ID>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) } })</ID>
<ID>Wrapping:SerialInterface.kt$SerialInterface$(</ID>
<ID>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 -&gt; errormsg("Serial error: $e") } debug("$device disconnected") onDeviceDisconnect(false) } })</ID>
<ID>Wrapping:ServiceClient.kt$ServiceClient$Closeable, Logging</ID>
</CurrentIssues>
</SmellBaseline>

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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() {}
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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

Wyświetl plik

@ -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<MarkerWithLabel>,
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
},
)

Wyświetl plik

@ -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}")
}
}
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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")
}
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

Wyświetl plik

@ -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.
}
}

Wyświetl plik

@ -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}")
}
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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")
}
}
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}

Wyświetl plik

@ -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") ||

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}

Wyświetl plik

@ -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<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable,
Logging {
/** A wrapper that cleans up the service binding process */
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable {
var serviceP: T? = null
@ -72,17 +70,17 @@ open class ServiceClient<T : IInterface>(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<T : IInterface>(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()
}
}
}

Wyświetl plik

@ -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()
}
}
}

Wyświetl plik

@ -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<in T> : Logging {
/** A deferred execution object (with various possible implementations) */
interface Continuation<in T> {
abstract fun resume(res: Result<T>)
// 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<in T>(private val cb: (Result<T>) -> Unit) : Continuation<T> {
override fun resume(res: Result<T>) = cb(res)
}
@ -46,8 +40,8 @@ class CallbackContinuation<in T>(private val cb: (Result<T>) -> 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<T> : Continuation<T> {
@ -84,8 +78,8 @@ class SyncContinuation<T> : Continuation<T> {
}
/**
* 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
*/

Wyświetl plik

@ -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)

Wyświetl plik

@ -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<ImmutableList<UiMeshLog>> =
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<MeshLog>) = databaseLogs

Wyświetl plik

@ -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<NodesRoutes.NodeDetailGraph>().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")
}
}
}

Wyświetl plik

@ -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<Int> = uiPreferencesDataSource.theme
val firmwareVersion = myNodeInfo.mapNotNull { nodeInfo -> nodeInfo?.firmwareVersion }
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
val deviceHardware: StateFlow<DeviceHardware?> =
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<MeshProtos.ClientNotification?> = 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<AdminProtos.SharedContact?> = 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")
}
}

Wyświetl plik

@ -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<BluetoothBroadcastReceiver>,
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 {

Wyświetl plik

@ -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<LocationManager>,
) : 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<Boolean> = MutableStateFlow(false)
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
val receivingLocationUpdates: StateFlow<Boolean>
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()
}

Wyświetl plik

@ -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}")
}
}
}

Wyświetl plik

@ -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<NsdManager>,
private val connectivityManager: dagger.Lazy<ConnectivityManager>,
private val dispatchers: CoroutineDispatchers,
) : Logging {
) {
val networkAvailable: Flow<Boolean>
get() = connectivityManager.get().networkAvailable()
.flowOn(dispatchers.io)
.conflate()
get() = connectivityManager.get().networkAvailable().flowOn(dispatchers.io).conflate()
val resolvedList: Flow<List<NsdServiceInfo>>
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

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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<BluetoothInterface>,
Logging {
) : InterfaceSpec<BluetoothInterface> {
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

Wyświetl plik

@ -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")

Wyświetl plik

@ -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> = _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")
}
}
}

Wyświetl plik

@ -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<SerialConnection?>()
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)
}
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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<UsbManager?>,
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()

Wyświetl plik

@ -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()
}
}
}
}
}

Wyświetl plik

@ -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<UsbBroadcastReceiver>,
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>
) : Logging {
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>,
) {
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
@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<Boolean> =
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()) }
}

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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<PacketRepository>
@ -156,7 +154,7 @@ class MeshService :
private val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
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<MeshProtos.NodeInfo>()
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<NodeInfo> = 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
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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<ServiceStarter>()
.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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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") }
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 ")
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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")
}
}
}

Wyświetl plik

@ -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() },
)
}

Wyświetl plik

@ -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<MyNodeEntity?> = 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}")
}
}
}

Wyświetl plik

@ -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<SettingsRoutes.Settings>().destNum
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?>
@ -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<MyNodeEntity?>
@ -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) }
}
}

Wyświetl plik

@ -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())),

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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 <T> 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)
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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}")
}
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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<Project> {
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<Project> {
}
}
}
extensions.configure<ApplicationExtension> {
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<CrashlyticsExtension> {
mappingFileUploadEnabled = false
}
}
}
}
}
}

Wyświetl plik

@ -26,6 +26,7 @@ pluginManagement {
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
google {
content {
includeGroupByRegex("com\\.android.*")

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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"
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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()}")
}
}

Wyświetl plik

@ -15,19 +15,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

Wyświetl plik

@ -15,18 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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"
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

Wyświetl plik

@ -35,11 +35,8 @@ class FirmwareReleaseLocalDataSource @Inject constructor(private val firmwareRel
firmwareReleases: List<NetworkFirmwareRelease>,
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))
}
}

Wyświetl plik

@ -40,7 +40,14 @@ data class DeviceVersion(val asString: String) : Comparable<DeviceVersion> {
@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()
}

Wyświetl plik

@ -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<Boolean>
/** 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<Boolean> = 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) }
}
}

Wyświetl plik

@ -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

Wyświetl plik

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