Merge branch 'dev'

This commit is contained in:
Aayush Gupta
2024-12-21 18:01:46 +07:00
9 changed files with 104 additions and 92 deletions

View File

@@ -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)

View File

@@ -1,11 +0,0 @@
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
)

View File

@@ -27,7 +27,6 @@ import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.Algorithm import com.aurora.store.data.model.Algorithm
import com.aurora.store.data.model.DownloadInfo import com.aurora.store.data.model.DownloadInfo
import com.aurora.store.data.model.DownloadStatus 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.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.download.Download 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) // Purchase the app (free apps needs to be purchased too)
val requestList = mutableListOf<Request>() val requestList = mutableListOf<GPlayFile>()
if (download.sharedLibs.isNotEmpty()) { if (download.sharedLibs.isNotEmpty()) {
download.sharedLibs.forEach { download.sharedLibs.forEach {
PathUtil.getLibDownloadDir( PathUtil.getLibDownloadDir(
@@ -135,10 +134,10 @@ class DownloadWorker @AssistedInject constructor(
it.packageName it.packageName
).mkdirs() ).mkdirs()
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) } 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 // Update data for notification
download.totalFiles = requestList.size download.totalFiles = requestList.size
@@ -169,8 +168,13 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
if (!requestList.all { it.file.exists() }) { try {
Log.e(TAG, "Downloaded files are missing") // 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() onFailure()
return Result.failure() 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. * Downloads the file from the given request.
* Failed downloads aren't removed and persists as long as [CacheWorker] doesn't cleans them. * 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. * @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) { return withContext(Dispatchers.IO) {
val algorithm = if (request.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256 val file = PathUtil.getLocalFile(appContext, gFile, download)
val expectedSha = if (algorithm == Algorithm.SHA1) request.sha1 else request.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 (request.file.exists() && validSha(request.file, expectedSha, algorithm)) { if (file.exists() && verifyFile(gFile)) {
Log.i(TAG, "${request.file} is already downloaded!") Log.i(TAG, "$file is already downloaded!")
downloadedBytes += request.file.length() downloadedBytes += file.length()
return@withContext Result.success() return@withContext Result.success()
} }
try { try {
// Download as a temporary file to avoid installing corrupted files // Download as a temporary file to avoid installing corrupted files
val tmpFileSuffix = ".tmp" val tmpFileSuffix = ".tmp"
val tmpFile = File(request.file.absolutePath + tmpFileSuffix) val tmpFile = File(file.absolutePath + tmpFileSuffix)
val isNewFile = tmpFile.createNewFile() val isNewFile = tmpFile.createNewFile()
val okHttpClient = httpClient as HttpClient val okHttpClient = httpClient as HttpClient
@@ -289,24 +273,19 @@ class DownloadWorker @AssistedInject constructor(
headers["Range"] = "bytes=${tmpFile.length()}-" 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 { 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 (!tmpFile.renameTo(file)) {
if (!validSha(tmpFile, expectedSha, algorithm)) {
throw Exception("Incorrect hash for $tmpFile")
}
if (!tmpFile.renameTo(request.file)) {
throw Exception("Failed to remove .tmp extension from $tmpFile") throw Exception("Failed to remove .tmp extension from $tmpFile")
} }
return@withContext Result.success() return@withContext Result.success()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to download ${request.file}!", exception) Log.e(TAG, "Failed to download $file!", exception)
notifyStatus(DownloadStatus.FAILED) notifyStatus(DownloadStatus.FAILED)
return@withContext Result.failure() return@withContext Result.failure()
} }
@@ -371,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
@@ -387,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 ->

View File

@@ -40,6 +40,7 @@ import com.aurora.extensions.isHuawei
import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isTAndAbove import com.aurora.extensions.isTAndAbove
import com.aurora.extensions.isVAndAbove
import com.aurora.extensions.isValidApp import com.aurora.extensions.isValidApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R 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 { fun isSharedLibrary(context: Context, packageName: String): Boolean {
return if (isOAndAbove) { return if (isOAndAbove) {
getAllSharedLibraries(context).any { it.name == packageName } getAllSharedLibraries(context).any { it.name == packageName }

View File

@@ -21,6 +21,7 @@ package com.aurora.store.util
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import com.aurora.store.data.room.download.Download
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import com.aurora.gplayapi.data.models.File as GPlayFile import com.aurora.gplayapi.data.models.File as GPlayFile
@@ -62,19 +63,33 @@ object PathUtil {
) )
} }
fun getApkDownloadFile( /**
context: Context, * Returns an instance of java's [File] class for the given [GPlayFile]
packageName: String, * @param context [Context]
versionCode: Int, * @param gFile [GPlayFile] to download
file: GPlayFile, * @param download An instance of [Download]
sharedLibPackageName: String? = null */
): File { fun getLocalFile(context: Context, gFile: GPlayFile, download: Download): File {
val downloadDir = if (!sharedLibPackageName.isNullOrBlank()) { val isSharedLib = download.sharedLibs.any { it.fileList.contains(gFile) }
getLibDownloadDir(context, packageName, versionCode, sharedLibPackageName) return when (gFile.type) {
} else { GPlayFile.FileType.BASE, GPlayFile.FileType.SPLIT -> {
getAppDownloadDir(context, packageName, versionCode) val downloadDir = if (isSharedLib) {
getLibDownloadDir(
context,
download.packageName,
download.versionCode,
download.packageName
)
} else {
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 { fun getZipFile(context: Context, packageName: String, versionCode: Int): File {
@@ -91,10 +106,6 @@ object PathUtil {
return File(Environment.getExternalStorageDirectory(), "/Android/obb/$packageName") 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 { fun getSpoofDirectory(context: Context): File {
return File(context.filesDir, SPOOF) return File(context.filesDir, SPOOF)
} }
@@ -105,15 +116,5 @@ object PathUtil {
file.createNewFile() file.createNewFile()
return file return file
} }
fun canReadWriteOBB(context: Context): Boolean {
val obbDir = context.obbDir.parentFile
if (obbDir != null) {
return obbDir.exists() && obbDir.canRead() && obbDir.canWrite()
}
return false
}
} }

View File

@@ -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?) {

View File

@@ -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)

View File

@@ -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())
@@ -609,10 +624,16 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
} }
} }
} else { } else {
if (app.isFree) { if (PackageUtil.isArchived(requireContext(), app.packageName)) {
binding.layoutDetailsApp.btnPrimaryAction.setText(R.string.action_install) binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_unarchive)
} else { } else {
binding.layoutDetailsApp.btnPrimaryAction.text = app.price if (app.isFree) {
binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_install)
} else {
binding.layoutDetailsApp.btnPrimaryAction.text = app.price
}
} }
binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener { binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener {

View File

@@ -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>
@@ -479,4 +480,7 @@
<!-- SpoofFragment --> <!-- SpoofFragment -->
<string name="default_spoof">Default</string> <string name="default_spoof">Default</string>
<string name="available_spoof">Available</string> <string name="available_spoof">Available</string>
<!-- AppDetailsFragment -->
<string name="action_unarchive">Unarchive</string>
</resources> </resources>