feat(analytics): Integrate Datadog for RUM and Logging (#2578)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-08-01 16:54:46 -05:00 committed by GitHub
parent f5478b42c3
commit ab22a655c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 348 additions and 194 deletions

View file

@ -18,7 +18,6 @@
package com.geeksville.mesh
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
@ -26,8 +25,5 @@ class MeshUtilApplication : GeeksvilleApplication() {
override fun onCreate() {
super.onCreate()
Logging.showLogs = BuildConfig.DEBUG
}
}
}

View file

@ -23,21 +23,24 @@ import android.content.SharedPreferences
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.analytics.AnalyticsProvider
import com.geeksville.mesh.model.DeviceHardware
import timber.log.Timber
open class GeeksvilleApplication : Application(), Logging {
open class GeeksvilleApplication :
Application(),
Logging {
companion object {
lateinit var analytics: AnalyticsProvider
}
/// Are we running inside the testlab?
// / 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")
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
if (testLabSetting != null) info("Testlab is $testLabSetting")
return "true" == testLabSetting
}
@ -48,9 +51,7 @@ open class GeeksvilleApplication : Application(), Logging {
var isAnalyticsAllowed: Boolean
get() = analyticsPrefs.getBoolean("allowed", true)
set(value) {
analyticsPrefs.edit {
putBoolean("allowed", value)
}
analyticsPrefs.edit { putBoolean("allowed", value) }
// Change the flag with the providers
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
@ -64,10 +65,18 @@ open class GeeksvilleApplication : Application(), Logging {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
val nopAnalytics = com.geeksville.mesh.analytics.NopAnalytics(this)
analytics = nopAnalytics
isAnalyticsAllowed = false
}
}
fun Context.isGooglePlayAvailable(): Boolean = false
fun Context.isGooglePlayAvailable(): Boolean = false
fun setAttributes(deviceVersion: String, deviceHardware: DeviceHardware) {
// No-op for F-Droid version
}

View file

@ -21,11 +21,11 @@ import android.os.Debug
import com.geeksville.mesh.android.AppPrefs
import com.geeksville.mesh.android.BuildUtils.isEmulator
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.Exceptions
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.Firebase
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.setCustomKeys
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class MeshUtilApplication : GeeksvilleApplication() {
@ -33,30 +33,15 @@ class MeshUtilApplication : GeeksvilleApplication() {
override fun onCreate() {
super.onCreate()
Logging.showLogs = BuildConfig.DEBUG
// We default to off in the manifest - we turn on here if the user approves
// leave off when running in the debugger
if (!isEmulator && (!BuildConfig.DEBUG || !Debug.isDebuggerConnected())) {
val crashlytics = Firebase.crashlytics
crashlytics.setCrashlyticsCollectionEnabled(isAnalyticsAllowed)
crashlytics.setCustomKey("debug_build", BuildConfig.DEBUG)
val crashlytics = FirebaseCrashlytics.getInstance()
val pref = AppPrefs(this)
crashlytics.setUserId(pref.getInstallId()) // be able to group all bugs per anonymous user
// We always send our log messages to the crashlytics lib, but they only get sent to the server if we report an exception
// This makes log messages work properly if someone turns on analytics just before they click report bug.
// send all log messages through crashyltics, so if we do crash we'll have those in the report
val standardLogger = Logging.printlog
Logging.printlog = { level, tag, message ->
crashlytics.log("$tag: $message")
standardLogger(level, tag, message)
}
fun sendCrashReports() {
if (isAnalyticsAllowed)
crashlytics.sendUnsentReports()
if (isAnalyticsAllowed) crashlytics.sendUnsentReports()
}
// Send any old reports if user approves
@ -67,6 +52,30 @@ class MeshUtilApplication : GeeksvilleApplication() {
crashlytics.recordException(exception)
sendCrashReports() // Send the new report
}
Timber.plant(CrashlyticsTree())
}
}
}
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?) {
FirebaseCrashlytics.getInstance().setCustomKeys {
key(KEY_PRIORITY, priority)
key(KEY_TAG, tag ?: "No Tag")
key(KEY_MESSAGE, message)
}
if (t == null) {
FirebaseCrashlytics.getInstance().recordException(Exception(message))
} else {
FirebaseCrashlytics.getInstance().recordException(t)
}
}
}

View file

@ -21,44 +21,54 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.provider.Settings
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
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.timber.DatadogTree
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.analytics.AnalyticsProvider
import com.geeksville.mesh.analytics.FirebaseAnalytics
import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.util.exceptionReporter
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailabilityLight
import com.suddenh4x.ratingdialog.AppRating
import timber.log.Timber
/**
* Created by kevinh on 1/4/15.
*/
open class GeeksvilleApplication : Application(), Logging {
/** Created by kevinh on 1/4/15. */
open class GeeksvilleApplication :
Application(),
Logging {
companion object {
lateinit var analytics: AnalyticsProvider
}
/// Are we running inside the testlab?
// / 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")
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
if (testLabSetting != null) info("Testlab is $testLabSetting")
return "true" == testLabSetting
}
private val analyticsPrefs: SharedPreferences by lazy {
getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE)
}
private val analyticsPrefs: SharedPreferences by lazy { getSharedPreferences("analytics-prefs", MODE_PRIVATE) }
var isAnalyticsAllowed: Boolean
get() = analyticsPrefs.getBoolean("allowed", true)
set(value) {
analyticsPrefs.edit {
putBoolean("allowed", value)
}
analyticsPrefs.edit { putBoolean("allowed", value) }
// Change the flag with the providers
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
@ -68,12 +78,20 @@ open class GeeksvilleApplication : Application(), Logging {
fun askToRate(activity: AppCompatActivity) {
if (!isGooglePlayAvailable()) return
exceptionReporter { // we don't want to crash our app because of bugs in this optional feature
exceptionReporter {
// we don't want to crash our app because of bugs in this optional feature
AppRating.Builder(activity)
.setMinimumLaunchTimes(10) // default is 5, 3 means app is launched 3 or more times
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days later than installation
.setMinimumLaunchTimesToShowAgain(5) // default is 5, 1 means app is launched 1 or more times after neutral button clicked
.setMinimumDaysToShowAgain(14) // default is 14, 1 means app is launched 1 or more days after neutral button clicked
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days
// later than installation
.setMinimumLaunchTimesToShowAgain(
5,
) // default is 5, 1 means app is launched 1 or more times after neutral button
// clicked
.setMinimumDaysToShowAgain(
14,
) // default is 14, 1 means app is launched 1 or more days after neutral button
// clicked
.showIfMeetsConditions()
}
}
@ -81,19 +99,64 @@ open class GeeksvilleApplication : Application(), Logging {
override fun onCreate() {
super.onCreate()
val firebaseAnalytics = com.geeksville.mesh.analytics.FirebaseAnalytics(this)
val logger =
Logger.Builder()
.setNetworkInfoEnabled(true)
.setLogcatLogsEnabled(true)
.setRemoteSampleRate(100f)
.setBundleWithTraceEnabled(true)
.setName("TimberLogger")
.build()
val firebaseAnalytics = FirebaseAnalytics(this)
analytics = firebaseAnalytics
// Set analytics per prefs
isAnalyticsAllowed = isAnalyticsAllowed
if (isAnalyticsAllowed || BuildConfig.DEBUG) {
// datadog analytics
val configuration =
Configuration.Builder(
clientToken = BuildConfig.datadogClientToken,
env = if (BuildConfig.DEBUG || true) "debug" else "release",
variant = BuildConfig.FLAVOR,
)
.useSite(DatadogSite.US5)
.setCrashReportsEnabled(true)
.setUseDeveloperModeWhenDebuggable(true)
.build()
val consent =
if (isAnalyticsAllowed) {
TrackingConsent.GRANTED
} else {
TrackingConsent.NOT_GRANTED
}
Datadog.initialize(this, configuration, consent)
Datadog.setVerbosity(Log.VERBOSE)
val rumConfiguration =
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
.trackUserInteractions()
.trackLongTasks()
.trackBackgroundEvents(true)
.enableComposeActionTracking()
.build()
Rum.enable(rumConfiguration)
val logsConfig = LogsConfiguration.Builder().build()
Logs.enable(logsConfig)
Timber.plant(Timber.DebugTree(), DatadogTree(logger))
}
}
}
fun Context.isGooglePlayAvailable(): Boolean {
return GoogleApiAvailabilityLight.getInstance()
.isGooglePlayServicesAvailable(this)
.let {
it != ConnectionResult.SERVICE_MISSING &&
it != ConnectionResult.SERVICE_INVALID
}
}
fun setAttributes(firmwareVersion: String, deviceHardware: DeviceHardware) {
GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion)
GlobalRumMonitor.get().addAttribute("device_hardware", deviceHardware.hwModelSlug)
}
fun Context.isGooglePlayAvailable(): Boolean =
GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(this).let {
it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
}

View file

@ -17,73 +17,32 @@
package com.geeksville.mesh.android
import android.os.Build
import android.util.Log
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.util.Exceptions
/**
* Created by kevinh on 12/24/14.
*/
typealias LogPrinter = (Int, String, String) -> Unit
import timber.log.Timber
interface Logging {
companion object {
/** Some vendors strip log messages unless the severity is super high.
*
* alps == Soyes
* HMD Global == mfg of the Nokia 7.2
*/
private val badVendors = setOf("OnePlus", "alps", "HMD Global", "Sony")
private fun tag(): String = this.javaClass.name
/// if false NO logs will be shown, set this in the application based on BuildConfig.DEBUG
var showLogs = true
fun info(msg: String) = Timber.tag(tag()).i(msg)
/** if true, all logs will be printed at error level. Sometimes necessary for buggy ROMs
* that filter logcat output below this level.
*
* Since there are so many bad vendors, we just always lie if we are a release build
*/
var forceErrorLevel = !BuildConfig.DEBUG || badVendors.contains(Build.MANUFACTURER)
fun debug(msg: String) = Timber.tag(tag()).d(msg)
/// If false debug logs will not be shown (but others might)
var showDebug = true
fun warn(msg: String) = Timber.tag(tag()).w(msg)
/**
* By default all logs are printed using the standard android Log class. But clients
* can change printlog to a different implementation (for logging to files or via
* google crashlytics)
*/
var printlog: LogPrinter = { level, tag, message ->
if (showLogs) {
if (showDebug || level > Log.DEBUG) {
Log.println(if (forceErrorLevel) Log.ERROR else level, tag, message)
}
}
/**
* 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)
}
}
private fun tag(): String = this.javaClass.getName()
fun info(msg: String) = printlog(Log.INFO, tag(), msg)
fun verbose(msg: String) = printlog(Log.VERBOSE, tag(), msg)
fun debug(msg: String) = printlog(Log.DEBUG, tag(), msg)
fun warn(msg: String) = printlog(Log.WARN, tag(), 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)
printlog(Log.ERROR, tag(), "$msg (exception ${ex.message})")
else
printlog(Log.ERROR, tag(), "$msg")
}
/// Kotlin assertions are disabled on android, so instead we use this assert helper
// / Kotlin assertions are disabled on android, so instead we use this assert helper
fun logAssert(f: Boolean) {
if (!f) {
val ex = AssertionError("Assertion failed")
@ -93,8 +52,8 @@ interface Logging {
}
}
/// Report an error (including messaging our crash reporter service if allowed
// / Report an error (including messaging our crash reporter service if allowed
fun reportError(s: String) {
Exceptions.report(Exception("logging reportError: $s"), s)
}
}
}

View file

@ -56,6 +56,7 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.database.entity.asDeviceVersion
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
@ -192,6 +193,7 @@ constructor(
private val radioConfigRepository: RadioConfigRepository,
private val radioInterfaceService: RadioInterfaceService,
private val meshLogRepository: MeshLogRepository,
private val deviceHardwareRepository: DeviceHardwareRepository,
private val packetRepository: PacketRepository,
private val quickChatActionRepository: QuickChatActionRepository,
private val locationRepository: LocationRepository,
@ -219,8 +221,17 @@ constructor(
viewModelScope.launch { _excludedModulesUnlocked.value = true }
}
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 { deviceHardwareRepository.getDeviceHardwareByModel(it.number) }
}
.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null)
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = radioConfigRepository.clientNotification
fun clearClientNotification(notification: MeshProtos.ClientNotification) {

View file

@ -27,7 +27,9 @@ import kotlinx.coroutines.withContext
import java.io.IOException
import javax.inject.Inject
class DeviceHardwareRepository @Inject constructor(
class DeviceHardwareRepository
@Inject
constructor(
private val apiDataSource: DeviceHardwareRemoteDataSource,
private val localDataSource: DeviceHardwareLocalDataSource,
private val jsonDataSource: DeviceHardwareJsonDataSource,
@ -45,15 +47,13 @@ class DeviceHardwareRepository @Inject constructor(
} else {
val cachedHardware = localDataSource.getByHwModel(hwModel)
if (cachedHardware != null && !isCacheExpired(cachedHardware.lastUpdated)) {
debug("Using recent cached device hardware")
val externalModel = cachedHardware.asExternalModel()
return@withContext externalModel
}
}
try {
debug("Fetching device hardware from server")
val deviceHardware = apiDataSource.getAllDeviceHardware()
?: throw IOException("empty response from server")
val deviceHardware =
apiDataSource.getAllDeviceHardware() ?: throw IOException("empty response from server")
localDataSource.insertAllDeviceHardware(deviceHardware)
val cachedHardware = localDataSource.getByHwModel(hwModel)
val externalModel = cachedHardware?.asExternalModel()
@ -65,7 +65,6 @@ class DeviceHardwareRepository @Inject constructor(
debug("Using stale cached device hardware")
return@withContext cachedHardware.asExternalModel()
}
debug("Loading and caching device hardware from local JSON asset")
localDataSource.insertAllDeviceHardware(jsonDataSource.loadDeviceHardwareFromJsonAsset())
cachedHardware = localDataSource.getByHwModel(hwModel)
val externalModel = cachedHardware?.asExternalModel()
@ -78,10 +77,7 @@ class DeviceHardwareRepository @Inject constructor(
localDataSource.deleteAllDeviceHardware()
}
/**
* Check if the cache is expired
*/
private fun isCacheExpired(lastUpdated: Long): Boolean {
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}
/** Check if the cache is expired */
private fun isCacheExpired(lastUpdated: Long): Boolean =
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.repository.api
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.database.entity.FirmwareRelease
import com.geeksville.mesh.database.entity.FirmwareReleaseType
@ -28,7 +27,9 @@ import kotlinx.coroutines.flow.flow
import java.io.IOException
import javax.inject.Inject
class FirmwareReleaseRepository @Inject constructor(
class FirmwareReleaseRepository
@Inject
constructor(
private val apiDataSource: FirmwareReleaseRemoteDataSource,
private val localDataSource: FirmwareReleaseLocalDataSource,
private val jsonDataSource: FirmwareReleaseJsonDataSource,
@ -43,61 +44,50 @@ class FirmwareReleaseRepository @Inject constructor(
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
private fun getLatestFirmware(
releaseType: FirmwareReleaseType,
refresh: Boolean = false
): Flow<FirmwareRelease?> = flow {
if (refresh) {
invalidateCache()
} else {
val cachedRelease = localDataSource.getLatestRelease(releaseType)
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
debug("Using recent cached firmware release")
val externalModel = cachedRelease.asExternalModel()
private fun getLatestFirmware(releaseType: FirmwareReleaseType, refresh: Boolean = false): Flow<FirmwareRelease?> =
flow {
if (refresh) {
invalidateCache()
} else {
val cachedRelease = localDataSource.getLatestRelease(releaseType)
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
val externalModel = cachedRelease.asExternalModel()
emit(externalModel)
return@flow
}
}
try {
val networkFirmwareReleases =
apiDataSource.getFirmwareReleases() ?: throw IOException("empty response from server")
val releases =
when (releaseType) {
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(releases, releaseType)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
} catch (e: IOException) {
warn("Failed to fetch firmware releases from server: ${e.message}")
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
val releases =
when (releaseType) {
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(releases, releaseType)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
return@flow
}
}
try {
debug("Fetching firmware releases from server")
val networkFirmwareReleases = apiDataSource.getFirmwareReleases()
?: throw IOException("empty response from server")
val releases = when (releaseType) {
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(
releases,
releaseType
)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
} catch (e: IOException) {
warn("Failed to fetch firmware releases from server: ${e.message}")
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
val releases = when (releaseType) {
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(
releases,
releaseType
)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
}
}
suspend fun invalidateCache() {
localDataSource.deleteAllFirmwareReleases()
}
/**
* Check if the cache is expired
*/
private fun isCacheExpired(lastUpdated: Long): Boolean {
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}
/** Check if the cache is expired */
private fun isCacheExpired(lastUpdated: Long): Boolean =
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}

View file

@ -83,6 +83,7 @@ import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.setAttributes
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.Node
@ -306,6 +307,7 @@ fun MainScreen(
}
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun VersionChecks(viewModel: UIViewModel) {
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
@ -313,6 +315,10 @@ 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.collectAsState(DeviceVersion("2.6.4"))
LaunchedEffect(connectionState, firmwareEdition) {
if (connectionState == MeshService.ConnectionState.CONNECTED) {
@ -330,6 +336,15 @@ private fun VersionChecks(viewModel: UIViewModel) {
}
}
}
LaunchedEffect(connectionState, currentFirmwareVersion, currentDeviceHardware) {
if (connectionState == MeshService.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 == MeshService.ConnectionState.CONNECTED) {