kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor(analytics)!: modularize analytics - remove Logging (#3256)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>pull/3259/head
rodzic
9aa0cf9335
commit
cad88d277b
|
@ -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)
|
||||
|
|
|
@ -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 >= 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.<no name provided>$// 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$<no name provided> : 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: () -> 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 -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</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 -> errormsg("Serial error: $e") } debug("$device disconnected") onDeviceDisconnect(false) } })</ID>
|
||||
<ID>Wrapping:ServiceClient.kt$ServiceClient$Closeable, Logging</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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") ||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,21 +90,20 @@ 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 {
|
||||
private val connection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
|
||||
if (!isClosed) {
|
||||
val s = stubFactory(binder)
|
||||
|
@ -114,13 +111,12 @@ open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T
|
|||
onConnected(s)
|
||||
|
||||
// after calling our handler, tell anyone who was waiting for this connection to complete
|
||||
lock.withLock {
|
||||
condition.signalAll()
|
||||
}
|
||||
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
|
||||
// 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")
|
||||
Timber.w("A service connected while we were closing it, ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,18 +17,16 @@
|
|||
|
||||
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
|
||||
|
@ -38,10 +36,8 @@ class DeferredExecution : Logging {
|
|||
|
||||
// / 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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -27,36 +27,37 @@ 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)
|
||||
val locationRequest =
|
||||
LocationRequestCompat.Builder(intervalMs)
|
||||
.setMinUpdateDistanceMeters(minDistanceM)
|
||||
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
|
||||
.build()
|
||||
|
@ -66,7 +67,7 @@ class LocationRepository @Inject constructor(
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,13 +48,16 @@ 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 {
|
||||
usbRepository
|
||||
.createSerialConnection(
|
||||
device,
|
||||
object : SerialConnectionListener {
|
||||
override fun onMissingPermission() {
|
||||
errormsg("Need permissions for port")
|
||||
Timber.e("Need permissions for port")
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
|
@ -62,18 +65,18 @@ class SerialInterface @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onDataReceived(bytes: ByteArray) {
|
||||
debug("Received ${bytes.size} byte(s)")
|
||||
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")
|
||||
thrown?.let { e -> Timber.e("Serial error: $e") }
|
||||
Timber.d("$device disconnected")
|
||||
onDeviceDisconnect(false)
|
||||
}
|
||||
}).also { conn ->
|
||||
},
|
||||
)
|
||||
.also { conn ->
|
||||
connRef.set(conn)
|
||||
conn.connect()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,10 +119,11 @@ class TCPInterface @AssistedInject constructor(
|
|||
backoffDelay = MIN_BACKOFF_MILLIS
|
||||
|
||||
var timeoutCount = 0
|
||||
while (timeoutCount < 180) try { // close after 90s of inactivity
|
||||
while (timeoutCount < 180) {
|
||||
try { // close after 90s of inactivity
|
||||
val c = inputStream.read()
|
||||
if (c == -1) {
|
||||
warn("Got EOF on TCP stream")
|
||||
Timber.w("Got EOF on TCP stream")
|
||||
break
|
||||
} else {
|
||||
timeoutCount = 0
|
||||
|
@ -136,6 +135,7 @@ class TCPInterface @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onDeviceDisconnect(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +80,11 @@ internal class SerialConnectionImpl(
|
|||
port.dtr = true
|
||||
port.rts = true
|
||||
|
||||
debug("Starting serial reader thread")
|
||||
val io = SerialInputOutputManager(port, object : SerialInputOutputManager.Listener {
|
||||
Timber.d("Starting serial reader thread")
|
||||
val io =
|
||||
SerialInputOutputManager(
|
||||
port,
|
||||
object : SerialInputOutputManager.Listener {
|
||||
override fun onNewData(data: ByteArray) {
|
||||
listener.onDataReceived(data)
|
||||
}
|
||||
|
@ -96,7 +99,9 @@ internal class SerialConnectionImpl(
|
|||
closedLatch.countDown()
|
||||
listener.onDisconnected(e)
|
||||
}
|
||||
}).apply {
|
||||
},
|
||||
)
|
||||
.apply {
|
||||
readTimeout = 200 // To save battery we only timeout ever so often
|
||||
ioRef.set(this)
|
||||
}
|
||||
|
|
|
@ -23,20 +23,17 @@ 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 {
|
||||
internal val intentFilter
|
||||
get() =
|
||||
IntentFilter().apply {
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
|
||||
}
|
||||
|
@ -47,15 +44,15 @@ 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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
val serialDevicesWithDrivers =
|
||||
_serialDevices
|
||||
.mapLatest { serialDevices ->
|
||||
val serialProber = usbSerialProberLazy.get()
|
||||
buildMap {
|
||||
serialDevices.forEach { (k, v) ->
|
||||
serialProber.probeDevice(v)?.let { driver ->
|
||||
put(k, driver)
|
||||
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
|
||||
val serialDevicesWithPermission =
|
||||
_serialDevices
|
||||
.mapLatest { serialDevices ->
|
||||
usbManagerLazy.get()?.let { usbManager ->
|
||||
serialDevices.filterValues { device ->
|
||||
usbManager.hasPermission(device)
|
||||
}
|
||||
serialDevices.filterValues { device -> usbManager.hasPermission(device) }
|
||||
} ?: emptyMap()
|
||||
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, 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()) }
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -853,20 +853,20 @@ 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(icon: ImageVector, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,9 @@ 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 {
|
||||
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,28 +52,13 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,4 +73,3 @@ fun <T> toRemoteExceptions(inner: () -> T): T = try {
|
|||
else -> throw RemoteException(ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ pluginManagement {
|
|||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()}")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -35,13 +35,10 @@ 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAllFirmwareReleases() = withContext(Dispatchers.IO) { firmwareReleaseDao.deleteAll() }
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Ładowanie…
Reference in New Issue