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), CANCELLED(R.string.status_cancelled),
COMPLETED(R.string.status_completed), COMPLETED(R.string.status_completed),
QUEUED(R.string.status_queued), QUEUED(R.string.status_queued),
UNAVAILABLE(R.string.status_unavailable); UNAVAILABLE(R.string.status_unavailable),
VERIFYING(R.string.status_verifying);
companion object { companion object {
val finished = listOf(FAILED, CANCELLED, COMPLETED) 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 // Mark download as completed
onSuccess() onSuccess()
return Result.success() return Result.success()
@@ -239,11 +250,9 @@ class DownloadWorker @AssistedInject constructor(
private suspend fun downloadFile(gFile: GPlayFile): Result { private suspend fun downloadFile(gFile: GPlayFile): Result {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val file = PathUtil.getLocalFile(appContext, gFile, download) 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 and has integrity intact, no need to download again
if (file.exists() && validSha(file, expectedSha, algorithm)) { if (file.exists() && verifyFile(gFile)) {
Log.i(TAG, "$file is already downloaded!") Log.i(TAG, "$file is already downloaded!")
downloadedBytes += file.length() downloadedBytes += file.length()
return@withContext Result.success() 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)) { if (!tmpFile.renameTo(file)) {
throw Exception("Failed to remove .tmp extension from $tmpFile") throw Exception("Failed to remove .tmp extension from $tmpFile")
} }
@@ -346,6 +350,7 @@ class DownloadWorker @AssistedInject constructor(
downloadDao.updateStatus(download.packageName, status) downloadDao.updateStatus(download.packageName, status)
when (status) { when (status) {
DownloadStatus.VERIFYING,
DownloadStatus.CANCELLED -> return DownloadStatus.CANCELLED -> return
DownloadStatus.COMPLETED -> { DownloadStatus.COMPLETED -> {
// Mark progress as 100 manually to avoid race conditions // 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. * Verifies integrity of a downloaded [GPlayFile].
* @param file [File] to check * @param gFile [GPlayFile] to verify
* @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.
*/ */
@OptIn(ExperimentalStdlibApi::class) @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) { return withContext(Dispatchers.IO) {
val messageDigest = MessageDigest.getInstance(algorithm.value) val messageDigest = MessageDigest.getInstance(algorithm.value)
DigestInputStream(file.inputStream(), messageDigest).use { input -> DigestInputStream(file.inputStream(), messageDigest).use { input ->

View File

@@ -55,7 +55,9 @@ class UpdateButton : RelativeLayout {
fun updateState(downloadStatus: DownloadStatus) { fun updateState(downloadStatus: DownloadStatus) {
val displayChild = when (downloadStatus) { val displayChild = when (downloadStatus) {
DownloadStatus.QUEUED, DownloadStatus.QUEUED,
DownloadStatus.DOWNLOADING -> 1 DownloadStatus.DOWNLOADING,
DownloadStatus.VERIFYING -> 1
else -> 0 else -> 0
} }
@@ -64,6 +66,9 @@ class UpdateButton : RelativeLayout {
binding.viewFlipper.displayedChild = displayChild binding.viewFlipper.displayedChild = displayChild
} }
} }
// Not allowed to cancel installation at this point
binding.btnNegative.isEnabled = downloadStatus != DownloadStatus.VERIFYING
} }
fun addPositiveOnClickListener(onClickListener: OnClickListener?) { fun addPositiveOnClickListener(onClickListener: OnClickListener?) {

View File

@@ -104,6 +104,7 @@ class AppUpdateView @JvmOverloads constructor(
if (download != null) { if (download != null) {
binding.btnAction.updateState(download.downloadStatus) binding.btnAction.updateState(download.downloadStatus)
when (download.downloadStatus) { when (download.downloadStatus) {
DownloadStatus.VERIFYING,
DownloadStatus.QUEUED -> { DownloadStatus.QUEUED -> {
binding.progressDownload.isIndeterminate = true binding.progressDownload.isIndeterminate = true
animateImageView(scaleFactor = 0.75f) animateImageView(scaleFactor = 0.75f)

View File

@@ -228,11 +228,26 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
setOnClickListener { openApp() } setOnClickListener { openApp() }
} }
binding.layoutDetailsApp.btnSecondaryAction.apply { binding.layoutDetailsApp.btnSecondaryAction.apply {
isEnabled = true
text = getString(R.string.action_cancel) text = getString(R.string.action_cancel)
setOnClickListener { viewModel.cancelDownload(app) } 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() else -> checkAndSetupInstall()
} }
}.launchIn(viewLifecycleOwner.lifecycleScope) }.launchIn(viewLifecycleOwner.lifecycleScope)
@@ -572,7 +587,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
// Setup primary and secondary action buttons // Setup primary and secondary action buttons
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true binding.layoutDetailsApp.btnSecondaryAction.isEnabled = true
if (app.isInstalled) { if (app.isInstalled) {
val isUpdatable = PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong()) 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_completed">Completed</string>
<string name="status_queued">Queued</string> <string name="status_queued">Queued</string>
<string name="status_unavailable">Unavailable</string> <string name="status_unavailable">Unavailable</string>
<string name="status_verifying">Verifying</string>
<!-- UnarchivePackageReceiver --> <!-- UnarchivePackageReceiver -->
<string name="authentication_required_title">Authentication required</string> <string name="authentication_required_title">Authentication required</string>