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