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