From 7fdf06d73a3e4ced82e31d5a786870b8569bea39 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Fri, 20 Dec 2024 11:27:44 +0700 Subject: [PATCH 1/3] DownloadWorker: Extract and simplify GPlayFile -> File logic Signed-off-by: Aayush Gupta --- .../com/aurora/store/data/model/Request.kt | 11 ---- .../aurora/store/data/work/DownloadWorker.kt | 57 ++++++------------- .../java/com/aurora/store/util/PathUtil.kt | 53 ++++++++--------- 3 files changed, 43 insertions(+), 78 deletions(-) delete mode 100644 app/src/main/java/com/aurora/store/data/model/Request.kt diff --git a/app/src/main/java/com/aurora/store/data/model/Request.kt b/app/src/main/java/com/aurora/store/data/model/Request.kt deleted file mode 100644 index 92435a024..000000000 --- a/app/src/main/java/com/aurora/store/data/model/Request.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.aurora.store.data.model - -import java.io.File - -data class Request( - val url: String, - val file: File, - val size: Long, - val sha1: String, - val sha256: String -) diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index e09aaf206..030681653 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -27,7 +27,6 @@ import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.Algorithm import com.aurora.store.data.model.DownloadInfo import com.aurora.store.data.model.DownloadStatus -import com.aurora.store.data.model.Request import com.aurora.store.data.network.HttpClient import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.room.download.Download @@ -125,7 +124,7 @@ class DownloadWorker @AssistedInject constructor( } // Purchase the app (free apps needs to be purchased too) - val requestList = mutableListOf() + val requestList = mutableListOf() if (download.sharedLibs.isNotEmpty()) { download.sharedLibs.forEach { PathUtil.getLibDownloadDir( @@ -135,10 +134,10 @@ class DownloadWorker @AssistedInject constructor( it.packageName ).mkdirs() it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) } - requestList.addAll(getDownloadRequest(it.fileList, it.packageName)) + requestList.addAll(it.fileList) } } - requestList.addAll(getDownloadRequest(download.fileList, null)) + requestList.addAll(download.fileList) // Update data for notification download.totalFiles = requestList.size @@ -169,12 +168,6 @@ class DownloadWorker @AssistedInject constructor( } } - if (!requestList.all { it.file.exists() }) { - Log.e(TAG, "Downloaded files are missing") - onFailure() - return Result.failure() - } - // Mark download as completed onSuccess() return Result.success() @@ -237,47 +230,29 @@ class DownloadWorker @AssistedInject constructor( } } - private fun getDownloadRequest(files: List, libPackageName: String?): List { - val downloadList = mutableListOf() - files.filter { it.url.isNotBlank() }.forEach { - val file = when (it.type) { - GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> { - PathUtil.getApkDownloadFile( - appContext, download.packageName, download.versionCode, it, libPackageName - ) - } - - GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> { - PathUtil.getObbDownloadFile(download.packageName, it) - } - } - downloadList.add(Request(it.url, file, it.size, it.sha1, it.sha256)) - } - return downloadList - } - /** * Downloads the file from the given request. * Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them. - * @param request A [Request] to download + * @param gFile A [GPlayFile] to download * @return A [Result] indicating whether the file was downloaded or not. */ - private suspend fun downloadFile(request: Request): Result { + private suspend fun downloadFile(gFile: GPlayFile): Result { return withContext(Dispatchers.IO) { - val algorithm = if (request.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256 - val expectedSha = if (algorithm == Algorithm.SHA1) request.sha1 else request.sha256 + val file = PathUtil.getLocalFile(appContext, gFile, download) + val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256 + val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256 // If file exists and sha matches the request, no need to download again - if (request.file.exists() && validSha(request.file, expectedSha, algorithm)) { - Log.i(TAG, "${request.file} is already downloaded!") - downloadedBytes += request.file.length() + if (file.exists() && validSha(file, expectedSha, algorithm)) { + Log.i(TAG, "$file is already downloaded!") + downloadedBytes += file.length() return@withContext Result.success() } try { // Download as a temporary file to avoid installing corrupted files val tmpFileSuffix = ".tmp" - val tmpFile = File(request.file.absolutePath + tmpFileSuffix) + val tmpFile = File(file.absolutePath + tmpFileSuffix) val isNewFile = tmpFile.createNewFile() val okHttpClient = httpClient as HttpClient @@ -289,9 +264,9 @@ class DownloadWorker @AssistedInject constructor( headers["Range"] = "bytes=${tmpFile.length()}-" } - okHttpClient.call(request.url, headers).body?.byteStream()?.use { input -> + okHttpClient.call(gFile.url, headers).body?.byteStream()?.use { input -> FileOutputStream(tmpFile, !isNewFile).use { - input.copyTo(it, request.size).collect { p -> onProgress(p) } + input.copyTo(it, gFile.size).collect { p -> onProgress(p) } } } @@ -300,13 +275,13 @@ class DownloadWorker @AssistedInject constructor( throw Exception("Incorrect hash for $tmpFile") } - if (!tmpFile.renameTo(request.file)) { + if (!tmpFile.renameTo(file)) { throw Exception("Failed to remove .tmp extension from $tmpFile") } return@withContext Result.success() } catch (exception: Exception) { - Log.e(TAG, "Failed to download ${request.file}!", exception) + Log.e(TAG, "Failed to download $file!", exception) notifyStatus(DownloadStatus.FAILED) return@withContext Result.failure() } diff --git a/app/src/main/java/com/aurora/store/util/PathUtil.kt b/app/src/main/java/com/aurora/store/util/PathUtil.kt index 948073b30..22758989d 100644 --- a/app/src/main/java/com/aurora/store/util/PathUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PathUtil.kt @@ -21,6 +21,7 @@ package com.aurora.store.util import android.content.Context import android.os.Environment +import com.aurora.store.data.room.download.Download import java.io.File import java.util.UUID import com.aurora.gplayapi.data.models.File as GPlayFile @@ -62,19 +63,33 @@ object PathUtil { ) } - fun getApkDownloadFile( - context: Context, - packageName: String, - versionCode: Int, - file: GPlayFile, - sharedLibPackageName: String? = null - ): File { - val downloadDir = if (!sharedLibPackageName.isNullOrBlank()) { - getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName) - } else { - getAppDownloadDir(context, packageName, versionCode) + /** + * Returns an instance of java's [File] class for the given [GPlayFile] + * @param context [Context] + * @param gFile [GPlayFile] to download + * @param download An instance of [Download] + */ + fun getLocalFile(context: Context, gFile: GPlayFile, download: Download): File { + val isSharedLib = download.sharedLibs.any { it.fileList.contains(gFile) } + return when (gFile.type) { + GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> { + val downloadDir = if (isSharedLib) { + getLibDownloadDir( + context, + download.packageName, + download.versionCode, + download.packageName + ) + } else { + getAppDownloadDir(context, download.packageName, download.versionCode) + } + return File(downloadDir, gFile.name) + } + + GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> { + File(getObbDownloadDir(download.packageName), gFile.name) + } } - return File(downloadDir, file.name) } fun getZipFile(context: Context, packageName: String, versionCode: Int): File { @@ -91,10 +106,6 @@ object PathUtil { return File(Environment.getExternalStorageDirectory(), "/Android/obb/$packageName") } - fun getObbDownloadFile(packageName: String, file: GPlayFile): File { - return File(getObbDownloadDir(packageName), file.name) - } - fun getSpoofDirectory(context: Context): File { return File(context.filesDir, SPOOF) } @@ -105,15 +116,5 @@ object PathUtil { file.createNewFile() return file } - - fun canReadWriteOBB(context: Context): Boolean { - val obbDir = context.obbDir.parentFile - - if (obbDir != null) { - return obbDir.exists() && obbDir.canRead() && obbDir.canWrite() - } - - return false - } } From 42f87b5f642f711e91ff621841fbd11453d0e020 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Fri, 20 Dec 2024 12:55:22 +0700 Subject: [PATCH 2/3] DownloadWorker: Introduce a new verifying status for download This would be useful we implement frosting block verification Signed-off-by: Aayush Gupta --- .../aurora/store/data/model/DownloadStatus.kt | 3 +- .../aurora/store/data/work/DownloadWorker.kt | 36 +++++++++++-------- .../custom/layouts/button/UpdateButton.kt | 7 +++- .../view/epoxy/views/app/AppUpdateView.kt | 1 + .../view/ui/details/AppDetailsFragment.kt | 17 ++++++++- app/src/main/res/values/strings.xml | 1 + 6 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt b/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt index 1b58c01b4..4e1a8596a 100644 --- a/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt +++ b/app/src/main/java/com/aurora/store/data/model/DownloadStatus.kt @@ -9,7 +9,8 @@ enum class DownloadStatus(@StringRes val localized: Int) { CANCELLED(R.string.status_cancelled), COMPLETED(R.string.status_completed), QUEUED(R.string.status_queued), - UNAVAILABLE(R.string.status_unavailable); + UNAVAILABLE(R.string.status_unavailable), + VERIFYING(R.string.status_verifying); companion object { val finished = listOf(FAILED, CANCELLED, COMPLETED) diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index 030681653..301aacf43 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -168,6 +168,17 @@ class DownloadWorker @AssistedInject constructor( } } + 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() + return Result.failure() + } + // Mark download as completed onSuccess() return Result.success() @@ -239,11 +250,9 @@ class DownloadWorker @AssistedInject constructor( private suspend fun downloadFile(gFile: GPlayFile): Result { return withContext(Dispatchers.IO) { val file = PathUtil.getLocalFile(appContext, gFile, download) - val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256 - val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256 - // If file exists and sha matches the request, no need to download again - if (file.exists() && validSha(file, expectedSha, algorithm)) { + // If file exists and has integrity intact, no need to download again + if (file.exists() && verifyFile(gFile)) { Log.i(TAG, "$file is already downloaded!") downloadedBytes += file.length() return@withContext Result.success() @@ -270,11 +279,6 @@ class DownloadWorker @AssistedInject constructor( } } - // Ensure downloaded file matches expected sha - if (!validSha(tmpFile, expectedSha, algorithm)) { - throw Exception("Incorrect hash for $tmpFile") - } - if (!tmpFile.renameTo(file)) { throw Exception("Failed to remove .tmp extension from $tmpFile") } @@ -346,6 +350,7 @@ class DownloadWorker @AssistedInject constructor( downloadDao.updateStatus(download.packageName, status) when (status) { + DownloadStatus.VERIFYING, DownloadStatus.CANCELLED -> return DownloadStatus.COMPLETED -> { // Mark progress as 100 manually to avoid race conditions @@ -362,14 +367,15 @@ class DownloadWorker @AssistedInject constructor( } /** - * Validates whether given file has the expected SHA hash sum. - * @param file [File] to check - * @param expectedSha Expected SHA hash sum - * @param algorithm [Algorithm] of the SHA - * @return A boolean whether the given file has the expected SHA or not. + * Verifies integrity of a downloaded [GPlayFile]. + * @param gFile [GPlayFile] to verify */ @OptIn(ExperimentalStdlibApi::class) - private suspend fun validSha(file: File, expectedSha: String, algorithm: Algorithm): Boolean { + private suspend fun verifyFile(gFile: GPlayFile): Boolean { + val file = PathUtil.getLocalFile(appContext, gFile, download) + val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256 + val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256 + return withContext(Dispatchers.IO) { val messageDigest = MessageDigest.getInstance(algorithm.value) DigestInputStream(file.inputStream(), messageDigest).use { input -> diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt index 66d8a6168..54c195377 100644 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt @@ -55,7 +55,9 @@ class UpdateButton : RelativeLayout { fun updateState(downloadStatus: DownloadStatus) { val displayChild = when (downloadStatus) { DownloadStatus.QUEUED, - DownloadStatus.DOWNLOADING -> 1 + DownloadStatus.DOWNLOADING, + DownloadStatus.VERIFYING -> 1 + else -> 0 } @@ -64,6 +66,9 @@ class UpdateButton : RelativeLayout { binding.viewFlipper.displayedChild = displayChild } } + + // Not allowed to cancel installation at this point + binding.btnNegative.isEnabled = downloadStatus != DownloadStatus.VERIFYING } fun addPositiveOnClickListener(onClickListener: OnClickListener?) { diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt index 2652a470b..f29244cc3 100644 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt @@ -104,6 +104,7 @@ class AppUpdateView @JvmOverloads constructor( if (download != null) { binding.btnAction.updateState(download.downloadStatus) when (download.downloadStatus) { + DownloadStatus.VERIFYING, DownloadStatus.QUEUED -> { binding.progressDownload.isIndeterminate = true animateImageView(scaleFactor = 0.75f) diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index 3453781f1..fc3eb6bdc 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -228,11 +228,26 @@ class AppDetailsFragment : BaseFragment() { setOnClickListener { openApp() } } binding.layoutDetailsApp.btnSecondaryAction.apply { + isEnabled = true text = getString(R.string.action_cancel) setOnClickListener { viewModel.cancelDownload(app) } } } + DownloadStatus.VERIFYING -> { + transformIcon(true) + binding.layoutDetailsApp.btnPrimaryAction.apply { + isEnabled = false + text = getString(R.string.action_open) + setOnClickListener(null) + } + binding.layoutDetailsApp.btnSecondaryAction.apply { + isEnabled = false + text = getString(R.string.action_cancel) + setOnClickListener(null) + } + } + else -> checkAndSetupInstall() } }.launchIn(viewLifecycleOwner.lifecycleScope) @@ -572,7 +587,7 @@ class AppDetailsFragment : BaseFragment() { // Setup primary and secondary action buttons binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true - binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true + binding.layoutDetailsApp.btnSecondaryAction.isEnabled = true if (app.isInstalled) { val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72d6baf6b..2317e8000 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,6 +471,7 @@ Completed Queued Unavailable + Verifying Authentication required From d48e24f37603af0d736c75fd7c35da171dd547b5 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Sat, 21 Dec 2024 12:35:39 +0700 Subject: [PATCH 3/3] AppDetailsFragment: Also show Unarchive as primary action Show action text as "Unarchive" if the package was archived instead of "Install" Signed-off-by: Aayush Gupta --- .../main/java/com/aurora/store/util/PackageUtil.kt | 9 +++++++++ .../store/view/ui/details/AppDetailsFragment.kt | 12 +++++++++--- app/src/main/res/values/strings.xml | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index e3491cc3b..1ad600a6e 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -40,6 +40,7 @@ import com.aurora.extensions.isHuawei import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isTAndAbove +import com.aurora.extensions.isVAndAbove import com.aurora.extensions.isValidApp import com.aurora.store.BuildConfig import com.aurora.store.R @@ -95,6 +96,14 @@ object PackageUtil { } } + fun isArchived(context: Context, packageName: String): Boolean { + return try { + isVAndAbove && context.packageManager.getArchivedPackage(packageName) != null + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + fun isSharedLibrary(context: Context, packageName: String): Boolean { return if (isOAndAbove) { getAllSharedLibraries(context).any { it.name == packageName } diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index fc3eb6bdc..3a27befd8 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -624,10 +624,16 @@ class AppDetailsFragment : BaseFragment() { } } } else { - if (app.isFree) { - binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) + if (PackageUtil.isArchived(requireContext(), app.packageName)) { + binding.layoutDetailsApp.btnPrimaryAction.text = + getString(R.string.action_unarchive) } else { - binding.layoutDetailsApp.btnPrimaryAction.text = app.price + if (app.isFree) { + binding.layoutDetailsApp.btnPrimaryAction.text = + getString(R.string.action_install) + } else { + binding.layoutDetailsApp.btnPrimaryAction.text = app.price + } } binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2317e8000..2b088d12e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -480,4 +480,7 @@ Default Available + + + Unarchive