Skip to content
Snippets Groups Projects
Commit 3959413a authored by Aayush Gupta's avatar Aayush Gupta
Browse files

Merge branch 'dev'

parents 39875c28 d48e24f3
No related branches found
No related tags found
No related merge requests found
......@@ -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)
......
package com.aurora.store.data.model
import java.io.File
data class Request(
val url: String,
val file: File,
val size: Long,
val sha1: String,
val sha256: String
)
......@@ -27,7 +27,6 @@ import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.Algorithm
import com.aurora.store.data.model.DownloadInfo
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.Request
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.download.Download
......@@ -125,7 +124,7 @@ class DownloadWorker @AssistedInject constructor(
}
// Purchase the app (free apps needs to be purchased too)
val requestList = mutableListOf<Request>()
val requestList = mutableListOf<GPlayFile>()
if (download.sharedLibs.isNotEmpty()) {
download.sharedLibs.forEach {
PathUtil.getLibDownloadDir(
......@@ -135,10 +134,10 @@ class DownloadWorker @AssistedInject constructor(
it.packageName
).mkdirs()
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
requestList.addAll(getDownloadRequest(it.fileList, it.packageName))
requestList.addAll(it.fileList)
}
}
requestList.addAll(getDownloadRequest(download.fileList, null))
requestList.addAll(download.fileList)
// Update data for notification
download.totalFiles = requestList.size
......@@ -169,8 +168,13 @@ class DownloadWorker @AssistedInject constructor(
}
}
if (!requestList.all { it.file.exists() }) {
Log.e(TAG, "Downloaded files are missing")
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()
}
......@@ -237,47 +241,27 @@ class DownloadWorker @AssistedInject constructor(
}
}
private fun getDownloadRequest(files: List<GPlayFile>, libPackageName: String?): List<Request> {
val downloadList = mutableListOf<Request>()
files.filter { it.url.isNotBlank() }.forEach {
val file = when (it.type) {
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
PathUtil.getApkDownloadFile(
appContext, download.packageName, download.versionCode, it, libPackageName
)
}
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> {
PathUtil.getObbDownloadFile(download.packageName, it)
}
}
downloadList.add(Request(it.url, file, it.size, it.sha1, it.sha256))
}
return downloadList
}
/**
* Downloads the file from the given request.
* Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them.
* @param request A [Request] to download
* @param gFile A [GPlayFile] to download
* @return A [Result] indicating whether the file was downloaded or not.
*/
private suspend fun downloadFile(request: Request): Result {
private suspend fun downloadFile(gFile: GPlayFile): Result {
return withContext(Dispatchers.IO) {
val algorithm = if (request.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
val expectedSha = if (algorithm == Algorithm.SHA1) request.sha1 else request.sha256
val file = PathUtil.getLocalFile(appContext, gFile, download)
// If file exists and sha matches the request, no need to download again
if (request.file.exists() && validSha(request.file, expectedSha, algorithm)) {
Log.i(TAG, "${request.file} is already downloaded!")
downloadedBytes += request.file.length()
// 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()
}
try {
// Download as a temporary file to avoid installing corrupted files
val tmpFileSuffix = ".tmp"
val tmpFile = File(request.file.absolutePath + tmpFileSuffix)
val tmpFile = File(file.absolutePath + tmpFileSuffix)
val isNewFile = tmpFile.createNewFile()
val okHttpClient = httpClient as HttpClient
......@@ -289,24 +273,19 @@ class DownloadWorker @AssistedInject constructor(
headers["Range"] = "bytes=${tmpFile.length()}-"
}
okHttpClient.call(request.url, headers).body?.byteStream()?.use { input ->
okHttpClient.call(gFile.url, headers).body?.byteStream()?.use { input ->
FileOutputStream(tmpFile, !isNewFile).use {
input.copyTo(it, request.size).collect { p -> onProgress(p) }
input.copyTo(it, gFile.size).collect { p -> onProgress(p) }
}
}
// Ensure downloaded file matches expected sha
if (!validSha(tmpFile, expectedSha, algorithm)) {
throw Exception("Incorrect hash for $tmpFile")
}
if (!tmpFile.renameTo(request.file)) {
if (!tmpFile.renameTo(file)) {
throw Exception("Failed to remove .tmp extension from $tmpFile")
}
return@withContext Result.success()
} catch (exception: Exception) {
Log.e(TAG, "Failed to download ${request.file}!", exception)
Log.e(TAG, "Failed to download $file!", exception)
notifyStatus(DownloadStatus.FAILED)
return@withContext Result.failure()
}
......@@ -371,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
......@@ -387,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 ->
......
......@@ -40,6 +40,7 @@ import com.aurora.extensions.isHuawei
import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isVAndAbove
import com.aurora.extensions.isValidApp
import com.aurora.store.BuildConfig
import com.aurora.store.R
......@@ -95,6 +96,14 @@ object PackageUtil {
}
}
fun isArchived(context: Context, packageName: String): Boolean {
return try {
isVAndAbove && context.packageManager.getArchivedPackage(packageName) != null
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun isSharedLibrary(context: Context, packageName: String): Boolean {
return if (isOAndAbove) {
getAllSharedLibraries(context).any { it.name == packageName }
......
......@@ -21,6 +21,7 @@ package com.aurora.store.util
import android.content.Context
import android.os.Environment
import com.aurora.store.data.room.download.Download
import java.io.File
import java.util.UUID
import com.aurora.gplayapi.data.models.File as GPlayFile
......@@ -62,19 +63,33 @@ object PathUtil {
)
}
fun getApkDownloadFile(
context: Context,
packageName: String,
versionCode: Int,
file: GPlayFile,
sharedLibPackageName: String? = null
): File {
val downloadDir = if (!sharedLibPackageName.isNullOrBlank()) {
getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName)
/**
* Returns an instance of java's [File] class for the given [GPlayFile]
* @param context [Context]
* @param gFile [GPlayFile] to download
* @param download An instance of [Download]
*/
fun getLocalFile(context: Context, gFile: GPlayFile, download: Download): File {
val isSharedLib = download.sharedLibs.any { it.fileList.contains(gFile) }
return when (gFile.type) {
GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
val downloadDir = if (isSharedLib) {
getLibDownloadDir(
context,
download.packageName,
download.versionCode,
download.packageName
)
} else {
getAppDownloadDir(context, packageName, versionCode)
getAppDownloadDir(context, download.packageName, download.versionCode)
}
return File(downloadDir, gFile.name)
}
GPlayFile.FileType.OBB, GPlayFile.FileType.PATCH -> {
File(getObbDownloadDir(download.packageName), gFile.name)
}
}
return File(downloadDir, file.name)
}
fun getZipFile(context: Context, packageName: String, versionCode: Int): File {
......@@ -91,10 +106,6 @@ object PathUtil {
return File(Environment.getExternalStorageDirectory(), "/Android/obb/$packageName")
}
fun getObbDownloadFile(packageName: String, file: GPlayFile): File {
return File(getObbDownloadDir(packageName), file.name)
}
fun getSpoofDirectory(context: Context): File {
return File(context.filesDir, SPOOF)
}
......@@ -105,15 +116,5 @@ object PathUtil {
file.createNewFile()
return file
}
fun canReadWriteOBB(context: Context): Boolean {
val obbDir = context.obbDir.parentFile
if (obbDir != null) {
return obbDir.exists() && obbDir.canRead() && obbDir.canWrite()
}
return false
}
}
......@@ -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())
......@@ -608,12 +623,18 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
AppInstaller.uninstall(requireContext(), app.packageName)
}
}
} else {
if (PackageUtil.isArchived(requireContext(), app.packageName)) {
binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_unarchive)
} else {
if (app.isFree) {
binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install)
binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_install)
} else {
binding.layoutDetailsApp.btnPrimaryAction.text = app.price
}
}
binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener {
if (authProvider.isAnonymous && !app.isFree) {
......
......@@ -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>
......@@ -479,4 +480,7 @@
<!-- SpoofFragment -->
<string name="default_spoof">Default</string>
<string name="available_spoof">Available</string>
<!-- AppDetailsFragment -->
<string name="action_unarchive">Unarchive</string>
</resources>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment