Serialize downloads and make cache cleanup install-aware

- CacheWorker no longer purges the files of a download that is still in-flight
  or downloaded and awaiting install, so missing the system install prompt for
  longer than the cache window no longer forces a re-download.
- observeDownloads now starts the next queued download only once nothing else
  is purchasing/downloading/verifying, instead of merely checking for a
  DOWNLOADING row. This serializes downloads so concurrent workers can't clobber
  the shared foreground/progress notification.
This commit is contained in:
Rahul Patel
2026-05-30 00:27:51 +05:30
parent ada5a1f899
commit ab9b66eb94
3 changed files with 32 additions and 1 deletions

View File

@@ -81,7 +81,11 @@ class DownloadHelper @Inject constructor(
private fun observeDownloads() {
downloadDao.downloads().onEach { list ->
try {
if (list.none { it.status == DownloadStatus.DOWNLOADING }) {
// Serialize downloads: only start the next queued item once nothing else is
// actively purchasing/downloading/verifying. Previously this only checked for
// DOWNLOADING, so a worker in PURCHASING/VERIFYING didn't count and a second
// download could start concurrently and clobber the shared notification.
if (list.none { it.status in DownloadStatus.processing }) {
list.find { it.status == DownloadStatus.QUEUED }
?.let { queuedDownload ->
Log.i(TAG, "Enqueued download worker for ${queuedDownload.packageName}")

View File

@@ -16,5 +16,13 @@ enum class DownloadStatus(@StringRes val localized: Int) {
companion object {
val finished = listOf(FAILED, CANCELLED, COMPLETED)
val running = listOf(QUEUED, PURCHASING, DOWNLOADING)
/**
* States in which a download worker is actively occupying the (single) download
* slot — purchasing, transferring bytes or verifying. Used to serialize downloads:
* the next [QUEUED] item is only started once none of these are in progress, so
* concurrent workers can't clobber the shared foreground/progress notification.
*/
val processing = setOf(PURCHASING, DOWNLOADING, VERIFYING)
}
}

View File

@@ -8,6 +8,8 @@ import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.util.PathUtil
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -17,12 +19,14 @@ import java.util.concurrent.TimeUnit.HOURS
import java.util.concurrent.TimeUnit.MINUTES
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.flow.first
/**
* A periodic worker to automatically clear the old downloads cache periodically.
*/
@HiltWorker
class CacheWorker @AssistedInject constructor(
private val downloadDao: DownloadDao,
@Assisted private val context: Context,
@Assisted workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
@@ -58,6 +62,17 @@ class CacheWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
Log.i(TAG, "Cleaning cache")
// Files for downloads that are still in-flight or downloaded & awaiting install
// must be protected from the age-based purge, otherwise a download the user hasn't
// installed yet (e.g. they missed the system prompt) would lose its files and have
// to be re-downloaded. Keyed by packageName -> versionCode.
val protectedVersions = runCatching {
downloadDao.downloads().first()
.filter { it.isActive || it.status == DownloadStatus.COMPLETED }
.map { it.packageName to it.versionCode }
.toSet()
}.getOrDefault(emptySet())
PathUtil.getOldDownloadDirectories(context).filter { it.exists() }.forEach { dir ->
// Downloads
Log.i(TAG, "Deleting old unused download directory: $dir")
@@ -75,10 +90,14 @@ class CacheWorker @AssistedInject constructor(
download.listFiles()!!.forEach { versionCode ->
// 20240325
val isProtected = (download.name to versionCode.name.toLongOrNull()) in
protectedVersions
if (versionCode.listFiles().isNullOrEmpty()) {
// Purge empty non-accessible directory
Log.i(TAG, "Removing empty directory for ${download.name}, ${versionCode.name}")
versionCode.deleteRecursively()
} else if (isProtected) {
Log.i(TAG, "Keeping ${download.name} (${versionCode.name}); install pending")
} else {
versionCode.deleteIfOld()
}