mirror of
https://codeberg.org/mi6e4ka/openstore.git
synced 2026-08-20 06:24:32 +00:00
fix: some fixes from gemini
This commit is contained in:
parent
846677c37a
commit
fc91100a99
6 changed files with 105 additions and 151 deletions
|
|
@ -21,6 +21,7 @@ import java.io.FileOutputStream
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -28,7 +29,7 @@ import okhttp3.Request
|
|||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
import net.jpountz.xxhash.XXHashFactory
|
||||
import java.security.MessageDigest
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
|
@ -58,94 +59,97 @@ class ApkInstaller(private val context: Context) {
|
|||
val downloadRunner = CoroutineScope(Dispatchers.IO).launch {
|
||||
val totalBytesToDownload = files.sumOf { it.size }
|
||||
val totalBytesDownloaded = AtomicLong(0)
|
||||
val downloadedFiles = mutableListOf<File>()
|
||||
val downloadedFiles = Collections.synchronizedList(mutableListOf<File>())
|
||||
try {
|
||||
val downloadJobs = files.map { file ->
|
||||
async {
|
||||
val fileName = file.url.split("/").last()
|
||||
val apkFile = getApkFile(fileName)
|
||||
downloadedFiles.add(apkFile)
|
||||
|
||||
if (apkFile.exists() && apkFile.length() == file.size) {
|
||||
if (verifyFileHash(apkFile, file.hash)) {
|
||||
return@async apkFile
|
||||
val resultingFiles = coroutineScope {
|
||||
files.map { file ->
|
||||
async {
|
||||
val fileName = file.url.split("/").last()
|
||||
val apkFile = getApkFile(fileName)
|
||||
if (!downloadedFiles.contains(apkFile)) {
|
||||
downloadedFiles.add(apkFile)
|
||||
}
|
||||
|
||||
if (apkFile.exists() && apkFile.length() == file.size) {
|
||||
if (verifyFileHash(apkFile, file.hash)) {
|
||||
return@async apkFile
|
||||
}
|
||||
apkFile.delete()
|
||||
}
|
||||
|
||||
val request = Request.Builder().url(file.url).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) throw IOException("Ошибка загрузки: ${response.code}")
|
||||
|
||||
val responseBody =
|
||||
response.body ?: throw IOException("Пустой ответ от сервера")
|
||||
|
||||
responseBody.byteStream().use { inputStream ->
|
||||
FileOutputStream(apkFile).use { outputStream ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
if (call.isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
|
||||
val newTotalDownloaded =
|
||||
totalBytesDownloaded.addAndFetch(bytesRead.toLong())
|
||||
val percent = if (totalBytesToDownload > 0) {
|
||||
(newTotalDownloaded * 100 / totalBytesToDownload).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(percent, newTotalDownloaded, totalBytesToDownload)
|
||||
}
|
||||
}
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
if (!verifyFileHash(apkFile, file.hash)) {
|
||||
throw HashMismatchException(apkFile, file.hash)
|
||||
}
|
||||
|
||||
apkFile
|
||||
}
|
||||
apkFile.delete()
|
||||
}
|
||||
|
||||
val request = Request.Builder().url(file.url).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) throw IOException("Ошибка загрузки: ${response.code}")
|
||||
|
||||
val responseBody =
|
||||
response.body ?: throw IOException("Пустой ответ от сервера")
|
||||
val inputStream = responseBody.byteStream()
|
||||
val outputStream = FileOutputStream(apkFile)
|
||||
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
if (call.isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
|
||||
val newTotalDownloaded =
|
||||
totalBytesDownloaded.addAndFetch(bytesRead.toLong())
|
||||
val percent = if (totalBytesToDownload > 0) {
|
||||
(newTotalDownloaded * 100 / totalBytesToDownload).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(percent, newTotalDownloaded, totalBytesToDownload)
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
|
||||
if (!verifyFileHash(apkFile, file.hash)) {
|
||||
return@async apkFile
|
||||
}
|
||||
|
||||
apkFile
|
||||
}
|
||||
}
|
||||
}
|
||||
val resultingFiles = downloadJobs.awaitAll()
|
||||
val invalidPair = files.zip(resultingFiles)
|
||||
.firstOrNull { (fileSpec, downloadedFile) -> !verifyFileHash(downloadedFile, fileSpec.hash) }
|
||||
|
||||
if (invalidPair != null) {
|
||||
val (fileSpec, downloadedFile) = invalidPair
|
||||
val expectedHash = formatHashForDisplay(fileSpec.hash)
|
||||
val actualHash = computeExistingHash(downloadedFile, fileSpec.hash)
|
||||
withContext(Dispatchers.Main) {
|
||||
onInvalidHash(resultingFiles, expectedHash, actualHash)
|
||||
}
|
||||
return@launch
|
||||
}.awaitAll()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess(resultingFiles)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is HashMismatchException) {
|
||||
val expectedHash = formatHashForDisplay(e.expectedHash)
|
||||
val actualHash = computeExistingHash(e.file)
|
||||
val filesCopy = downloadedFiles.toList()
|
||||
withContext(Dispatchers.Main) {
|
||||
onInvalidHash(filesCopy, expectedHash, actualHash)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
downloadedFiles.forEach { file ->
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
if (e is CancellationException) {
|
||||
onCancel()
|
||||
} else {
|
||||
onError(e)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (e is CancellationException) {
|
||||
onCancel()
|
||||
} else {
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +159,8 @@ class ApkInstaller(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private class HashMismatchException(val file: File, val expectedHash: String) : IOException("Hash mismatch")
|
||||
|
||||
fun getApkFile(fileName: String): File {
|
||||
return File(context.externalCacheDir ?: context.cacheDir, fileName)
|
||||
}
|
||||
|
|
@ -174,38 +180,12 @@ class ApkInstaller(private val context: Context) {
|
|||
private fun verifyFileHash(file: File, expectedHash: String): Boolean {
|
||||
val normalizedHash = expectedHash.trim().ifEmpty { return true }
|
||||
val expectedBytes = decodeHashValue(normalizedHash) ?: return true
|
||||
return when (expectedBytes.size) {
|
||||
8 -> {
|
||||
val actualBytes = digestFileXXH64(file) ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
16 -> {
|
||||
val actualBytes = digestFile(file, "MD5") ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
20 -> {
|
||||
val actualBytes = digestFile(file, "SHA-1") ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
32 -> {
|
||||
val actualBytes = digestFile(file, "SHA-256") ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
48 -> {
|
||||
val actualBytes = digestFile(file, "SHA-384") ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
64 -> {
|
||||
val actualBytes = digestFile(file, "SHA-512") ?: return true
|
||||
actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
else -> {
|
||||
// Unknown digest length, compare by hex string if possible.
|
||||
digestFile(file, "SHA-256")?.let { bytes ->
|
||||
bytes.toHexString().equals(normalizedHash, ignoreCase = true)
|
||||
} ?: true
|
||||
}
|
||||
}
|
||||
|
||||
// Only XXH64 is supported now (8 bytes)
|
||||
if (expectedBytes.size != 8) return true
|
||||
|
||||
val actualBytes = digestFileXXH64(file) ?: return false
|
||||
return actualBytes.contentEquals(expectedBytes)
|
||||
}
|
||||
|
||||
private fun decodeHashValue(hash: String): ByteArray? {
|
||||
|
|
@ -228,40 +208,9 @@ class ApkInstaller(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun computeExistingHash(file: File, expectedHash: String): String {
|
||||
val normalizedHash = expectedHash.trim().ifEmpty { return "" }
|
||||
val expectedBytes = decodeHashValue(normalizedHash) ?: return ""
|
||||
val actualBytes = when (expectedBytes.size) {
|
||||
8 -> digestFileXXH64(file)
|
||||
16 -> digestFile(file, "MD5")
|
||||
20 -> digestFile(file, "SHA-1")
|
||||
32 -> digestFile(file, "SHA-256")
|
||||
48 -> digestFile(file, "SHA-384")
|
||||
64 -> digestFile(file, "SHA-512")
|
||||
else -> digestFile(file, "SHA-256")
|
||||
} ?: return ""
|
||||
|
||||
return if (normalizedHash.matches(Regex("^[0-9A-Fa-f]+$"))) {
|
||||
actualBytes.toHexString()
|
||||
} else {
|
||||
Base64.encodeToString(actualBytes, Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
private fun digestFile(file: File, algorithm: String): ByteArray? {
|
||||
return try {
|
||||
val digest = MessageDigest.getInstance(algorithm)
|
||||
FileInputStream(file).use { fis ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
while (fis.read(buffer).also { bytesRead = it } != -1) {
|
||||
digest.update(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
digest.digest()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
private fun computeExistingHash(file: File): String {
|
||||
val actualBytes = digestFileXXH64(file) ?: return ""
|
||||
return actualBytes.toHexString()
|
||||
}
|
||||
|
||||
private fun digestFileXXH64(file: File): ByteArray? {
|
||||
|
|
@ -292,7 +241,11 @@ class ApkInstaller(private val context: Context) {
|
|||
}
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
return joinToString(separator = "") { "%02x".format(it) }
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (b in this) {
|
||||
sb.append(String.format("%02x", b))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun installApp(
|
||||
|
|
|
|||
|
|
@ -220,8 +220,8 @@ fun DetailsScreen(
|
|||
}
|
||||
if (showHashDetails) {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.expected_hash) + " " + (state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash) + " " + (state.invalidActualHash ?: ""))
|
||||
Text(stringResource(R.string.expected_hash, state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash, state.invalidActualHash ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ fun UpdatesScreen(navController: NavController) {
|
|||
}
|
||||
if (showHashDetails) {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.expected_hash) + " " + (state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash) + " " + (state.invalidActualHash ?: ""))
|
||||
Text(stringResource(R.string.expected_hash, state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash, state.invalidActualHash ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@
|
|||
<string name="invalid_download_hash_title">File downloaded incorrectly</string>
|
||||
<string name="invalid_download_hash_message">Checksum does not match. Retry download or install file as is.</string>
|
||||
<string name="show_hashes">Show hashes</string>
|
||||
<string name="expected_hash">Expected hash:</string>
|
||||
<string name="actual_hash">Actual hash:</string>
|
||||
<string name="expected_hash">Expected hash: %1$s</string>
|
||||
<string name="actual_hash">Actual hash: %1$s</string>
|
||||
<string name="download_again">Download again</string>
|
||||
<string name="install_as_is">Install as is</string>
|
||||
<string name="not_found_error">Nothing found</string>
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@
|
|||
<string name="invalid_download_hash_title">Файл скачан некорректно</string>
|
||||
<string name="invalid_download_hash_message">Контрольная сумма не совпадает. Повторите загрузку или установите файл как есть.</string>
|
||||
<string name="show_hashes">Показать хеши</string>
|
||||
<string name="expected_hash">Ожидаемый хеш:</string>
|
||||
<string name="actual_hash">Фактический хеш:</string>
|
||||
<string name="expected_hash">Ожидаемый хеш: %1$s</string>
|
||||
<string name="actual_hash">Фактический хеш: %1$s</string>
|
||||
<string name="download_again">Скачать по новой</string>
|
||||
<string name="install_as_is">Установить как есть</string>
|
||||
<string name="not_found_error">Ничего не найдено</string>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ animation = "1.9.0"
|
|||
material = "1.14.0-alpha04"
|
||||
kotlinxDatetime = "0.7.0"
|
||||
appcompat = "1.7.1"
|
||||
lz4Java = "1.8.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
|
@ -56,9 +57,9 @@ androidx-paging-compose = { group = "androidx.paging", name = "paging-compose",
|
|||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
lz4-java = { group = "org.lz4", name = "lz4-java", version.ref = "lz4Java" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue