From 4d4429fd7ad754155bc1b3bfe666e235971efa00 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 28 Dec 2023 20:45:05 +0530 Subject: [PATCH] DownloadWorker: Verify downloaded files using SHA256 or SHA1 This lets us avoid downloading same files again Signed-off-by: Aayush Gupta --- .../com/aurora/store/data/model/Request.kt | 4 +- .../aurora/store/data/work/DownloadWorker.kt | 47 +++++++++++++-- .../custom/layouts/button/ActionButton.kt | 4 -- .../custom/layouts/button/UpdateButton.kt | 6 -- .../view/epoxy/views/app/AppUpdateView.kt | 21 ------- .../view/ui/details/AppDetailsFragment.kt | 60 +------------------ .../store/view/ui/updates/UpdatesFragment.kt | 21 ------- .../store/viewmodel/all/UpdatesViewModel.kt | 15 ----- .../viewmodel/details/AppDetailsViewModel.kt | 14 ----- .../main/res/layout/view_update_button.xml | 12 ---- 10 files changed, 46 insertions(+), 158 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/model/Request.kt b/app/src/main/java/com/aurora/store/data/model/Request.kt index 6f628e25e..e283ed9ab 100644 --- a/app/src/main/java/com/aurora/store/data/model/Request.kt +++ b/app/src/main/java/com/aurora/store/data/model/Request.kt @@ -3,5 +3,7 @@ package com.aurora.store.data.model data class Request( val url: String, val filePath: String, - val size: Long + val size: Long, + val sha1: String, + val sha256: String ) 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 39e48f576..99fe07b63 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 @@ -33,6 +33,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.withContext import java.io.File import java.net.URL +import java.security.DigestInputStream +import java.security.MessageDigest import kotlinx.coroutines.NonCancellable import kotlin.properties.Delegates import com.aurora.gplayapi.data.models.File as GPlayFile @@ -191,27 +193,40 @@ class DownloadWorker @AssistedInject constructor( PathUtil.getObbDownloadFile(download.packageName, it) } } - downloadList.add(Request(it.url, filePath, it.size)) + downloadList.add(Request(it.url, filePath, it.size, it.sha1, it.sha256)) } return downloadList } + @OptIn(ExperimentalStdlibApi::class) private suspend fun downloadFile(request: Request): Result { return withContext(Dispatchers.IO) { val requestFile = File(request.filePath) + + // If file exists, no need to download again + if (!shouldDownload(request)) { + Log.i(TAG, "$requestFile already exists") + return@withContext Result.success() + } + try { + val algorithm = if (request.sha256.isBlank()) "SHA-1" else "SHA-256" + val messageDigest = MessageDigest.getInstance(algorithm) + requestFile.createNewFile() - URL(request.url).openStream().use { input -> + DigestInputStream(URL(request.url).openStream(), messageDigest).use { input -> requestFile.outputStream().use { input.copyTo(it, request.size).collectLatest { p -> onProgress(p) } } } - // Ensure downloaded file exists - if (!File(request.filePath).exists()) { - Log.e(TAG, "Failed to find downloaded file at ${request.filePath}") + + val sha = messageDigest.digest().toHexString() + if (!File(request.filePath).exists() || !(sha == request.sha1 || sha == request.sha256)) { + Log.e(TAG, "$requestFile is either missing or corrupt") notifyStatus(DownloadStatus.FAILED) return@withContext Result.failure() } + return@withContext Result.success() } catch (exception: Exception) { Log.e(TAG, "Failed to download ${request.filePath}!", exception) @@ -277,4 +292,26 @@ class DownloadWorker @AssistedInject constructor( val notificationID = if (dID != -1) dID else download.packageName.hashCode() notificationManager.notify(notificationID, notification) } + + @OptIn(ExperimentalStdlibApi::class) + private suspend fun shouldDownload(request: Request): Boolean { + return withContext(Dispatchers.IO) { + val file = File(request.filePath) + if (file.exists()) { + val algorithm = if (request.sha256.isBlank()) "SHA-1" else "SHA-256" + val messageDigest = MessageDigest.getInstance(algorithm) + DigestInputStream(file.inputStream(), messageDigest).use { input -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read = input.read(buffer, 0, DEFAULT_BUFFER_SIZE) + while (read > -1) { + read = input.read(buffer, 0, DEFAULT_BUFFER_SIZE) + } + } + val sha = messageDigest.digest().toHexString() + return@withContext !(sha == request.sha1 || sha == request.sha256) + } else { + return@withContext true + } + } + } } diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt index b1a24bff9..c944e6025 100644 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt @@ -129,8 +129,4 @@ class ActionButton : RelativeLayout { fun addOnClickListener(onClickListener: OnClickListener?) { B.btn.setOnClickListener(onClickListener) } - - fun addOnLongClickListener(onLongClickListener: OnLongClickListener?) { - B.btn.setOnLongClickListener(onLongClickListener) - } } 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 6a48705e3..f8261d32a 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 @@ -25,7 +25,6 @@ import android.widget.RelativeLayout import com.aurora.extensions.getString import com.aurora.extensions.runOnUiThread import com.aurora.store.R -import com.aurora.store.State import com.aurora.store.data.model.DownloadStatus import com.aurora.store.databinding.ViewUpdateButtonBinding @@ -77,7 +76,6 @@ class UpdateButton : RelativeLayout { val displayChild = when (downloadStatus) { DownloadStatus.QUEUED, DownloadStatus.DOWNLOADING -> 2 - DownloadStatus.COMPLETED -> 3 else -> 0 } @@ -95,8 +93,4 @@ class UpdateButton : RelativeLayout { fun addNegativeOnClickListener(onClickListener: OnClickListener?) { B.btnNegative.setOnClickListener(onClickListener) } - - fun addInstallOnClickListener(onClickListener: OnClickListener?) { - B.btnInstall.setOnClickListener(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 07377e07a..eccb43432 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 @@ -44,9 +44,7 @@ import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.download.Download import com.aurora.store.databinding.ViewAppUpdateBinding import com.aurora.store.util.CommonUtil -import com.aurora.store.util.PathUtil import com.aurora.store.view.epoxy.views.BaseView -import java.io.File @ModelView( autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, @@ -135,20 +133,6 @@ class AppUpdateView : RelativeLayout { } } } - DownloadStatus.COMPLETED -> { - B.progressDownload.invisible() - - // It is possible that while the app was downloaded in past, files have been removed - // Check once again to reflect appropriate status - val files = File( - PathUtil.getAppDownloadDir( - context, - download.packageName, - download.versionCode - ).path - ).listFiles() - if (files.isNullOrEmpty()) B.btnAction.updateState(DownloadStatus.UNAVAILABLE) - } else -> { B.progressDownload.progress = 0 B.progressDownload.invisible() @@ -172,11 +156,6 @@ class AppUpdateView : RelativeLayout { B.btnAction.addNegativeOnClickListener(onClickListener) } - @CallbackProp - fun installAction(onClickListener: OnClickListener?) { - B.btnAction.addInstallOnClickListener(onClickListener) - } - @CallbackProp fun longClick(onClickListener: OnLongClickListener?) { B.layoutContent.setOnLongClickListener(onClickListener) 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 8a9b17b3e..033a01353 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 @@ -52,7 +52,6 @@ import com.aurora.extensions.isRAndAbove import com.aurora.extensions.runOnUiThread import com.aurora.extensions.share import com.aurora.extensions.show -import com.aurora.extensions.showDialog import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -96,7 +95,6 @@ import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.io.File import java.util.Locale import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter @@ -437,31 +435,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { } } - @Synchronized - private fun install() { - val files = File( - PathUtil.getAppDownloadDir( - requireContext(), - app.packageName, - app.versionCode - ).path - ).listFiles() ?: return - - val apkFiles = files.filter { it.path.endsWith(".apk") } - val preferredInstaller = - Preferences.getInteger(requireContext(), Preferences.PREFERENCE_INSTALLER_ID) - - if (apkFiles.size > 1 && preferredInstaller == 1) { - showDialog(R.string.title_installer, R.string.dialog_desc_native_split) - } else { - viewModel.install(requireContext(), app.packageName, apkFiles) - - runOnUiThread { - binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) - } - } - } - @Synchronized private fun uninstallApp() { val installer = AppInstaller.getInstance(requireContext()).getPreferredInstaller() @@ -554,31 +527,13 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { } @Synchronized - private fun startDownload(forceDownload: Boolean = false) { + private fun startDownload() { when (downloadStatus) { DownloadStatus.DOWNLOADING -> { flip(1) toast("Already downloading") } - DownloadStatus.COMPLETED -> { - if (forceDownload) { - purchase() - return - } - - // Check if files are still present, else proceed to re-download the app - val files = File( - PathUtil.getAppDownloadDir( - requireContext(), - app.packageName, - app.versionCode - ).path - ).listFiles() - - if (files?.isNotEmpty() == true) install() else purchase() - } - else -> { flip(1) purchase() @@ -660,10 +615,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { ("$installedVersion ➔ ${app.versionName} (${app.versionCode})") btn.setText(R.string.action_update) btn.addOnClickListener { startDownload() } - btn.addOnLongClickListener { - startDownload(true) - true - } } else { binding.layoutDetailsApp.txtLine3.text = installedVersion btn.setText(R.string.action_open) @@ -689,15 +640,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { startDownload() } } - btn.addOnLongClickListener { - if (authData.isAnonymous && !app.isFree) { - toast(R.string.toast_purchase_blocked) - } else { - btn.setText(R.string.download_metadata) - startDownload(true) - } - true - } if (uninstallActionEnabled) { binding.layoutDetailsToolbar.toolbar.invalidateMenu() } diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index 537cc8740..f97abf59b 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -30,13 +30,11 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import com.aurora.extensions.isRAndAbove import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App import com.aurora.store.R import com.aurora.store.data.event.BusEvent -import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.room.download.Download import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.util.PathUtil @@ -48,7 +46,6 @@ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.all.UpdatesViewModel import dagger.hilt.android.AndroidEntryPoint -import java.io.File import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -186,24 +183,6 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) { } .positiveAction { _ -> updateSingle(app) } .negativeAction { _ -> cancelSingle(app) } - .installAction { _ -> - val files = File( - PathUtil.getAppDownloadDir( - requireContext(), - app.packageName, - app.versionCode - ).path - ).listFiles() - - // Downloaded files are missing, trigger re-download - if (files.isNullOrEmpty()) { - updateSingle(app) - return@installAction - } - - val apkFiles = files.filter { it.path.endsWith(".apk") } - viewModel.install(requireContext(), app.packageName, apkFiles) - } ) } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt index f9dd10218..29d37e60a 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt @@ -20,15 +20,12 @@ package com.aurora.store.viewmodel.all import android.app.Application -import android.content.Context import android.util.Log import androidx.core.content.pm.PackageInfoCompat import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.App -import com.aurora.store.data.installer.AppInstaller import com.aurora.store.util.DownloadWorkerUtil import dagger.hilt.android.lifecycle.HiltViewModel -import java.io.File import java.util.Locale import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -70,18 +67,6 @@ class UpdatesViewModel @Inject constructor( } } - @Synchronized - fun install(context: Context, packageName: String, files: List) { - try { - viewModelScope.launch(Dispatchers.IO) { - AppInstaller.getInstance(context).getPreferredInstaller() - .install(packageName, files) - } - } catch (exception: Exception) { - Log.e(TAG, "Failed to install app", exception) - } - } - fun download(app: App) { viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index 4edf2cba1..72a4417cf 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -9,7 +9,6 @@ import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.details.TestingProgramStatus import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.ReviewsHelper -import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.Report import com.aurora.store.data.network.HttpClient @@ -17,7 +16,6 @@ import com.aurora.store.data.providers.AuthProvider import com.aurora.store.util.DownloadWorkerUtil import com.google.gson.GsonBuilder import dagger.hilt.android.lifecycle.HiltViewModel -import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -146,18 +144,6 @@ class AppDetailsViewModel @Inject constructor( viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) } } - @Synchronized - fun install(context: Context, packageName: String, files: List) { - try { - viewModelScope.launch(Dispatchers.IO) { - AppInstaller.getInstance(context).getPreferredInstaller() - .install(packageName, files) - } - } catch (exception: Exception) { - Log.e(TAG, "Failed to install app", exception) - } - } - private fun parseResponse(response: String, packageName: String): List { try { val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC) diff --git a/app/src/main/res/layout/view_update_button.xml b/app/src/main/res/layout/view_update_button.xml index 8e4093b29..1e070d22c 100644 --- a/app/src/main/res/layout/view_update_button.xml +++ b/app/src/main/res/layout/view_update_button.xml @@ -70,18 +70,6 @@ android:textAppearance="@style/TextAppearance.Aurora.Line1" /> - - - -