Skip to content

Commit 5a413d0

Browse files
authored
fix: fdroid device hardware fallback using bundled JSON for incomplete cache entries (#3844)
1 parent d1e7bd1 commit 5a413d0

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

core/analytics/src/fdroid/kotlin/org/meshtastic/core/analytics/platform/FdroidPlatformAnalytics.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ import javax.inject.Inject
3030
*/
3131
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
3232
init {
33+
// For F-Droid builds we don't initialize external analytics services.
34+
// In debug builds we attach a DebugTree for convenient local logging.
3335
if (BuildConfig.DEBUG) {
3436
Timber.plant(Timber.DebugTree())
37+
Timber.i("F-Droid platform no-op analytics initialized (DebugTree planted).")
38+
} else {
39+
Timber.i("F-Droid platform no-op analytics initialized.")
3540
}
36-
Timber.i("F-Droid platform no-op analytics initialized.")
3741
}
3842

3943
override fun setDeviceAttributes(firmwareVersion: String, model: String) {

core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,54 +55,98 @@ constructor(
5555
*/
5656
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
5757
withContext(Dispatchers.IO) {
58+
Timber.d(
59+
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)",
60+
hwModel,
61+
forceRefresh,
62+
)
63+
5864
if (forceRefresh) {
65+
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
5966
localDataSource.deleteAllDeviceHardware()
6067
} else {
6168
// 1. Attempt to retrieve from cache first
6269
val cachedEntity = localDataSource.getByHwModel(hwModel)
6370
if (cachedEntity != null && !cachedEntity.isStale()) {
64-
Timber.d("Using fresh cached device hardware for model $hwModel")
71+
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
6572
return@withContext Result.success(cachedEntity.asExternalModel())
6673
}
74+
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
6775
}
6876

6977
// 2. Fetch from remote API
7078
runCatching {
71-
Timber.d("Fetching device hardware from remote API.")
79+
Timber.d("DeviceHardwareRepository: fetching device hardware from remote API")
7280
val remoteHardware = remoteDataSource.getAllDeviceHardware()
81+
Timber.d(
82+
"DeviceHardwareRepository: remote API returned %d device hardware entries",
83+
remoteHardware.size,
84+
)
7385

7486
localDataSource.insertAllDeviceHardware(remoteHardware)
75-
localDataSource.getByHwModel(hwModel)?.asExternalModel()
87+
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
88+
Timber.d(
89+
"DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s",
90+
hwModel,
91+
if (fromDb != null) "succeeded" else "returned null",
92+
)
93+
fromDb
7694
}
7795
.onSuccess {
7896
// Successfully fetched and found the model
7997
return@withContext Result.success(it)
8098
}
8199
.onFailure { e ->
82-
Timber.w("Failed to fetch device hardware from server: ${e.message}")
100+
Timber.w(
101+
e,
102+
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d",
103+
hwModel,
104+
)
83105

84-
// 3. Attempt to use stale cache as a fallback
106+
// 3. Attempt to use stale cache as a fallback, but only if it looks complete.
85107
val staleEntity = localDataSource.getByHwModel(hwModel)
86-
if (staleEntity != null) {
87-
Timber.d("Using stale cached device hardware for model $hwModel")
108+
if (staleEntity != null && !staleEntity.isIncomplete()) {
109+
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
88110
return@withContext Result.success(staleEntity.asExternalModel())
89111
}
90112

91-
// 4. Fallback to bundled JSON if cache is empty
92-
Timber.d("Cache is empty, falling back to bundled JSON asset.")
113+
// 4. Fallback to bundled JSON if cache is empty or incomplete
114+
Timber.d(
115+
"DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset",
116+
if (staleEntity == null) "empty" else "incomplete",
117+
hwModel,
118+
)
93119
return@withContext loadFromBundledJson(hwModel)
94120
}
95121
}
96122

97123
private suspend fun loadFromBundledJson(hwModel: Int): Result<DeviceHardware?> = runCatching {
124+
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
98125
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
126+
Timber.d("DeviceHardwareRepository: bundled JSON returned %d device hardware entries", jsonHardware.size)
127+
99128
localDataSource.insertAllDeviceHardware(jsonHardware)
100-
localDataSource.getByHwModel(hwModel)?.asExternalModel()
129+
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
130+
Timber.d(
131+
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
132+
hwModel,
133+
if (fromDb != null) "succeeded" else "returned null",
134+
)
135+
fromDb
101136
}
102137

103-
/** Extension function to check if the cached entity is stale. */
138+
/** Returns true if the cached entity is missing important fields and should be refreshed. */
139+
private fun DeviceHardwareEntity.isIncomplete(): Boolean =
140+
displayName.isBlank() || platformioTarget.isBlank() || images.isNullOrEmpty()
141+
142+
/**
143+
* Extension function to check if the cached entity is stale.
144+
*
145+
* We treat entries with missing critical fields (e.g., no images or target) as stale so that they can be
146+
* automatically healed from newer JSON snapshots even if their timestamp is recent.
147+
*/
104148
private fun DeviceHardwareEntity.isStale(): Boolean =
105-
(System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
149+
isIncomplete() || (System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
106150

107151
companion object {
108152
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.DAYS.toMillis(1)

0 commit comments

Comments
 (0)