fix: fdroid device hardware fallback using bundled JSON for incomplete cache entries (#3844)

This commit is contained in:
Mac DeCourcy
2025-11-28 12:45:22 -08:00
committed by GitHub
parent d1e7bd1ad5
commit 5a413d07e3
2 changed files with 61 additions and 13 deletions

View File

@@ -30,10 +30,14 @@ import javax.inject.Inject
*/
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
init {
// For F-Droid builds we don't initialize external analytics services.
// In debug builds we attach a DebugTree for convenient local logging.
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Timber.i("F-Droid platform no-op analytics initialized (DebugTree planted).")
} else {
Timber.i("F-Droid platform no-op analytics initialized.")
}
Timber.i("F-Droid platform no-op analytics initialized.")
}
override fun setDeviceAttributes(firmwareVersion: String, model: String) {

View File

@@ -55,54 +55,98 @@ constructor(
*/
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
withContext(Dispatchers.IO) {
Timber.d(
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)",
hwModel,
forceRefresh,
)
if (forceRefresh) {
Timber.d("DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache")
localDataSource.deleteAllDeviceHardware()
} else {
// 1. Attempt to retrieve from cache first
val cachedEntity = localDataSource.getByHwModel(hwModel)
if (cachedEntity != null && !cachedEntity.isStale()) {
Timber.d("Using fresh cached device hardware for model $hwModel")
Timber.d("DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d", hwModel)
return@withContext Result.success(cachedEntity.asExternalModel())
}
Timber.d("DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch", hwModel)
}
// 2. Fetch from remote API
runCatching {
Timber.d("Fetching device hardware from remote API.")
Timber.d("DeviceHardwareRepository: fetching device hardware from remote API")
val remoteHardware = remoteDataSource.getAllDeviceHardware()
Timber.d(
"DeviceHardwareRepository: remote API returned %d device hardware entries",
remoteHardware.size,
)
localDataSource.insertAllDeviceHardware(remoteHardware)
localDataSource.getByHwModel(hwModel)?.asExternalModel()
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s",
hwModel,
if (fromDb != null) "succeeded" else "returned null",
)
fromDb
}
.onSuccess {
// Successfully fetched and found the model
return@withContext Result.success(it)
}
.onFailure { e ->
Timber.w("Failed to fetch device hardware from server: ${e.message}")
Timber.w(
e,
"DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d",
hwModel,
)
// 3. Attempt to use stale cache as a fallback
// 3. Attempt to use stale cache as a fallback, but only if it looks complete.
val staleEntity = localDataSource.getByHwModel(hwModel)
if (staleEntity != null) {
Timber.d("Using stale cached device hardware for model $hwModel")
if (staleEntity != null && !staleEntity.isIncomplete()) {
Timber.d("DeviceHardwareRepository: using stale cached device hardware for hwModel=%d", hwModel)
return@withContext Result.success(staleEntity.asExternalModel())
}
// 4. Fallback to bundled JSON if cache is empty
Timber.d("Cache is empty, falling back to bundled JSON asset.")
// 4. Fallback to bundled JSON if cache is empty or incomplete
Timber.d(
"DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset",
if (staleEntity == null) "empty" else "incomplete",
hwModel,
)
return@withContext loadFromBundledJson(hwModel)
}
}
private suspend fun loadFromBundledJson(hwModel: Int): Result<DeviceHardware?> = runCatching {
Timber.d("DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d", hwModel)
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
Timber.d("DeviceHardwareRepository: bundled JSON returned %d device hardware entries", jsonHardware.size)
localDataSource.insertAllDeviceHardware(jsonHardware)
localDataSource.getByHwModel(hwModel)?.asExternalModel()
val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
Timber.d(
"DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s",
hwModel,
if (fromDb != null) "succeeded" else "returned null",
)
fromDb
}
/** Extension function to check if the cached entity is stale. */
/** Returns true if the cached entity is missing important fields and should be refreshed. */
private fun DeviceHardwareEntity.isIncomplete(): Boolean =
displayName.isBlank() || platformioTarget.isBlank() || images.isNullOrEmpty()
/**
* Extension function to check if the cached entity is stale.
*
* We treat entries with missing critical fields (e.g., no images or target) as stale so that they can be
* automatically healed from newer JSON snapshots even if their timestamp is recent.
*/
private fun DeviceHardwareEntity.isStale(): Boolean =
(System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
isIncomplete() || (System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
companion object {
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.DAYS.toMillis(1)