DownloadWorker: Verify downloaded files using SHA256 or SHA1
This lets us avoid downloading same files again Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,4 @@ class ActionButton : RelativeLayout {
|
||||
fun addOnClickListener(onClickListener: OnClickListener?) {
|
||||
B.btn.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun addOnLongClickListener(onLongClickListener: OnLongClickListener?) {
|
||||
B.btn.setOnLongClickListener(onLongClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<File>) {
|
||||
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) }
|
||||
}
|
||||
|
||||
@@ -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<File>) {
|
||||
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<Report> {
|
||||
try {
|
||||
val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
|
||||
|
||||
@@ -70,18 +70,6 @@
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_install"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_install"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
|
||||
</RelativeLayout>
|
||||
</ViewFlipper>
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user