DownloadWorker: Introduce a new verifying status for download

This would be useful we implement frosting block verification

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-12-20 12:55:22 +07:00
parent 7fdf06d73a
commit 42f87b5f64
6 changed files with 47 additions and 18 deletions

View File

@@ -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)

View File

@@ -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 ->

View File

@@ -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?) {

View File

@@ -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)

View File

@@ -228,11 +228,26 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
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<FragmentDetailsBinding>() {
// 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())

View File

@@ -471,6 +471,7 @@
<string name="status_completed">Completed</string>
<string name="status_queued">Queued</string>
<string name="status_unavailable">Unavailable</string>
<string name="status_verifying">Verifying</string>
<!-- UnarchivePackageReceiver -->
<string name="authentication_required_title">Authentication required</string>