DownloadWorker: Move downloads observing logic into worker as well

This allows us to keep all the modifications related to DownloadWorker in a
single place.

Also, drop the 3seconds delay and do the cleanup instead of delegating it to
worker if last job was failed. This is better and more error-proof.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-11-30 13:26:47 +05:30
parent 77738d8549
commit b2cbc8dc16
5 changed files with 66 additions and 63 deletions

View File

@@ -185,7 +185,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
//WorkManager //WorkManager
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.9.0-rc01")
// LeakCanary // LeakCanary
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")

View File

@@ -28,15 +28,3 @@ fun <T> MutableSet<T>.flushAndAdd(list: Set<T>) {
clear() clear()
addAll(list) addAll(list)
} }
fun <T> MutableSet<T>.copyAndAdd(element: T): MutableSet<T> {
val newSet = this.toMutableSet()
newSet.add(element)
return newSet
}
fun <T> MutableSet<T>.copyAndRemove(element: T): MutableSet<T> {
val newSet = this.toMutableSet()
newSet.remove(element)
return newSet
}

View File

@@ -27,11 +27,11 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.receiver.PackageManagerReceiver import com.aurora.store.data.receiver.PackageManagerReceiver
import com.aurora.store.data.service.NotificationService import com.aurora.store.data.service.NotificationService
import com.aurora.store.data.work.DownloadWorker
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.Fetch
import kotlinx.coroutines.flow.MutableStateFlow
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
class AuroraApplication : Application() { class AuroraApplication : Application() {
@@ -39,7 +39,7 @@ class AuroraApplication : Application() {
private lateinit var fetch: Fetch private lateinit var fetch: Fetch
companion object{ companion object{
val enqueuedDownloads = MutableStateFlow<MutableSet<App>>(mutableSetOf()) val enqueuedDownloads = mutableSetOf<App>()
val enqueuedInstalls: MutableSet<String> = mutableSetOf() val enqueuedInstalls: MutableSet<String> = mutableSetOf()
} }
@@ -58,6 +58,9 @@ class AuroraApplication : Application() {
fetch = DownloadManager.with(this).fetch fetch = DownloadManager.with(this).fetch
// Initialize DownloadWorker to observe and trigger downloads
DownloadWorker.initDownloadWorker(applicationContext)
//Register broadcast receiver for package install/uninstall //Register broadcast receiver for package install/uninstall
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
this, this,

View File

@@ -59,7 +59,6 @@ import com.aurora.extensions.toast
import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.model.SelfUpdate import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.data.work.DownloadWorker
import com.aurora.store.databinding.ActivityMainBinding import com.aurora.store.databinding.ActivityMainBinding
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
@@ -70,8 +69,6 @@ import com.aurora.store.view.ui.sheets.SelfUpdateSheet
import com.aurora.store.viewmodel.MainViewModel import com.aurora.store.viewmodel.MainViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -217,22 +214,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
// Handle enqueued downloads
GlobalScope.launch {
AuroraApplication.enqueuedDownloads.collect { enqueuedDownloads ->
try {
if (enqueuedDownloads.isNotEmpty()) {
// Let any existing work related to download/install get finished
delay(3000)
Log.i("Downloading ${enqueuedDownloads.first().packageName}")
DownloadWorker.downloadApp(applicationContext, enqueuedDownloads.first())
}
} catch (exception: Exception) {
Log.i("Failed to download enqueued apps", exception)
}
}
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@@ -12,13 +12,12 @@ import androidx.work.ExistingWorkPolicy.KEEP
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.copyAndAdd
import com.aurora.extensions.copyTo import com.aurora.extensions.copyTo
import com.aurora.extensions.isQAndAbove import com.aurora.extensions.isQAndAbove
import com.aurora.extensions.copyAndRemove
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.AuroraApplication import com.aurora.store.AuroraApplication
@@ -36,7 +35,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import kotlinx.coroutines.flow.update import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.deleteRecursively import kotlin.io.path.deleteRecursively
@@ -58,16 +58,54 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
private const val DOWNLOAD_APP = "DOWNLOAD_APP" private const val DOWNLOAD_APP = "DOWNLOAD_APP"
private const val DOWNLOAD_UPDATE = "DOWNLOAD_UPDATE" private const val DOWNLOAD_UPDATE = "DOWNLOAD_UPDATE"
fun isEnqueued(packageName: String): Boolean { private const val notificationID = 200
return AuroraApplication.enqueuedDownloads.value.any { it.packageName == packageName } private val cleanupStates = listOf(
WorkInfo.State.CANCELLED,
WorkInfo.State.FAILED
)
fun initDownloadWorker(applicationContext: Context) {
GlobalScope.launch {
WorkManager.getInstance(applicationContext)
.getWorkInfosByTagFlow(DOWNLOAD_WORKER)
.collectLatest { downloadsList ->
try {
if (downloadsList.all { it.state.isFinished }) {
// Do cleanup for last download if required
downloadsList.getOrNull(0)?.let { workInfo ->
if (workInfo.state in cleanupStates) {
onFailure(
applicationContext,
AuroraApplication.enqueuedDownloads.first()
)
}
}
// Check and trigger download if enqueue list is not empty
if (AuroraApplication.enqueuedDownloads.isNotEmpty()) {
val app = AuroraApplication.enqueuedDownloads.first()
Log.i(DOWNLOAD_WORKER, "Downloading ${app.packageName}")
downloadApp(applicationContext, app)
}
}
} catch (exception: Exception) {
Log.i(DOWNLOAD_WORKER, "Failed to download enqueued apps", exception)
}
}
}
} }
fun enqueueApp(app: App) { fun isEnqueued(packageName: String): Boolean {
AuroraApplication.enqueuedDownloads.update { it.copyAndAdd(app) } return AuroraApplication.enqueuedDownloads.any { it.packageName == packageName }
}
fun enqueueApp(context: Context, app: App) {
if (AuroraApplication.enqueuedDownloads.isEmpty()) downloadApp(context, app)
AuroraApplication.enqueuedDownloads.add(app)
} }
fun cancelDownload(context: Context, app: App) { fun cancelDownload(context: Context, app: App) {
AuroraApplication.enqueuedDownloads.update {it.copyAndRemove(app) }
WorkManager.getInstance(context).cancelAllWorkByTag(app.packageName) WorkManager.getInstance(context).cancelAllWorkByTag(app.packageName)
} }
@@ -78,13 +116,7 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
if (updates) workManager.cancelAllWorkByTag(DOWNLOAD_UPDATE) if (updates) workManager.cancelAllWorkByTag(DOWNLOAD_UPDATE)
} }
/** private fun downloadApp(context: Context, app: App) {
* Downloads and install an [App]
*
* Triggers Immediate downloads and installation. In most cases, you don't need to call
* this method. Consider using [enqueueApp] instead.
*/
fun downloadApp(context: Context, app: App) {
val work = OneTimeWorkRequestBuilder<DownloadWorker>() val work = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag(DOWNLOAD_WORKER) .addTag(DOWNLOAD_WORKER)
.addTag(app.packageName) .addTag(app.packageName)
@@ -98,6 +130,17 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueueUniqueWork("${DOWNLOAD_WORKER}/${app.packageName}", KEEP, work) .enqueueUniqueWork("${DOWNLOAD_WORKER}/${app.packageName}", KEEP, work)
} }
@OptIn(ExperimentalPathApi::class)
private fun onFailure(context: Context, app: App) {
Log.i(DOWNLOAD_WORKER, "Cleaning up!")
PathUtil.getAppDownloadDir(context, app.packageName, app.versionCode)
.deleteRecursively()
with (context.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager) {
cancel(notificationID)
}
AuroraApplication.enqueuedDownloads.remove(app)
}
} }
private lateinit var app: App private lateinit var app: App
@@ -109,7 +152,6 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
private var downloadedBytes = 0L private var downloadedBytes = 0L
private val TAG = DownloadWorker::class.java.simpleName private val TAG = DownloadWorker::class.java.simpleName
private val notificationID = 200
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
// Purchase the app (free apps needs to be purchased too) // Purchase the app (free apps needs to be purchased too)
@@ -122,7 +164,7 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
// Try to parse input data into a valid app // Try to parse input data into a valid app
try { try {
app = AuroraApplication.enqueuedDownloads.value.first() app = AuroraApplication.enqueuedDownloads.first()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "No apps enqueued for downloads", exception) Log.e(TAG, "No apps enqueued for downloads", exception)
return Result.failure() return Result.failure()
@@ -155,13 +197,11 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
.onFailure { .onFailure {
Log.e(TAG, "Failed to download ${app.packageName}", it) Log.e(TAG, "Failed to download ${app.packageName}", it)
downloading = false downloading = false
onFailure()
return Result.failure() return Result.failure()
} }
while (downloading) { while (downloading) {
delay(1000) delay(1000)
if (isStopped) { if (isStopped) {
onFailure()
break break
} }
} }
@@ -182,19 +222,10 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
} }
// Remove the app from the list // Remove the app from the list
AuroraApplication.enqueuedDownloads.update { it.copyAndRemove(app) } AuroraApplication.enqueuedDownloads.remove(app)
return Result.success() return Result.success()
} }
@OptIn(ExperimentalPathApi::class)
private fun onFailure() {
Log.i(TAG, "Cleaning up!")
PathUtil.getAppDownloadDir(appContext, app.packageName, app.versionCode)
.deleteRecursively()
notificationManager.cancel(notificationID)
AuroraApplication.enqueuedDownloads.update { it.copyAndRemove(app) }
}
private fun getDownloadRequest(files: List<GPlayFile>): List<Request> { private fun getDownloadRequest(files: List<GPlayFile>): List<Request> {
val downloadList = mutableListOf<Request>() val downloadList = mutableListOf<Request>()
files.filter { it.url.isNotBlank() }.forEach { files.filter { it.url.isNotBlank() }.forEach {