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:
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user