mirror of
https://codeberg.org/mi6e4ka/openstore.git
synced 2026-07-09 15:59:50 +00:00
from #19, thx Dmitry Co-authored-by: oltnd <oltn@kkmbr.xyz> Reviewed-on: https://codeberg.org/mi6e4ka/openstore/pulls/20
This commit is contained in:
parent
c759c1fcf9
commit
ff4afa61ab
10 changed files with 351 additions and 55 deletions
|
|
@ -105,5 +105,6 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.lz4.java)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
}
|
||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
|
@ -19,3 +19,6 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# Keep xxhash classes used by LZ4 library
|
||||
-keep class net.jpountz.xxhash.** { *; }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
|
|
@ -15,17 +16,20 @@ import dev.mi6e4ka.openstore.R
|
|||
import dev.mi6e4ka.openstore.data.model.DownloadLinkResponseItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
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
|
||||
import okhttp3.Request
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import net.jpountz.xxhash.XXHashFactory
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
|
@ -50,79 +54,102 @@ class ApkInstaller(private val context: Context) {
|
|||
onSuccess: (List<File>) -> Unit,
|
||||
onError: (Throwable) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onInvalidHash: (List<File>, String?, String?) -> Unit = { _, _, _ -> }
|
||||
): () -> Unit {
|
||||
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) {
|
||||
return@async apkFile
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
val resultingFiles = coroutineScope {
|
||||
files.map { file ->
|
||||
async {
|
||||
val fileName = file.url.split("/").last()
|
||||
val apkFile = getApkFile(fileName)
|
||||
if (!downloadedFiles.contains(apkFile)) {
|
||||
downloadedFiles.add(apkFile)
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
if (apkFile.exists() && apkFile.length() == file.size) {
|
||||
if (verifyFileHash(apkFile, file.hash)) {
|
||||
return@async apkFile
|
||||
}
|
||||
apkFile.delete()
|
||||
}
|
||||
|
||||
apkFile
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
val resultingFiles = downloadJobs.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,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)
|
||||
}
|
||||
|
|
@ -148,6 +177,77 @@ class ApkInstaller(private val context: Context) {
|
|||
private val flags = PendingIntent.FLAG_MUTABLE
|
||||
private val intent = Intent(context, PackageInstallerStatusReceiver::class.java)
|
||||
|
||||
private fun verifyFileHash(file: File, expectedHash: String): Boolean {
|
||||
val normalizedHash = expectedHash.trim().ifEmpty { return true }
|
||||
val expectedBytes = decodeHashValue(normalizedHash) ?: return 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? {
|
||||
return try {
|
||||
if (hash.matches(Regex("^[0-9A-Fa-f]+$"))) {
|
||||
hash.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
} else {
|
||||
Base64.decode(hash, Base64.DEFAULT)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatHashForDisplay(hash: String): String {
|
||||
return if (hash.matches(Regex("^[0-9A-Fa-f]+$"))) {
|
||||
hash.lowercase()
|
||||
} else {
|
||||
hash
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeExistingHash(file: File): String {
|
||||
val actualBytes = digestFileXXH64(file) ?: return ""
|
||||
return actualBytes.toHexString()
|
||||
}
|
||||
|
||||
private fun digestFileXXH64(file: File): ByteArray? {
|
||||
return try {
|
||||
val factory = XXHashFactory.fastestInstance()
|
||||
val hash64 = factory.newStreamingHash64(0L)
|
||||
FileInputStream(file).use { fis ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
while (fis.read(buffer).also { bytesRead = it } != -1) {
|
||||
hash64.update(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
val hashValue = hash64.getValue()
|
||||
byteArrayOf(
|
||||
(hashValue shr 56).toByte(),
|
||||
(hashValue shr 48).toByte(),
|
||||
(hashValue shr 40).toByte(),
|
||||
(hashValue shr 32).toByte(),
|
||||
(hashValue shr 24).toByte(),
|
||||
(hashValue shr 16).toByte(),
|
||||
(hashValue shr 8).toByte(),
|
||||
hashValue.toByte()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (b in this) {
|
||||
sb.append(String.format("%02x", b))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun installApp(
|
||||
packageName: String,
|
||||
apkFiles: List<File>,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -37,9 +38,11 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -105,6 +108,7 @@ fun DetailsScreen(
|
|||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showAppDescriptionSheet by remember { mutableStateOf(false) }
|
||||
var showHashDetails by remember { mutableStateOf(false) }
|
||||
// val appDescriptionSheetState = rememberModalBottomSheetState(
|
||||
// skipPartiallyExpanded = true
|
||||
// )
|
||||
|
|
@ -203,6 +207,36 @@ fun DetailsScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (state.invalidDownloadedApkFiles != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.clearInvalidDownloadState() },
|
||||
title = { Text(stringResource(R.string.invalid_download_hash_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringResource(R.string.invalid_download_hash_message))
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
TextButton(onClick = { showHashDetails = !showHashDetails }) {
|
||||
Text(stringResource(R.string.show_hashes))
|
||||
}
|
||||
if (showHashDetails) {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.expected_hash, state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash, state.invalidActualHash ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.retryDownloadAfterInvalid(context) }) {
|
||||
Text(stringResource(R.string.download_again))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.installAsIsAfterInvalid(context) }) {
|
||||
Text(stringResource(R.string.install_as_is))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (state.isLoading) {
|
||||
LinearProgressIndicator(modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ data class DetailsState(
|
|||
val isInstalled: Boolean = false,
|
||||
val isNeedUpgrade: Boolean = false,
|
||||
val isSupported: Boolean = true,
|
||||
val invalidDownloadedApkFiles: List<File>? = null,
|
||||
val invalidDownloadMessage: String? = null,
|
||||
val invalidExpectedHash: String? = null,
|
||||
val invalidActualHash: String? = null
|
||||
)
|
||||
|
||||
sealed class InstallStatus {
|
||||
|
|
@ -135,6 +139,17 @@ class DetailsViewModel(private val appPlatform: String, private val apkInstaller
|
|||
},
|
||||
onCancel = {
|
||||
_state.update { it.copy(isDownloading = false) }
|
||||
},
|
||||
onInvalidHash = { apkFiles, expectedHash, actualHash ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
isDownloading = false,
|
||||
invalidDownloadedApkFiles = apkFiles,
|
||||
invalidDownloadMessage = "Файл скачан некорректно.",
|
||||
invalidExpectedHash = expectedHash,
|
||||
invalidActualHash = actualHash
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -181,6 +196,42 @@ class DetailsViewModel(private val appPlatform: String, private val apkInstaller
|
|||
)
|
||||
}
|
||||
|
||||
fun retryDownloadAfterInvalid(context: Context) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidDownloadMessage = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
downloadAndInstallApp(context)
|
||||
}
|
||||
|
||||
fun installAsIsAfterInvalid(context: Context) {
|
||||
val apkFiles = state.value.invalidDownloadedApkFiles ?: return
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidDownloadMessage = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
installApp(context, apkFiles)
|
||||
}
|
||||
|
||||
fun clearInvalidDownloadState() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidDownloadMessage = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackageInfo(context: Context, packageName: String): PackageInfo? {
|
||||
Log.d(null, "try to check package $packageName")
|
||||
return try {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
|
|
@ -33,12 +34,15 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -65,6 +69,38 @@ fun UpdatesScreen(navController: NavController) {
|
|||
factory = UpdatesViewModelFactory(apkInstaller)
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
var showHashDetails by remember { mutableStateOf(false) }
|
||||
|
||||
if (state.invalidDownloadedApkFiles != null && state.invalidDownloadedAppPackage != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.clearInvalidDownloadState() },
|
||||
title = { Text(stringResource(R.string.invalid_download_hash_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringResource(R.string.invalid_download_hash_message))
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
TextButton(onClick = { showHashDetails = !showHashDetails }) {
|
||||
Text(stringResource(R.string.show_hashes))
|
||||
}
|
||||
if (showHashDetails) {
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.expected_hash, state.invalidExpectedHash ?: ""))
|
||||
Text(stringResource(R.string.actual_hash, state.invalidActualHash ?: ""))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.retryDownloadAfterInvalid(context) }) {
|
||||
Text(stringResource(R.string.download_again))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.installAsIsAfterInvalid(context) }) {
|
||||
Text(stringResource(R.string.install_as_is))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.initPreferences(context)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ data class UpdatesState(
|
|||
val error: String? = null,
|
||||
val downloadingPackages: Set<String> = emptySet(),
|
||||
val downloadProgress: Map<String, Int> = emptyMap(),
|
||||
val invalidDownloadedApkFiles: List<File>? = null,
|
||||
val invalidDownloadedAppPackage: String? = null,
|
||||
val invalidExpectedHash: String? = null,
|
||||
val invalidActualHash: String? = null,
|
||||
val excludeGooglePlay: Boolean = true
|
||||
)
|
||||
|
||||
|
|
@ -183,6 +187,18 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() {
|
|||
downloadProgress = it.downloadProgress - app.packageName
|
||||
)
|
||||
}
|
||||
},
|
||||
onInvalidHash = { apkFiles, expectedHash, actualHash ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
downloadingPackages = it.downloadingPackages - app.packageName,
|
||||
downloadProgress = it.downloadProgress - app.packageName,
|
||||
invalidDownloadedApkFiles = apkFiles,
|
||||
invalidDownloadedAppPackage = app.packageName,
|
||||
invalidExpectedHash = expectedHash,
|
||||
invalidActualHash = actualHash
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -234,6 +250,45 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() {
|
|||
cancelDownloads.remove(packageName)
|
||||
}
|
||||
|
||||
fun retryDownloadAfterInvalid(context: Context) {
|
||||
val packageName = state.value.invalidDownloadedAppPackage ?: return
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedAppPackage = null,
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
val app = state.value.appsWithUpdates.find { it.packageName == packageName } ?: return
|
||||
downloadAndUpdateApp(context, app)
|
||||
}
|
||||
|
||||
fun installAsIsAfterInvalid(context: Context) {
|
||||
val packageName = state.value.invalidDownloadedAppPackage ?: return
|
||||
val apkFiles = state.value.invalidDownloadedApkFiles ?: return
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedAppPackage = null,
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
installApp(context, packageName, apkFiles)
|
||||
}
|
||||
|
||||
fun clearInvalidDownloadState() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
invalidDownloadedAppPackage = null,
|
||||
invalidDownloadedApkFiles = null,
|
||||
invalidExpectedHash = null,
|
||||
invalidActualHash = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInstalledApps(context: Context): List<AppUpdateRequestEntry> {
|
||||
val pm = context.packageManager
|
||||
val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">OpenStore</string>
|
||||
<string name="safety_card_text">About app safety</string>
|
||||
<string name="error_message">Error: %1$s</string>
|
||||
<string name="provided_by">Provided by %1$s</string>
|
||||
|
|
@ -32,6 +33,13 @@
|
|||
<string name="positive_first">Positive</string>
|
||||
<string name="negative_first">Negative</string>
|
||||
<string name="download_has_started">Download has started</string>
|
||||
<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: %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>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_installer_title">Installer</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@
|
|||
<string name="positive_first">Позитивные</string>
|
||||
<string name="negative_first">Негативные</string>
|
||||
<string name="download_has_started">Загрузка началась</string>
|
||||
<string name="invalid_download_hash_title">Файл скачан некорректно</string>
|
||||
<string name="invalid_download_hash_message">Контрольная сумма не совпадает. Повторите загрузку или установите файл как есть.</string>
|
||||
<string name="show_hashes">Показать хеши</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>
|
||||
<string name="settings_title">Настройки</string>
|
||||
<string name="settings_installer_title">Установщик</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