DownloadWorker: Restrict concurrent downloads to one

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-11-01 17:48:50 +05:30
parent dcaf4f024a
commit 26c4749047
8 changed files with 63 additions and 41 deletions

View File

@@ -28,3 +28,15 @@ 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

@@ -23,6 +23,7 @@ package com.aurora.store
import android.app.Application import android.app.Application
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isPAndAbove
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
@@ -30,6 +31,7 @@ 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() {
@@ -37,6 +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 enqueuedInstalls: MutableSet<String> = mutableSetOf() val enqueuedInstalls: MutableSet<String> = mutableSetOf()
} }

View File

@@ -59,8 +59,8 @@ 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.CertUtil
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
@@ -69,6 +69,9 @@ import com.aurora.store.view.ui.sheets.NetworkDialogSheet
import com.aurora.store.view.ui.sheets.SelfUpdateSheet 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.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -92,6 +95,7 @@ class MainActivity : AppCompatActivity() {
R.id.updatesFragment R.id.updatesFragment
) )
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyThemeAccent() applyThemeAccent()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -213,6 +217,22 @@ 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

@@ -46,11 +46,7 @@ class DownloadManager private constructor(var context: Context) {
} }
private fun getFetchConfiguration(context: Context): FetchConfiguration { private fun getFetchConfiguration(context: Context): FetchConfiguration {
var maxActive = Preferences.getInteger(context, Preferences.PREFERENCE_DOWNLOAD_ACTIVE)
if (maxActive == 0)
maxActive = 3
return FetchConfiguration.Builder(context) return FetchConfiguration.Builder(context)
.setDownloadConcurrentLimit(maxActive)
.enableLogging(BuildConfig.DEBUG) .enableLogging(BuildConfig.DEBUG)
.enableHashCheck(true) .enableHashCheck(true)
.enableFileExistChecks(true) .enableFileExistChecks(true)

View File

@@ -8,17 +8,20 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.util.Log import android.util.Log
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy.REPLACE 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.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.data.model.DownloadStatus import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.Request import com.aurora.store.data.model.Request
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
@@ -26,13 +29,13 @@ import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.receiver.InstallReceiver import com.aurora.store.data.receiver.InstallReceiver
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest 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 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
@@ -42,25 +45,27 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
CoroutineWorker(appContext, workerParams) { CoroutineWorker(appContext, workerParams) {
companion object { companion object {
const val DOWNLOAD_DATA = "DOWNLOAD_DATA" const val DOWNLOAD_WORKER = "DOWNLOAD_WORKER"
const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS" const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS"
private const val TAG = "DownloadWorker" fun enqueueApp(app: App) {
AuroraApplication.enqueuedDownloads.update { it.copyAndAdd(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) { fun downloadApp(context: Context, app: App) {
Log.i(TAG, "Downloading ${app.packageName}")
val downloadData = Data.Builder()
.putString(DOWNLOAD_DATA, Gson().toJson(app))
.build()
val work = OneTimeWorkRequestBuilder<DownloadWorker>() val work = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(downloadData) .addTag(app.packageName)
.addTag(app.versionCode.toString()) .addTag(app.versionCode.toString())
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
.build() .build()
WorkManager.getInstance(context).enqueueUniqueWork(app.packageName, REPLACE, work) WorkManager.getInstance(context).enqueueUniqueWork(DOWNLOAD_WORKER, KEEP, work)
} }
} }
@@ -71,8 +76,6 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
private val TAG = DownloadWorker::class.java.simpleName private val TAG = DownloadWorker::class.java.simpleName
private val notificationID = 200 private val notificationID = 200
private val gson = Gson()
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)
val authData = AuthProvider.with(appContext).getAuthData() val authData = AuthProvider.with(appContext).getAuthData()
@@ -83,14 +86,11 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
// Try to parse input data into a valid app // Try to parse input data into a valid app
withContext(Dispatchers.Default) { try {
try { app = AuroraApplication.enqueuedDownloads.value.first()
app = gson.fromJson(inputData.getString(DOWNLOAD_DATA), App::class.java) } catch (exception: Exception) {
} catch (exception: Exception) { Log.e(TAG, "No apps enqueued for downloads", exception)
Log.e(TAG, "Failed parsing requested app!", exception) return Result.failure()
notifyStatus(DownloadStatus.FAILED)
return@withContext Result.failure()
}
} }
// Set work/service to foreground on < Android 12.0 // Set work/service to foreground on < Android 12.0
@@ -140,6 +140,9 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
appContext.sendBroadcast(it) appContext.sendBroadcast(it)
} }
// Remove the app from the list
AuroraApplication.enqueuedDownloads.update { it.copyAndRemove(app) }
return Result.success() return Result.success()
} }
@@ -149,6 +152,7 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
PathUtil.getAppDownloadDir(appContext, app.packageName, app.versionCode) PathUtil.getAppDownloadDir(appContext, app.packageName, app.versionCode)
.deleteRecursively() .deleteRecursively()
notificationManager.cancel(notificationID) 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> {

View File

@@ -48,7 +48,6 @@ object Preferences {
const val INSTALLATION_ABANDON_SESSION = "INSTALLATION_ABANDON_SESSION" const val INSTALLATION_ABANDON_SESSION = "INSTALLATION_ABANDON_SESSION"
const val PREFERENCE_DOWNLOAD_ACTIVE = "PREFERENCE_DOWNLOAD_ACTIVE"
const val PREFERENCE_DOWNLOAD_EXTERNAL = "PREFERENCE_DOWNLOAD_EXTERNAL" const val PREFERENCE_DOWNLOAD_EXTERNAL = "PREFERENCE_DOWNLOAD_EXTERNAL"
const val PREFERENCE_DOWNLOAD_DIRECTORY = "PREFERENCE_DOWNLOAD_DIRECTORY" const val PREFERENCE_DOWNLOAD_DIRECTORY = "PREFERENCE_DOWNLOAD_DIRECTORY"
const val PREFERENCE_DOWNLOAD_WIFI_ONLY = "PREFERENCE_DOWNLOAD_WIFI_ONLY" const val PREFERENCE_DOWNLOAD_WIFI_ONLY = "PREFERENCE_DOWNLOAD_WIFI_ONLY"

View File

@@ -38,7 +38,6 @@ import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_ACTIVE
import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_DIRECTORY import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_DIRECTORY
import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_EXTERNAL import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_EXTERNAL
import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_WIFI_ONLY import com.aurora.store.util.Preferences.PREFERENCE_DOWNLOAD_WIFI_ONLY
@@ -165,7 +164,6 @@ class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {
save(PREFERENCE_FILTER_SEARCH, true) save(PREFERENCE_FILTER_SEARCH, true)
/*Downloader*/ /*Downloader*/
save(PREFERENCE_DOWNLOAD_ACTIVE, 3)
save(PREFERENCE_DOWNLOAD_EXTERNAL, false) save(PREFERENCE_DOWNLOAD_EXTERNAL, false)
save(PREFERENCE_DOWNLOAD_DIRECTORY, PathUtil.getExternalPath(requireContext())) save(PREFERENCE_DOWNLOAD_DIRECTORY, PathUtil.getExternalPath(requireContext()))
save(PREFERENCE_DOWNLOAD_WIFI_ONLY, false) save(PREFERENCE_DOWNLOAD_WIFI_ONLY, false)

View File

@@ -47,14 +47,4 @@
app:key="PREFERENCE_DOWNLOAD_WIFI_ONLY" app:key="PREFERENCE_DOWNLOAD_WIFI_ONLY"
app:title="@string/pref_downloader_wifi_title" /> app:title="@string/pref_downloader_wifi_title" />
<SeekBarPreference
android:defaultValue="3"
android:key="PREFERENCE_DOWNLOAD_ACTIVE"
android:max="12"
android:summary="@string/pref_downloader_active_summary"
android:title="@string/pref_downloader_active_title"
app:adjustable="true"
app:iconSpaceReserved="false"
app:min="1"
app:showSeekBarValue="true" />
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>