DownloadWorker: Restrict concurrent downloads to one
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -27,4 +27,16 @@ fun <T> MutableList<T>.flushAndAdd(list: List<T>) {
|
|||||||
fun <T> MutableSet<T>.flushAndAdd(list: Set<T>) {
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user