mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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:
parent
f5478b42c3
commit
ab22a655c4
18 changed files with 348 additions and 194 deletions
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue