diff --git a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt index be720efa1..663d640e4 100644 --- a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt @@ -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}") diff --git a/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt b/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt index da3dada10..36b34c089 100644 --- a/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt +++ b/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt @@ -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) } } diff --git a/app/src/main/java/com/aurora/store/data/work/CacheWorker.kt b/app/src/main/java/com/aurora/store/data/work/CacheWorker.kt index 076f0db54..5e6fcb60c 100644 --- a/app/src/main/java/com/aurora/store/data/work/CacheWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/CacheWorker.kt @@ -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() }