[db] track and clear repo update errors

Historically, we've just shown a giant Toast for repo update errors, now we track them in the DB, so we can properly expose them in the UI.
This commit is contained in:
Torsten Grote
2025-12-12 12:15:42 -03:00
parent f81060859d
commit dea8a2e504
7 changed files with 75 additions and 7 deletions

View File

@@ -129,7 +129,14 @@ internal class IndexV1UpdaterTest : DbTest() {
assertIs<SigningException>(result.e)
// check that the DB transaction was rolled back and the DB wasn't changed
assertEquals(repo, repoDao.getRepository(repoId) ?: fail())
// except for adding the error to the repo
val expectedRepo = repo.copy(
preferences = repo.preferences.copy(
errorCount = 1,
lastError = "Signing certificate does not match"
),
)
assertEquals(expectedRepo, repoDao.getRepository(repoId) ?: fail())
assertEquals(0, appDao.countApps())
assertEquals(0, versionDao.countAppVersions())
}

View File

@@ -111,6 +111,7 @@ public data class Repository internal constructor(
lastUpdated: Long,
username: String? = null,
password: String? = null,
lastError: String? = null,
) : this(
repository = CoreRepository(
repoId = repoId,
@@ -132,6 +133,7 @@ public data class Repository internal constructor(
lastUpdated = lastUpdated,
username = username,
password = password,
lastError = lastError,
)
)
@@ -232,6 +234,8 @@ public data class Repository internal constructor(
get() {
return "https://fdroid.link/#$address?fingerprint=$fingerprint"
}
public val errorCount: Int get() = preferences.errorCount
public val lastError: String? get() = preferences.lastError
}
// Dummy repo to use in Compose Previews and in tests

View File

@@ -442,6 +442,21 @@ internal interface RepositoryDaoInt : RepositoryDao {
)
fun getArchiveRepoId(cert: String): Long?
@Query(
"""UPDATE ${RepositoryPreferences.TABLE}
SET errorCount = errorCount + 1, lastError = :errorMsg
WHERE repoId = :repoId
"""
)
fun trackRepoUpdateError(repoId: Long, errorMsg: String)
@Query(
"""UPDATE ${RepositoryPreferences.TABLE}
SET errorCount = 0, lastError = NULL WHERE repoId = :repoId
"""
)
fun resetRepoUpdateError(repoId: Long)
@Transaction
override fun deleteRepository(repoId: Long) {
deleteCoreRepository(repoId)

View File

@@ -1,7 +1,10 @@
package org.fdroid.index
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.Downloader
import org.fdroid.download.NotFoundException
import java.io.File
@@ -48,6 +51,8 @@ public fun interface TempFileProvider {
* A class to update information of a [Repository] in the database with a new downloaded index.
*/
public abstract class IndexUpdater {
@VisibleForTesting
internal abstract val repoDao: RepositoryDaoInt
/**
* The [IndexFormatVersion] used by this updater.
@@ -59,21 +64,46 @@ public abstract class IndexUpdater {
/**
* Updates an existing [repo] with a known [Repository.certificate].
*/
public fun update(repo: Repository): IndexUpdateResult = catchExceptions {
updateRepo(repo)
@WorkerThread
public fun update(repo: Repository): IndexUpdateResult = catchExceptions(repo) {
updateRepo(repo).also { result ->
// reset repo errors if repo updated fine again, but is still unchanged
if (repo.errorCount > 0 && result is IndexUpdateResult.Unchanged) {
repoDao.resetRepoUpdateError(repo.repoId)
}
}
}
private fun catchExceptions(block: () -> IndexUpdateResult): IndexUpdateResult {
@WorkerThread
protected abstract fun updateRepo(repo: Repository): IndexUpdateResult
@WorkerThread
private fun catchExceptions(
repo: Repository,
block: () -> IndexUpdateResult,
): IndexUpdateResult {
return try {
block()
} catch (e: NotFoundException) {
onError(repo.repoId, e)
IndexUpdateResult.NotFound
} catch (e: Exception) {
onError(repo.repoId, e)
IndexUpdateResult.Error(e)
}
}
protected abstract fun updateRepo(repo: Repository): IndexUpdateResult
@WorkerThread
private fun onError(repoId: Long, e: Exception) {
val msg = buildString {
append(e.localizedMessage ?: e.message ?: e.javaClass.simpleName)
e.cause?.let { cause ->
append("\n")
append(cause.localizedMessage ?: cause.message ?: cause.javaClass.simpleName)
}
}
repoDao.trackRepoUpdateError(repoId, msg)
}
}
internal fun Downloader.setIndexUpdateListener(

View File

@@ -8,6 +8,7 @@ import org.fdroid.database.DbV1StreamReceiver
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.FDroidDatabaseInt
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.DownloaderFactory
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.IndexFormatVersion.ONE
@@ -34,6 +35,7 @@ public class IndexV1Updater(
private val log = KotlinLogging.logger {}
public override val formatVersion: IndexFormatVersion = ONE
private val db: FDroidDatabaseInt = database as FDroidDatabaseInt
override val repoDao: RepositoryDaoInt = db.getRepositoryDao()
override fun updateRepo(repo: Repository): IndexUpdateResult {
// Normally, we shouldn't allow repository downgrades and assert the condition below.
@@ -66,10 +68,11 @@ public class IndexV1Updater(
streamProcessor.process(inputStream)
}
// update RepositoryPreferences with timestamp and ETag (for v1)
val repoDao = db.getRepositoryDao()
val updatedPrefs = repo.preferences.copy(
lastUpdated = System.currentTimeMillis(),
lastETag = eTag,
errorCount = 0,
lastError = null,
)
repoDao.updateRepositoryPreferences(updatedPrefs)
}

View File

@@ -6,6 +6,7 @@ import org.fdroid.database.DbV2StreamReceiver
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.FDroidDatabaseInt
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.DownloaderFactory
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.IndexFormatVersion.ONE
@@ -33,6 +34,7 @@ public class IndexV2Updater(
public override val formatVersion: IndexFormatVersion = TWO
private val db: FDroidDatabaseInt = database as FDroidDatabaseInt
override val repoDao: RepositoryDaoInt = db.getRepositoryDao()
override fun updateRepo(repo: Repository): IndexUpdateResult {
val (_, entry) = getCertAndEntry(repo, repo.certificate)
@@ -92,7 +94,6 @@ public class IndexV2Updater(
try {
downloader.download()
file.inputStream().use { inputStream ->
val repoDao = db.getRepositoryDao()
db.runInTransaction {
// ensure somebody else hasn't updated the repo in the meantime
val currentTimestamp = repoDao.getRepository(repo.repoId)?.timestamp
@@ -108,6 +109,8 @@ public class IndexV2Updater(
?: error("No repo prefs for ${repo.repoId}")
val updatedPrefs = repoPrefs.copy(
lastUpdated = System.currentTimeMillis(),
errorCount = 0,
lastError = null,
)
repoDao.updateRepositoryPreferences(updatedPrefs)
}

View File

@@ -190,6 +190,8 @@ internal class RepoAdderTest {
assertEquals(expectedResult, fetching.fetchResult)
every { newRepo.formatVersion } returns IndexFormatVersion.TWO
every { newRepo.repoId } returns 1
every { repoDao.trackRepoUpdateError(1, any()) } just Runs
repoAdder.addFetchedRepository()
@@ -225,6 +227,8 @@ internal class RepoAdderTest {
assertIs<IsNewRepository>(fetching.fetchResult)
every { newRepo.formatVersion } returns IndexFormatVersion.TWO
every { newRepo.repoId } returns 1
every { repoDao.trackRepoUpdateError(1, any()) } just Runs
repoAdder.addFetchedRepository()
@@ -852,6 +856,8 @@ internal class RepoAdderTest {
assertIs<Fetching>(awaitItem()) // still Fetching from last call
every { newRepo.formatVersion } returns IndexFormatVersion.TWO
every { newRepo.repoId } returns 1
every { repoDao.trackRepoUpdateError(1, any()) } just Runs
repoAdder.addFetchedRepository()
assertIs<Adding>(awaitItem()) // now moved to Adding