DownloadWorkerUtil: Use room database to store downloads
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -193,6 +193,16 @@ dependencies {
|
|||||||
ksp("com.google.dagger:hilt-android-compiler:$hiltVersion")
|
ksp("com.google.dagger:hilt-android-compiler:$hiltVersion")
|
||||||
implementation("com.google.dagger:hilt-android:$hiltVersion")
|
implementation("com.google.dagger:hilt-android:$hiltVersion")
|
||||||
|
|
||||||
|
val hiltWorkVersion = "1.1.0"
|
||||||
|
ksp("androidx.hilt:hilt-compiler:$hiltWorkVersion")
|
||||||
|
implementation("androidx.hilt:hilt-work:$hiltWorkVersion")
|
||||||
|
|
||||||
|
//Room
|
||||||
|
val roomVersion = "2.6.1"
|
||||||
|
ksp("androidx.room:room-compiler:$roomVersion")
|
||||||
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
|
implementation("androidx.room:room-runtime:$roomVersion")
|
||||||
|
|
||||||
// LeakCanary
|
// LeakCanary
|
||||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,19 @@
|
|||||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL"
|
||||||
tools:ignore="MissingClass" />
|
tools:ignore="MissingClass" />
|
||||||
|
|
||||||
|
<!-- Hilt (WorkManager) -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<!-- If you are using androidx.startup to initialize other components -->
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver android:name=".data.receiver.DownloadResumeReceiver" />
|
<receiver android:name=".data.receiver.DownloadResumeReceiver" />
|
||||||
<receiver android:name=".data.receiver.DownloadPauseReceiver" />
|
<receiver android:name=".data.receiver.DownloadPauseReceiver" />
|
||||||
<receiver android:name=".data.receiver.DownloadCancelReceiver" />
|
<receiver android:name=".data.receiver.DownloadCancelReceiver" />
|
||||||
|
|||||||
@@ -22,26 +22,39 @@ package com.aurora.store
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
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
|
||||||
import com.aurora.store.data.work.DownloadWorker
|
|
||||||
import com.aurora.store.util.CommonUtil
|
import com.aurora.store.util.CommonUtil
|
||||||
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
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 dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import javax.inject.Inject
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class AuroraApplication : Application() {
|
class AuroraApplication : Application(), Configuration.Provider {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var downloadWorkerUtil: DownloadWorkerUtil
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setMinimumLoggingLevel(android.util.Log.INFO)
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
|
||||||
private lateinit var fetch: Fetch
|
private lateinit var fetch: Fetch
|
||||||
|
|
||||||
companion object{
|
companion object {
|
||||||
val enqueuedDownloads = mutableSetOf<App>()
|
|
||||||
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
|
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +74,7 @@ class AuroraApplication : Application() {
|
|||||||
fetch = DownloadManager.with(this).fetch
|
fetch = DownloadManager.with(this).fetch
|
||||||
|
|
||||||
// Initialize DownloadWorker to observe and trigger downloads
|
// Initialize DownloadWorker to observe and trigger downloads
|
||||||
DownloadWorker.initDownloadWorker(applicationContext)
|
downloadWorkerUtil.init()
|
||||||
|
|
||||||
//Register broadcast receiver for package install/uninstall
|
//Register broadcast receiver for package install/uninstall
|
||||||
ContextCompat.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
|
|||||||
@@ -72,7 +72,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 dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -96,7 +95,6 @@ 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)
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ object RequestBuilder {
|
|||||||
fun buildRequest(context: Context, app: App, file: File): Request {
|
fun buildRequest(context: Context, app: App, file: File): Request {
|
||||||
val fileName = when (file.type) {
|
val fileName = when (file.type) {
|
||||||
File.FileType.BASE,
|
File.FileType.BASE,
|
||||||
File.FileType.SPLIT -> PathUtil.getApkDownloadFile(context, app, file)
|
File.FileType.SPLIT -> PathUtil.getApkDownloadFile(context, app.packageName, app.versionCode, file)
|
||||||
File.FileType.OBB,
|
File.FileType.OBB,
|
||||||
File.FileType.PATCH -> PathUtil.getObbDownloadFile(app, file)
|
File.FileType.PATCH -> PathUtil.getObbDownloadFile(app.packageName, file)
|
||||||
}
|
}
|
||||||
return Request(file.url, fileName).apply {
|
return Request(file.url, fileName).apply {
|
||||||
attachMetaData(context, app)
|
attachMetaData(context, app)
|
||||||
|
|||||||
@@ -6,5 +6,9 @@ enum class DownloadStatus {
|
|||||||
CANCELLED,
|
CANCELLED,
|
||||||
COMPLETED,
|
COMPLETED,
|
||||||
QUEUED,
|
QUEUED,
|
||||||
UNAVAILABLE
|
UNAVAILABLE;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.aurora.store.data.room
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
|
|
||||||
|
@Database(entities = [Download::class], version = 1, exportSchema = false)
|
||||||
|
abstract class AuroraDatabase : RoomDatabase() {
|
||||||
|
abstract fun downloadDao(): DownloadDao
|
||||||
|
}
|
||||||
31
app/src/main/java/com/aurora/store/data/room/RoomModule.kt
Normal file
31
app/src/main/java/com/aurora/store/data/room/RoomModule.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package com.aurora.store.data.room
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object RoomModule {
|
||||||
|
|
||||||
|
private const val DATABASE = "aurora_database"
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun providesRoomInstance(@ApplicationContext context: Context): AuroraDatabase {
|
||||||
|
return Room.databaseBuilder(context, AuroraDatabase::class.java, DATABASE)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesDownloadDao(auroraDatabase: AuroraDatabase): DownloadDao {
|
||||||
|
return auroraDatabase.downloadDao()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.aurora.store.data.room.download
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
|
|
||||||
|
@Entity(tableName = "download")
|
||||||
|
data class Download(
|
||||||
|
@PrimaryKey val packageName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val offerType: Int,
|
||||||
|
val isInstalled: Boolean,
|
||||||
|
val displayName: String,
|
||||||
|
val iconURL: String,
|
||||||
|
val size: Long,
|
||||||
|
val id: Int,
|
||||||
|
var status: DownloadStatus,
|
||||||
|
var progress: Int,
|
||||||
|
var speed: Long,
|
||||||
|
var timeRemaining: Long,
|
||||||
|
var totalFiles: Int,
|
||||||
|
var downloadedFiles: Int
|
||||||
|
) {
|
||||||
|
val isFinished get() = status in DownloadStatus.finished
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromApp(app: App): Download {
|
||||||
|
return Download(
|
||||||
|
app.packageName,
|
||||||
|
app.versionCode,
|
||||||
|
app.offerType,
|
||||||
|
app.isInstalled,
|
||||||
|
app.displayName,
|
||||||
|
app.iconArtwork.url,
|
||||||
|
app.size,
|
||||||
|
app.id,
|
||||||
|
DownloadStatus.QUEUED,
|
||||||
|
0,
|
||||||
|
0L,
|
||||||
|
0L,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.aurora.store.data.room.download
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DownloadDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(download: Download)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(download: Download)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM download")
|
||||||
|
fun downloads(): Flow<List<Download>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM download WHERE packageName = :packageName")
|
||||||
|
suspend fun getDownload(packageName: String): Download?
|
||||||
|
|
||||||
|
@Query("DELETE FROM download WHERE packageName = :packageName")
|
||||||
|
suspend fun delete(packageName: String)
|
||||||
|
}
|
||||||
@@ -6,147 +6,54 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ExistingWorkPolicy.KEEP
|
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.OutOfQuotaPolicy
|
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.aurora.Constants
|
import com.aurora.Constants
|
||||||
import com.aurora.extensions.copyTo
|
import com.aurora.extensions.copyTo
|
||||||
import com.aurora.extensions.isQAndAbove
|
import com.aurora.extensions.isQAndAbove
|
||||||
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.DownloadInfo
|
import com.aurora.store.data.model.DownloadInfo
|
||||||
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
|
||||||
import com.aurora.store.data.providers.AuthProvider
|
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.data.room.download.Download
|
||||||
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
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 dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
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.GlobalScope
|
import kotlinx.coroutines.NonCancellable
|
||||||
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
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
import kotlin.time.toJavaDuration
|
|
||||||
import com.aurora.gplayapi.data.models.File as GPlayFile
|
import com.aurora.gplayapi.data.models.File as GPlayFile
|
||||||
|
|
||||||
class DownloadWorker(private val appContext: Context, workerParams: WorkerParameters) :
|
@HiltWorker
|
||||||
CoroutineWorker(appContext, workerParams) {
|
class DownloadWorker @AssistedInject constructor(
|
||||||
|
private val downloadDao: DownloadDao,
|
||||||
|
@Assisted private val appContext: Context,
|
||||||
|
@Assisted workerParams: WorkerParameters
|
||||||
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
|
||||||
companion object {
|
private lateinit var download: Download
|
||||||
const val DOWNLOAD_WORKER = "DOWNLOAD_WORKER"
|
|
||||||
|
|
||||||
const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS"
|
|
||||||
const val DOWNLOAD_TIME = "DOWNLOAD_TIME"
|
|
||||||
const val DOWNLOAD_SPEED = "DOWNLOAD_SPEED"
|
|
||||||
|
|
||||||
private const val DOWNLOAD_APP = "DOWNLOAD_APP"
|
|
||||||
private const val DOWNLOAD_UPDATE = "DOWNLOAD_UPDATE"
|
|
||||||
|
|
||||||
private const val notificationID = 200
|
|
||||||
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 isEnqueued(packageName: String): Boolean {
|
|
||||||
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) {
|
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(app.packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelAll(context: Context, downloads: Boolean = true, updates: Boolean = true) {
|
|
||||||
val workManager = WorkManager.getInstance(context)
|
|
||||||
|
|
||||||
if (downloads) workManager.cancelAllWorkByTag(DOWNLOAD_APP)
|
|
||||||
if (updates) workManager.cancelAllWorkByTag(DOWNLOAD_UPDATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadApp(context: Context, app: App) {
|
|
||||||
val work = OneTimeWorkRequestBuilder<DownloadWorker>()
|
|
||||||
.addTag(DOWNLOAD_WORKER)
|
|
||||||
.addTag(app.packageName)
|
|
||||||
.addTag(app.versionCode.toString())
|
|
||||||
.addTag(if (app.isInstalled) DOWNLOAD_UPDATE else DOWNLOAD_APP)
|
|
||||||
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
|
|
||||||
.keepResultsForAtLeast(7.days.toJavaDuration())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Ensure all app downloads are unique to preserve individual records
|
|
||||||
WorkManager.getInstance(context)
|
|
||||||
.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 notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
private var downloading = false
|
private var downloading = false
|
||||||
|
|
||||||
|
private val NOTIFICATION_ID = 200
|
||||||
|
|
||||||
private var totalBytes by Delegates.notNull<Long>()
|
private var totalBytes by Delegates.notNull<Long>()
|
||||||
private var totalProgress = 0
|
private var totalProgress = 0
|
||||||
private var downloadedBytes = 0L
|
private var downloadedBytes = 0L
|
||||||
@@ -154,6 +61,18 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
|
|||||||
private val TAG = DownloadWorker::class.java.simpleName
|
private val TAG = DownloadWorker::class.java.simpleName
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
|
// Try to parse input data into a valid app
|
||||||
|
try {
|
||||||
|
val downloadData = inputData.getString(DownloadWorkerUtil.DOWNLOAD_DATA)
|
||||||
|
download = Gson().fromJson(downloadData, Download::class.java)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed to parse download data", exception)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set work/service to foreground on < Android 12.0
|
||||||
|
setForeground(getForegroundInfo())
|
||||||
|
|
||||||
// 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()
|
||||||
val purchaseHelper = PurchaseHelper(authData)
|
val purchaseHelper = PurchaseHelper(authData)
|
||||||
@@ -162,46 +81,40 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
|
|||||||
notificationManager =
|
notificationManager =
|
||||||
appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
|
appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
// Try to parse input data into a valid app
|
|
||||||
try {
|
|
||||||
app = AuroraApplication.enqueuedDownloads.first()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "No apps enqueued for downloads", exception)
|
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set work/service to foreground on < Android 12.0
|
|
||||||
setForeground(getForegroundInfo())
|
|
||||||
|
|
||||||
// Bail out if file list is empty
|
// Bail out if file list is empty
|
||||||
val files = purchaseHelper.purchase(app.packageName, app.versionCode, app.offerType)
|
val files =
|
||||||
|
purchaseHelper.purchase(download.packageName, download.versionCode, download.offerType)
|
||||||
if (files.isEmpty()) {
|
if (files.isEmpty()) {
|
||||||
Log.i(TAG, "Nothing to download!")
|
Log.i(TAG, "Nothing to download!")
|
||||||
notifyStatus(DownloadStatus.COMPLETED)
|
notifyStatus(DownloadStatus.FAILED)
|
||||||
return Result.success()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and verify all files exists
|
// Download and verify all files exists
|
||||||
totalBytes = files.sumOf { it.size }
|
totalBytes = files.sumOf { it.size }
|
||||||
|
|
||||||
PathUtil.getAppDownloadDir(appContext, app.packageName, app.versionCode).createDirectories()
|
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode)
|
||||||
|
.createDirectories()
|
||||||
if (files.any { it.type == GPlayFile.FileType.OBB || it.type == GPlayFile.FileType.PATCH }) {
|
if (files.any { it.type == GPlayFile.FileType.OBB || it.type == GPlayFile.FileType.PATCH }) {
|
||||||
PathUtil.getObbDownloadDir(app.packageName).createDirectories()
|
PathUtil.getObbDownloadDir(download.packageName).createDirectories()
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestList = getDownloadRequest(files)
|
val requestList = getDownloadRequest(files)
|
||||||
|
download.totalFiles = requestList.size
|
||||||
requestList.forEach { request ->
|
requestList.forEach { request ->
|
||||||
downloading = true
|
downloading = true
|
||||||
runCatching { downloadFile(request) }
|
runCatching { downloadFile(request); download.downloadedFiles++ }
|
||||||
.onSuccess { downloading = false }
|
.onSuccess { downloading = false }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Log.e(TAG, "Failed to download ${app.packageName}", it)
|
Log.e(TAG, "Failed to download ${download.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,30 +124,44 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
|
|||||||
|
|
||||||
// Mark download as completed
|
// Mark download as completed
|
||||||
notifyStatus(DownloadStatus.COMPLETED)
|
notifyStatus(DownloadStatus.COMPLETED)
|
||||||
Log.i(TAG, "Finished downloading ${app.packageName}")
|
Log.i(TAG, "Finished downloading ${download.packageName}")
|
||||||
|
|
||||||
// Notify for installation
|
// Notify for installation
|
||||||
Intent(appContext, InstallReceiver::class.java).also {
|
Intent(appContext, InstallReceiver::class.java).also {
|
||||||
it.action = InstallReceiver.ACTION_INSTALL_APP
|
it.action = InstallReceiver.ACTION_INSTALL_APP
|
||||||
it.putExtra(Constants.STRING_APP, app.packageName)
|
it.putExtra(Constants.STRING_APP, download.packageName)
|
||||||
it.putExtra(Constants.STRING_VERSION, app.versionCode)
|
it.putExtra(Constants.STRING_VERSION, download.versionCode)
|
||||||
appContext.sendBroadcast(it)
|
appContext.sendBroadcast(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the app from the list
|
|
||||||
AuroraApplication.enqueuedDownloads.remove(app)
|
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
private suspend fun onFailure() {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
Log.i(TAG, "Cleaning up!")
|
||||||
|
notifyStatus(DownloadStatus.FAILED)
|
||||||
|
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode)
|
||||||
|
.deleteRecursively()
|
||||||
|
with(appContext.getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager) {
|
||||||
|
cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
val filePath = when (it.type) {
|
val filePath = when (it.type) {
|
||||||
GPlayFile.FileType.BASE,
|
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
|
||||||
GPlayFile.FileType.SPLIT -> PathUtil.getApkDownloadFile(appContext, app, it)
|
PathUtil.getApkDownloadFile(
|
||||||
|
appContext, download.packageName, download.versionCode, it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
GPlayFile.FileType.OBB,
|
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> {
|
||||||
GPlayFile.FileType.PATCH -> PathUtil.getObbDownloadFile(app, it)
|
PathUtil.getObbDownloadFile(download.packageName, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloadList.add(Request(it.url, filePath, it.size))
|
downloadList.add(Request(it.url, filePath, it.size))
|
||||||
}
|
}
|
||||||
@@ -281,37 +208,38 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame
|
|||||||
downloadedBytes += downloadInfo.bytesCopied
|
downloadedBytes += downloadInfo.bytesCopied
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = Data.Builder()
|
download.apply {
|
||||||
.putInt(DOWNLOAD_PROGRESS, progress)
|
this.status = DownloadStatus.DOWNLOADING
|
||||||
.putLong(DOWNLOAD_SPEED, downloadInfo.speed)
|
this.progress = progress
|
||||||
.putLong(DOWNLOAD_TIME, bytesRemaining / speed * 1000)
|
this.speed = downloadInfo.speed
|
||||||
.build()
|
this.timeRemaining = bytesRemaining / speed * 1000
|
||||||
|
}
|
||||||
|
downloadDao.update(download)
|
||||||
|
|
||||||
setProgress(data)
|
notifyStatus(DownloadStatus.DOWNLOADING, NOTIFICATION_ID)
|
||||||
notifyStatus(DownloadStatus.DOWNLOADING, progress, notificationID)
|
|
||||||
totalProgress = progress
|
totalProgress = progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
val notification = NotificationUtil.getDownloadNotification(
|
val notification = NotificationUtil.getDownloadNotification(appContext, download, id)
|
||||||
appContext,
|
|
||||||
app,
|
|
||||||
DownloadStatus.QUEUED,
|
|
||||||
0,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
return if (isQAndAbove()) {
|
return if (isQAndAbove()) {
|
||||||
ForegroundInfo(notificationID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
ForegroundInfo(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||||
} else {
|
} else {
|
||||||
ForegroundInfo(notificationID, notification)
|
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyStatus(status: DownloadStatus, progress: Int = 100, dID: Int = -1) {
|
private suspend fun notifyStatus(status: DownloadStatus, dID: Int = -1) {
|
||||||
val notification =
|
// Update database for all status except downloading which is handled onProgress
|
||||||
NotificationUtil.getDownloadNotification(appContext, app, status, progress, id)
|
if (status != DownloadStatus.DOWNLOADING) {
|
||||||
notificationManager.notify(if (dID != -1) dID else app.packageName.hashCode(), notification)
|
download.status = status
|
||||||
|
downloadDao.update(download)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = NotificationUtil.getDownloadNotification(appContext, download, id)
|
||||||
|
val notificationID = if (dID != -1) dID else download.packageName.hashCode()
|
||||||
|
notificationManager.notify(notificationID, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt
Normal file
103
app/src/main/java/com/aurora/store/util/DownloadWorkerUtil.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.aurora.store.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.OutOfQuotaPolicy
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
|
import com.aurora.store.data.room.download.Download
|
||||||
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
|
import com.aurora.store.data.work.DownloadWorker
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
class DownloadWorkerUtil @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val downloadDao: DownloadDao,
|
||||||
|
private val gson: Gson
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DOWNLOAD_WORKER = "DOWNLOAD_WORKER"
|
||||||
|
const val DOWNLOAD_DATA = "DOWNLOAD_DATA"
|
||||||
|
|
||||||
|
private const val DOWNLOAD_APP = "DOWNLOAD_APP"
|
||||||
|
private const val DOWNLOAD_UPDATE = "DOWNLOAD_UPDATE"
|
||||||
|
private const val PACKAGE_NAME = "PACKAGE_NAME"
|
||||||
|
private const val VERSION_CODE = "VERSION_CODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadsList = downloadDao.downloads()
|
||||||
|
.stateIn(GlobalScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
// Run cleanup for last finished download and drop it database
|
||||||
|
GlobalScope.launch {
|
||||||
|
downloadDao.downloads()
|
||||||
|
.collectLatest { list ->
|
||||||
|
// Check and trigger next download in queue, if any
|
||||||
|
if (!list.any { it.status == DownloadStatus.DOWNLOADING }) {
|
||||||
|
val enqueuedDownloads = list.filter { it.status == DownloadStatus.QUEUED }
|
||||||
|
enqueuedDownloads.firstOrNull()?.let {
|
||||||
|
try {
|
||||||
|
Log.i(DOWNLOAD_WORKER, "Downloading ${it.packageName}")
|
||||||
|
trigger(it)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.i(DOWNLOAD_WORKER, "Failed to download app", exception)
|
||||||
|
downloadDao.delete(it.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun enqueueApp(app: App) {
|
||||||
|
downloadDao.insert(Download.fromApp(app))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelDownload(packageName: String) {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag("$PACKAGE_NAME:$packageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelAll(context: Context, downloads: Boolean = true, updates: Boolean = true) {
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
if (downloads) workManager.cancelAllWorkByTag(DOWNLOAD_APP)
|
||||||
|
if (updates) workManager.cancelAllWorkByTag(DOWNLOAD_UPDATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trigger(download: Download) {
|
||||||
|
val inputData = Data.Builder()
|
||||||
|
.putString(DOWNLOAD_DATA, gson.toJson(download))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val work = OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||||
|
.addTag(DOWNLOAD_WORKER)
|
||||||
|
.addTag("$PACKAGE_NAME:${download.packageName}")
|
||||||
|
.addTag("$VERSION_CODE:${download.versionCode}")
|
||||||
|
.addTag(if (download.isInstalled) DOWNLOAD_UPDATE else DOWNLOAD_APP)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
|
||||||
|
.setInputData(inputData)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Ensure all app downloads are unique to preserve individual records
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniqueWork(
|
||||||
|
"${DOWNLOAD_WORKER}/${download.packageName}",
|
||||||
|
ExistingWorkPolicy.KEEP, work
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import com.aurora.extensions.getStyledAttributeColor
|
|||||||
import com.aurora.extensions.isMAndAbove
|
import com.aurora.extensions.isMAndAbove
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.store.MainActivity
|
import com.aurora.store.MainActivity
|
||||||
|
import com.aurora.store.data.room.download.Download as AuroraDownload
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.data.activity.InstallActivity
|
import com.aurora.store.data.activity.InstallActivity
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
@@ -180,7 +181,7 @@ object NotificationUtil {
|
|||||||
|
|
||||||
Status.COMPLETED -> if (fetchGroup.groupDownloadProgress == 100) {
|
Status.COMPLETED -> if (fetchGroup.groupDownloadProgress == 100) {
|
||||||
builder.setAutoCancel(true)
|
builder.setAutoCancel(true)
|
||||||
builder.setContentIntent(getContentIntentForDetails(context, app))
|
builder.setContentIntent(getContentIntentForDetails(context, app.packageName))
|
||||||
builder.setStyle(progressBigText)
|
builder.setStyle(progressBigText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +201,15 @@ object NotificationUtil {
|
|||||||
|
|
||||||
fun getDownloadNotification(
|
fun getDownloadNotification(
|
||||||
context: Context,
|
context: Context,
|
||||||
app: App,
|
download: AuroraDownload,
|
||||||
status: DownloadStatus,
|
|
||||||
progress: Int,
|
|
||||||
workID: UUID
|
workID: UUID
|
||||||
): Notification {
|
): Notification {
|
||||||
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
||||||
builder.setContentTitle(app.displayName)
|
builder.setContentTitle(download.displayName)
|
||||||
builder.color = ContextCompat.getColor(context, R.color.colorAccent)
|
builder.color = ContextCompat.getColor(context, R.color.colorAccent)
|
||||||
builder.setContentIntent(getContentIntentForDownloads(context))
|
builder.setContentIntent(getContentIntentForDownloads(context))
|
||||||
|
|
||||||
when (status) {
|
when (download.status) {
|
||||||
DownloadStatus.CANCELLED -> {
|
DownloadStatus.CANCELLED -> {
|
||||||
builder.setSmallIcon(R.drawable.ic_download_cancel)
|
builder.setSmallIcon(R.drawable.ic_download_cancel)
|
||||||
builder.setContentText(context.getString(R.string.download_canceled))
|
builder.setContentText(context.getString(R.string.download_canceled))
|
||||||
@@ -225,17 +224,17 @@ object NotificationUtil {
|
|||||||
builder.setCategory(Notification.CATEGORY_ERROR)
|
builder.setCategory(Notification.CATEGORY_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadStatus.COMPLETED -> if (progress == 100) {
|
DownloadStatus.COMPLETED -> if (download.progress == 100) {
|
||||||
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
builder.setContentText(context.getString(R.string.download_completed))
|
builder.setContentText(context.getString(R.string.download_completed))
|
||||||
builder.setAutoCancel(true)
|
builder.setAutoCancel(true)
|
||||||
builder.setCategory(Notification.CATEGORY_STATUS)
|
builder.setCategory(Notification.CATEGORY_STATUS)
|
||||||
builder.setContentIntent(getContentIntentForDetails(context, app))
|
builder.setContentIntent(getContentIntentForDetails(context, download.packageName))
|
||||||
builder.addAction(
|
builder.addAction(
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_install,
|
R.drawable.ic_install,
|
||||||
context.getString(R.string.action_install),
|
context.getString(R.string.action_install),
|
||||||
getInstallIntent(context, app.packageName, app.versionCode)
|
getInstallIntent(context, download.packageName, download.versionCode)
|
||||||
).build()
|
).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -243,15 +242,20 @@ object NotificationUtil {
|
|||||||
DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> {
|
DownloadStatus.DOWNLOADING, DownloadStatus.QUEUED -> {
|
||||||
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
builder.setContentText(
|
builder.setContentText(
|
||||||
if (progress == 0) {
|
if (download.progress == 0) {
|
||||||
context.getString(R.string.download_queued)
|
context.getString(R.string.download_queued)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.alt_download_progress)
|
context.getString(
|
||||||
|
R.string.download_progress,
|
||||||
|
download.downloadedFiles,
|
||||||
|
download.totalFiles,
|
||||||
|
CommonUtil.humanReadableByteSpeed(download.speed, true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
builder.setOngoing(true)
|
builder.setOngoing(true)
|
||||||
builder.setCategory(Notification.CATEGORY_PROGRESS)
|
builder.setCategory(Notification.CATEGORY_PROGRESS)
|
||||||
builder.setProgress(100, progress, progress == 0)
|
builder.setProgress(100, download.progress, download.progress == 0)
|
||||||
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
|
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
|
||||||
builder.addAction(
|
builder.addAction(
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
@@ -274,7 +278,7 @@ object NotificationUtil {
|
|||||||
setSmallIcon(R.drawable.ic_install)
|
setSmallIcon(R.drawable.ic_install)
|
||||||
setContentTitle(app.displayName)
|
setContentTitle(app.displayName)
|
||||||
setContentText(content)
|
setContentText(content)
|
||||||
setContentIntent(getContentIntentForDetails(context, app))
|
setContentIntent(getContentIntentForDetails(context, app.packageName))
|
||||||
setSubText(app.packageName)
|
setSubText(app.packageName)
|
||||||
}
|
}
|
||||||
return builder.build()
|
return builder.build()
|
||||||
@@ -307,12 +311,12 @@ object NotificationUtil {
|
|||||||
return PendingIntent.getBroadcast(context, groupId, intent, flags)
|
return PendingIntent.getBroadcast(context, groupId, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getContentIntentForDetails(context: Context, app: App?): PendingIntent {
|
private fun getContentIntentForDetails(context: Context, packageName: String): PendingIntent {
|
||||||
return NavDeepLinkBuilder(context)
|
return NavDeepLinkBuilder(context)
|
||||||
.setGraph(R.navigation.mobile_navigation)
|
.setGraph(R.navigation.mobile_navigation)
|
||||||
.setDestination(R.id.appDetailsFragment)
|
.setDestination(R.id.appDetailsFragment)
|
||||||
.setComponentName(MainActivity::class.java)
|
.setComponentName(MainActivity::class.java)
|
||||||
.setArguments(bundleOf("packageName" to app!!.packageName))
|
.setArguments(bundleOf("packageName" to packageName))
|
||||||
.createPendingIntent()
|
.createPendingIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,13 @@ object PathUtil {
|
|||||||
return Path(getPackageDirectory(context, packageName), versionCode.toString())
|
return Path(getPackageDirectory(context, packageName), versionCode.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkDownloadFile(context: Context, app: App, file: File): String {
|
fun getApkDownloadFile(
|
||||||
return getVersionDirectory(context, app.packageName, app.versionCode) + "/${file.name}"
|
context: Context,
|
||||||
|
packageName: String,
|
||||||
|
versionCode: Int,
|
||||||
|
file: File
|
||||||
|
): String {
|
||||||
|
return getVersionDirectory(context, packageName, versionCode) + "/${file.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkDownloadFile(context: Context, packageName: String, versionCode: Int): String {
|
fun getApkDownloadFile(context: Context, packageName: String, versionCode: Int): String {
|
||||||
@@ -84,9 +89,9 @@ object PathUtil {
|
|||||||
return "${getExternalPath(context)}/Exports/"
|
return "${getExternalPath(context)}/Exports/"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getObbDownloadPath(app: App): String {
|
private fun getObbDownloadPath(packageName: String): String {
|
||||||
return Environment.getExternalStorageDirectory()
|
return Environment.getExternalStorageDirectory()
|
||||||
.toString() + "/Android/obb/" + app.packageName
|
.toString() + "/Android/obb/" + packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getObbDownloadDir(packageName: String): Path {
|
fun getObbDownloadDir(packageName: String): Path {
|
||||||
@@ -97,8 +102,8 @@ object PathUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getObbDownloadFile(app: App, file: File): String {
|
fun getObbDownloadFile(packageName: String, file: File): String {
|
||||||
val obbDir = getObbDownloadPath(app)
|
val obbDir = getObbDownloadPath(packageName)
|
||||||
return "$obbDir/${file.name}"
|
return "$obbDir/${file.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||||
@@ -70,7 +68,6 @@ import com.aurora.store.data.installer.AppInstaller
|
|||||||
import com.aurora.store.data.installer.RootInstaller
|
import com.aurora.store.data.installer.RootInstaller
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.data.providers.AuthProvider
|
import com.aurora.store.data.providers.AuthProvider
|
||||||
import com.aurora.store.data.work.DownloadWorker
|
|
||||||
import com.aurora.store.databinding.FragmentDetailsBinding
|
import com.aurora.store.databinding.FragmentDetailsBinding
|
||||||
import com.aurora.store.databinding.LayoutDetailsBetaBinding
|
import com.aurora.store.databinding.LayoutDetailsBetaBinding
|
||||||
import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
|
import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
|
||||||
@@ -101,6 +98,8 @@ import org.greenrobot.eventbus.Subscribe
|
|||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -119,7 +118,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
private val startForStorageManagerResult =
|
private val startForStorageManagerResult =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (isRAndAbove() && Environment.isExternalStorageManager()) {
|
if (isRAndAbove() && Environment.isExternalStorageManager()) {
|
||||||
DownloadWorker.enqueueApp(requireContext(), app)
|
viewModel.download(app)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.permissions_denied)
|
toast(R.string.permissions_denied)
|
||||||
}
|
}
|
||||||
@@ -127,7 +126,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
private val startForPermissions =
|
private val startForPermissions =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
if (it) {
|
if (it) {
|
||||||
DownloadWorker.enqueueApp(requireContext(), app)
|
viewModel.download(app)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.permissions_denied)
|
toast(R.string.permissions_denied)
|
||||||
}
|
}
|
||||||
@@ -239,35 +238,30 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
|
|
||||||
// Downloads
|
// Downloads
|
||||||
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
|
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
|
||||||
DownloadWorker.cancelDownload(it.context, app)
|
viewModel.cancelDownload(app)
|
||||||
if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0)
|
if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val uniqueWorkName = "${DownloadWorker.DOWNLOAD_WORKER}/${app.packageName}"
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
WorkManager.getInstance(view.context)
|
viewModel.downloadsList
|
||||||
.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)
|
.filter { list -> list.any { it.packageName == app.packageName } }
|
||||||
.observe(viewLifecycleOwner) { workList ->
|
.collectLatest { downloadsList ->
|
||||||
workList.getOrNull(0)?.let {
|
val download = downloadsList.find { it.packageName == app.packageName }
|
||||||
if (it.state.isFinished) flip(0) else flip(1)
|
download?.let {
|
||||||
when (it.state) {
|
downloadStatus = it.status
|
||||||
WorkInfo.State.ENQUEUED,
|
|
||||||
WorkInfo.State.RUNNING -> {
|
if (it.isFinished) flip(0) else flip(1)
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
when (it.status) {
|
||||||
updateProgress(
|
DownloadStatus.QUEUED,
|
||||||
it.progress.getInt(DownloadWorker.DOWNLOAD_PROGRESS, 0),
|
DownloadStatus.DOWNLOADING -> {
|
||||||
it.progress.getLong(DownloadWorker.DOWNLOAD_SPEED, -1),
|
updateProgress(it.progress, it.speed, it.timeRemaining)
|
||||||
it.progress.getLong(DownloadWorker.DOWNLOAD_TIME, -1)
|
}
|
||||||
)
|
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkInfo.State.FAILED -> downloadStatus = DownloadStatus.FAILED
|
|
||||||
WorkInfo.State.CANCELLED -> downloadStatus = DownloadStatus.CANCELLED
|
|
||||||
WorkInfo.State.SUCCEEDED -> downloadStatus = DownloadStatus.COMPLETED
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reviews
|
// Reviews
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
@@ -587,6 +581,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
flip(1)
|
||||||
purchase()
|
purchase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,7 +599,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
DownloadWorker.enqueueApp(requireContext(), app)
|
viewModel.download(app)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ContextCompat.checkSelfPermission(
|
if (ContextCompat.checkSelfPermission(
|
||||||
@@ -612,13 +607,13 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
DownloadWorker.enqueueApp(requireContext(), app)
|
viewModel.download(app)
|
||||||
} else {
|
} else {
|
||||||
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DownloadWorker.enqueueApp(requireContext(), app)
|
viewModel.download(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,8 +674,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
|
|||||||
binding.layoutDetailsToolbar.toolbar.invalidateMenu()
|
binding.layoutDetailsToolbar.toolbar.invalidateMenu()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val downloading = downloadStatus == DownloadStatus.DOWNLOADING
|
if (downloadStatus == DownloadStatus.QUEUED) {
|
||||||
if (DownloadWorker.isEnqueued(app.packageName) && !downloading) {
|
|
||||||
flip(1)
|
flip(1)
|
||||||
} else if (app.isFree) {
|
} else if (app.isFree) {
|
||||||
btn.setText(R.string.action_install)
|
btn.setText(R.string.action_install)
|
||||||
|
|||||||
@@ -14,15 +14,21 @@ import com.aurora.store.data.model.ExodusReport
|
|||||||
import com.aurora.store.data.model.Report
|
import com.aurora.store.data.model.Report
|
||||||
import com.aurora.store.data.network.HttpClient
|
import com.aurora.store.data.network.HttpClient
|
||||||
import com.aurora.store.data.providers.AuthProvider
|
import com.aurora.store.data.providers.AuthProvider
|
||||||
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AppDetailsViewModel : ViewModel() {
|
@HiltViewModel
|
||||||
|
class AppDetailsViewModel @Inject constructor(
|
||||||
|
private val downloadWorkerUtil: DownloadWorkerUtil
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val TAG = AppDetailsViewModel::class.java.simpleName
|
private val TAG = AppDetailsViewModel::class.java.simpleName
|
||||||
|
|
||||||
@@ -44,6 +50,8 @@ class AppDetailsViewModel : ViewModel() {
|
|||||||
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
|
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
|
||||||
val testingProgramStatus = _testingProgramStatus.asSharedFlow()
|
val testingProgramStatus = _testingProgramStatus.asSharedFlow()
|
||||||
|
|
||||||
|
val downloadsList get() = downloadWorkerUtil.downloadsList
|
||||||
|
|
||||||
fun fetchAppDetails(context: Context, packageName: String) {
|
fun fetchAppDetails(context: Context, packageName: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -129,6 +137,14 @@ class AppDetailsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun download(app: App) {
|
||||||
|
viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelDownload(app: App) {
|
||||||
|
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun install(context: Context, packageName: String, files: List<Any>) {
|
fun install(context: Context, packageName: String, files: List<Any>) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user