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:
@@ -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)
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user