diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 48d4e3c35..e8d0cb0c8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -160,6 +160,11 @@
android:name=".data.receiver.InstallerStatusReceiver"
android:exported="false" />
+
+
+
()
private var totalProgress = 0
private var downloadedBytes = 0L
private val TAG = DownloadWorker::class.java.simpleName
+ object Exceptions {
+ val InvalidAuthDataException = Exception("AuthData is invalid")
+ val NoPackageNameException = Exception("No packagename provided")
+ val NothingToDownloadException = Exception("Failed to purchase app")
+ val DownloadFailedException = Exception("Download was failed or cancelled")
+ val VerificationFailedException = Exception("Verification of downloaded files failed")
+ }
+
override suspend fun doWork(): Result {
super.doWork()
// Bail out immediately if authData is not valid
- if (!authProvider.isSavedAuthDataValid()) {
- Log.e(TAG, "AuthData is not valid, exiting!")
- onFailure()
- return Result.failure()
- }
+ if (!authProvider.isSavedAuthDataValid()) return onFailure(Exceptions.InvalidAuthDataException)
// Fetch required data for download
try {
- val packageName =
- inputData.getString(DownloadHelper.PACKAGE_NAME) ?: return Result.failure()
+ val packageName = inputData.getString(DownloadHelper.PACKAGE_NAME)
+
+ // Bail out if no package name is provided
+ if (packageName.isNullOrBlank()) return onFailure(Exceptions.NoPackageNameException)
notificationId = packageName.hashCode()
download = downloadDao.getDownload(packageName)
@@ -102,110 +112,131 @@ class DownloadWorker @AssistedInject constructor(
icon = Bitmap.createScaledBitmap(bitmap, 96, 96, true)
}
} catch (exception: Exception) {
- Log.e(TAG, "Failed to parse download data", exception)
- onFailure(exception)
- return Result.failure()
+ return onFailure(exception)
}
// Set work/service to foreground on < Android 12.0
setForeground(getForegroundInfo())
- // Bail out if file list is empty
+ // Try to purchase the app if file list is empty
download.fileList = download.fileList.ifEmpty {
purchase(download.packageName, download.versionCode, download.offerType)
}
- if (download.fileList.isEmpty()) {
- Log.i(TAG, "Nothing to download!")
- onFailure()
- return Result.failure()
- }
+ // Bail out if file list is empty after purchase
+ if (download.fileList.isEmpty()) return onFailure(Exceptions.NothingToDownloadException)
// Create dirs & generate download request for files and shared libs (if any)
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode).mkdirs()
+
+ // Create OBB dir if required
if (download.fileList.requiresObbDir()) {
PathUtil.getObbDownloadDir(download.packageName).mkdirs()
}
- // Purchase the app (free apps needs to be purchased too)
- val requestList = mutableListOf()
+ val files = mutableListOf()
+
+ // Check if shared libs are present, if yes, handle them first
if (download.sharedLibs.isNotEmpty()) {
download.sharedLibs.forEach {
+ // Create shared lib download dir
PathUtil.getLibDownloadDir(
appContext,
download.packageName,
download.versionCode,
it.packageName
).mkdirs()
- it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
- requestList.addAll(it.fileList)
+
+ // Purchase shared lib if file list is empty
+ it.fileList = it.fileList.ifEmpty {
+ purchase(it.packageName, it.versionCode, 0)
+ }
+ files.addAll(it.fileList)
}
}
- requestList.addAll(download.fileList)
+ files.addAll(download.fileList)
// Update data for notification
- download.totalFiles = requestList.size
- totalBytes = requestList.sumOf { it.size }
+ download.totalFiles = files.size
+ totalBytes = files.sumOf { it.size }
// Update database with all latest purchases
downloadDao.updateFiles(download.packageName, download.fileList)
downloadDao.updateSharedLibs(download.packageName, download.sharedLibs)
- // Download and verify all files exists
- requestList.forEach { request ->
- downloading = true
- runCatching { downloadFile(request); download.downloadedFiles++ }
- .onSuccess { downloading = false }
- .onFailure {
- Log.e(TAG, "Failed to download ${download.packageName}", it)
- downloading = false
+ // Download files
+ val downloadSuccess = downloadFiles(download.packageName, files)
- onFailure(it as Exception)
+ // Report failure if download was stopped or failed
+ if (!downloadSuccess || isStopped) return onFailure(Exceptions.DownloadFailedException)
- return Result.failure()
- }
- while (downloading) {
- delay(1000)
- val d = downloadDao.getDownload(download.packageName)
- if (isStopped || d.downloadStatus == DownloadStatus.CANCELLED) {
- onFailure(CancellationException())
- break
- }
- }
- }
+ // Verify downloaded files
+ val verifySuccess = verifyFiles(download.packageName, files)
- try {
- // Verify all downloaded files
- Log.i(TAG, "Verifying downloaded files")
- notifyStatus(DownloadStatus.VERIFYING)
- requestList.forEach { require(verifyFile(it)) }
- } catch (exception: Exception) {
- Log.e(TAG, "Failed to verify downloaded files!", exception)
- onFailure(exception)
- return Result.failure()
- }
+ // Report failure if verification failed
+ if (!verifySuccess || isStopped) return onFailure(Exceptions.VerificationFailedException)
- // Mark download as completed
- onSuccess()
- return Result.success()
+ Log.i(TAG, "Finished downloading & verifying ${download.packageName}")
+ notifyStatus(DownloadStatus.COMPLETED)
+
+ return onSuccess()
}
- private suspend fun onSuccess() {
- withContext(NonCancellable) {
- Log.i(TAG, "Finished downloading ${download.packageName}")
- notifyStatus(DownloadStatus.COMPLETED)
-
+ private suspend fun downloadFiles(
+ packageName: String,
+ files: List
+ ): Boolean = withContext(Dispatchers.IO) {
+ for (file in files) {
try {
- appInstaller.getPreferredInstaller().install(download)
- } catch (exception: Exception) {
- Log.e(TAG, "Failed to install ${download.packageName}", exception)
+ downloadFile(file)
+
+ if (isStopped) {
+ Log.w(TAG, "Download cancelled for $packageName")
+ return@withContext false
+ }
+
+ download.downloadedFiles++
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to download $packageName", e)
+ return@withContext false
}
}
+
+ true
+ }
+
+ private suspend fun verifyFiles(
+ packageName: String,
+ files: List
+ ): Boolean = withContext(NonCancellable) {
+ notifyStatus(DownloadStatus.VERIFYING)
+
+ for (file in files) {
+ try {
+ verifyFile(file)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to verify $packageName : ${file.name}", e)
+ return@withContext false
+ }
+ }
+
+ true
+ }
+
+ private suspend fun onSuccess(): Result = withContext(NonCancellable) {
+ return@withContext try {
+ appInstaller.getPreferredInstaller().install(download)
+ Result.success()
+ } catch (exception: Exception) {
+ Log.e(TAG, "Failed to install ${download.packageName}", exception)
+ onFailure(exception)
+ }
}
- private suspend fun onFailure(exception: Exception = Exception("Something went wrong!")) {
+ private suspend fun onFailure(exception: Exception = Exception("Something went wrong!")): Result =
withContext(NonCancellable) {
- Log.i(TAG, "Failed downloading ${download.packageName}")
+ Log.e(TAG, "Job failed: ${download.packageName}", exception)
+
val cancelReasons = listOf(STOP_REASON_USER, STOP_REASON_CANCELLED_BY_APP)
if (isSAndAbove && stopReason in cancelReasons) {
notifyStatus(DownloadStatus.CANCELLED)
@@ -221,9 +252,11 @@ class DownloadWorker @AssistedInject constructor(
}
}
+ // Remove all notifications
notificationManager.cancel(notificationId)
+
+ return@withContext Result.failure()
}
- }
/**
* Purchases the app to get the download URL of the required files
@@ -258,6 +291,7 @@ class DownloadWorker @AssistedInject constructor(
* @return A [Result] indicating whether the file was downloaded or not.
*/
private suspend fun downloadFile(gFile: GPlayFile): Result {
+ Log.i(TAG, "Downloading ${gFile.name}")
return withContext(Dispatchers.IO) {
val file = PathUtil.getLocalFile(appContext, gFile, download)
@@ -338,7 +372,7 @@ class DownloadWorker @AssistedInject constructor(
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = if (this::download.isInitialized) {
- NotificationUtil.getDownloadNotification(appContext, download, id, icon)
+ NotificationUtil.getDownloadNotification(appContext, download, icon)
} else {
NotificationUtil.getDownloadNotification(appContext)
}
@@ -353,7 +387,6 @@ class DownloadWorker @AssistedInject constructor(
/**
* Notifies the user of the current status of the download.
* @param status Current [DownloadStatus]
- * @param dID ID of the notification, defaults to hashCode of the download's packageName
*/
private suspend fun notifyStatus(status: DownloadStatus) {
// Update status in database
@@ -373,7 +406,7 @@ class DownloadWorker @AssistedInject constructor(
else -> {}
}
- val notification = NotificationUtil.getDownloadNotification(appContext, download, id, icon)
+ val notification = NotificationUtil.getDownloadNotification(appContext, download, icon)
notificationManager.notify(notificationId, notification)
}
@@ -384,6 +417,7 @@ class DownloadWorker @AssistedInject constructor(
@OptIn(ExperimentalStdlibApi::class)
private suspend fun verifyFile(gFile: GPlayFile): Boolean {
val file = PathUtil.getLocalFile(appContext, gFile, download)
+ Log.i(TAG, "Verifying $file")
val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256
diff --git a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt
index e37ac9655..b25ff944d 100644
--- a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt
+++ b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt
@@ -15,13 +15,14 @@ import androidx.core.app.PendingIntentCompat
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.navigation.NavDeepLinkBuilder
-import androidx.work.WorkManager
import com.aurora.Constants
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.data.activity.InstallActivity
+import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.DownloadStatus
+import com.aurora.store.data.receiver.DownloadCancelReceiver
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.update.Update
import java.util.UUID
@@ -88,7 +89,6 @@ object NotificationUtil {
fun getDownloadNotification(
context: Context,
download: AuroraDownload,
- workID: UUID,
largeIcon: Bitmap? = null
): Notification {
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_DOWNLOADS)
@@ -96,6 +96,17 @@ object NotificationUtil {
builder.setContentIntent(getContentIntentForDownloads(context))
builder.setLargeIcon(largeIcon)
+ val cancelIntent = Intent(context, DownloadCancelReceiver::class.java).apply {
+ putExtra(DownloadHelper.PACKAGE_NAME, download.packageName)
+ }
+
+ val pendingCancelIntent = PendingIntent.getBroadcast(
+ context,
+ 0,
+ cancelIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
when (download.downloadStatus) {
DownloadStatus.CANCELLED -> {
builder.setSmallIcon(R.drawable.ic_download_cancel)
@@ -152,7 +163,7 @@ object NotificationUtil {
NotificationCompat.Action.Builder(
R.drawable.ic_download_cancel,
context.getString(R.string.action_cancel),
- WorkManager.getInstance(context).createCancelPendingIntent(workID)
+ pendingCancelIntent
).build()
)
}