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() {
|
private fun observeDownloads() {
|
||||||
downloadDao.downloads().onEach { list ->
|
downloadDao.downloads().onEach { list ->
|
||||||
try {
|
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 }
|
list.find { it.status == DownloadStatus.QUEUED }
|
||||||
?.let { queuedDownload ->
|
?.let { queuedDownload ->
|
||||||
Log.i(TAG, "Enqueued download worker for ${queuedDownload.packageName}")
|
Log.i(TAG, "Enqueued download worker for ${queuedDownload.packageName}")
|
||||||
|
|||||||
@@ -16,5 +16,13 @@ enum class DownloadStatus(@StringRes val localized: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
||||||
val running = listOf(QUEUED, PURCHASING, DOWNLOADING)
|
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.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
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 com.aurora.store.util.PathUtil
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
@@ -17,12 +19,14 @@ import java.util.concurrent.TimeUnit.HOURS
|
|||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A periodic worker to automatically clear the old downloads cache periodically.
|
* A periodic worker to automatically clear the old downloads cache periodically.
|
||||||
*/
|
*/
|
||||||
@HiltWorker
|
@HiltWorker
|
||||||
class CacheWorker @AssistedInject constructor(
|
class CacheWorker @AssistedInject constructor(
|
||||||
|
private val downloadDao: DownloadDao,
|
||||||
@Assisted private val context: Context,
|
@Assisted private val context: Context,
|
||||||
@Assisted workerParams: WorkerParameters
|
@Assisted workerParams: WorkerParameters
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
@@ -58,6 +62,17 @@ class CacheWorker @AssistedInject constructor(
|
|||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.i(TAG, "Cleaning cache")
|
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 ->
|
PathUtil.getOldDownloadDirectories(context).filter { it.exists() }.forEach { dir ->
|
||||||
// Downloads
|
// Downloads
|
||||||
Log.i(TAG, "Deleting old unused download directory: $dir")
|
Log.i(TAG, "Deleting old unused download directory: $dir")
|
||||||
@@ -75,10 +90,14 @@ class CacheWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
download.listFiles()!!.forEach { versionCode ->
|
download.listFiles()!!.forEach { versionCode ->
|
||||||
// 20240325
|
// 20240325
|
||||||
|
val isProtected = (download.name to versionCode.name.toLongOrNull()) in
|
||||||
|
protectedVersions
|
||||||
if (versionCode.listFiles().isNullOrEmpty()) {
|
if (versionCode.listFiles().isNullOrEmpty()) {
|
||||||
// Purge empty non-accessible directory
|
// Purge empty non-accessible directory
|
||||||
Log.i(TAG, "Removing empty directory for ${download.name}, ${versionCode.name}")
|
Log.i(TAG, "Removing empty directory for ${download.name}, ${versionCode.name}")
|
||||||
versionCode.deleteRecursively()
|
versionCode.deleteRecursively()
|
||||||
|
} else if (isProtected) {
|
||||||
|
Log.i(TAG, "Keeping ${download.name} (${versionCode.name}); install pending")
|
||||||
} else {
|
} else {
|
||||||
versionCode.deleteIfOld()
|
versionCode.deleteIfOld()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user