اصلاح سیستم دانلود به‌روزرسانی با Ktor و حل مشکل گیر کردن در ۰٪

This commit is contained in:
Oqba committed 2026-09-21 18:08:35 +03:30
1 parent ed1d3cecbb
commit ebf9798c16
2 files changed
+99 -92

No files matched your search

@@ -1,10 +1,13 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package com.oqba26.monthlypaymentapp
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.core.net.toUri
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -96,6 +99,7 @@ import com.oqba26.monthlypaymentapp.ui.screens.PersonDetailScreen
import com.oqba26.monthlypaymentapp.ui.screens.PersonScreen
import com.oqba26.monthlypaymentapp.ui.screens.SettingsScreen
import com.oqba26.monthlypaymentapp.ui.theme.MonthlyPaymentManagement2Theme
import com.oqba26.monthlypaymentapp.utils.DownloadState
import com.oqba26.monthlypaymentapp.utils.UpdateInfo
import com.oqba26.monthlypaymentapp.utils.UpdateManager
import com.oqba26.monthlypaymentapp.viewmodel.ContactViewModel
@@ -213,24 +217,41 @@ fun MainAppHost(viewModel: PersonViewModel) {
updateInfo = info,
onDismiss = { updateInfo = null },
onConfirm = {
val id = updateManager.downloadAndInstall(info.url, "MonthlyPaymentApp_v${info.versionName}.apk")
if (id != -1L) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.packageManager.canRequestPackageInstalls()) {
Toast.makeText(context, "لطفاً اجازه نصب برنامه‌های ناشناخته را بدهید", Toast.LENGTH_LONG).show()
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = "package:${context.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
return@UpdateDialog
}
isDownloading = true
downloadProgress = 0f
if (!info.isForceUpdate) {
updateInfo = null
}
scope.launch {
updateManager.getDownloadProgress(id).collect { progress ->
downloadProgress = progress
if (progress >= 1f) {
val fileName = "MonthlyPaymentApp_v${info.versionName}.apk"
updateManager.downloadApk(info.url, fileName).collect { state ->
when (state) {
is DownloadState.Progress -> {
downloadProgress = state.progress
}
is DownloadState.Success -> {
isDownloading = false
if (!info.isForceUpdate) {
updateInfo = null
updateManager.installApk(state.file)
}
is DownloadState.Error -> {
isDownloading = false
Toast.makeText(context, "خطا در دانلود: ${state.message}", Toast.LENGTH_LONG).show()
updateManager.openInBrowser(info.url)
}
}
}
}
if (!info.isForceUpdate) {
updateInfo = null
}
}
}
)
}
@@ -252,15 +273,23 @@ fun MainAppHost(viewModel: PersonViewModel) {
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
if (downloadProgress > 0f) {
LinearProgressIndicator(
progress = { downloadProgress },
modifier = Modifier.fillMaxWidth().height(8.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
} else {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().height(8.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "${(downloadProgress * 100).toInt()}%",
text = if (downloadProgress > 0f) "${(downloadProgress * 100).toInt()}%" else "در حال اتصال...",
style = MaterialTheme.typography.bodyMedium
)
}
@@ -1,34 +1,31 @@
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
@file:OptIn(InternalSerializationApi::class)
package com.oqba26.monthlypaymentapp.utils
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Environment
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import io.ktor.client.*
import io.ktor.client.statement.bodyAsText
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.File
import kotlin.time.Duration.Companion.milliseconds
@Serializable
data class UpdateInfo(
@@ -39,6 +36,12 @@ data class UpdateInfo(
val isForceUpdate: Boolean = false,
)
sealed class DownloadState {
data class Progress(val progress: Float) : DownloadState()
data class Success(val file: File) : DownloadState()
data class Error(val message: String) : DownloadState()
}
class UpdateManager(private val context: Context) {
private val updateUrl = "https://raw.githubusercontent.com/oqba26/MonthlyPaymentApp/main/update.json"
@@ -81,90 +84,53 @@ class UpdateManager(private val context: Context) {
null
}
fun downloadAndInstall(url: String, fileName: String): Long {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!context.packageManager.canRequestPackageInstalls()) {
Toast.makeText(context, "لطفاً اجازه نصب برنامه‌های ناشناخته را بدهید", Toast.LENGTH_LONG).show()
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = "package:${context.packageName}".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
return -1L
}
}
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri = url.toUri()
val oldFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
if (oldFile.exists()) oldFile.delete()
val request = DownloadManager.Request(uri)
.setTitle("دریافت به‌روزرسانی")
.setDescription("نسخه جدید در حال دانلود است...")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
val downloadId = downloadManager.enqueue(request)
val onComplete = object : BroadcastReceiver() {
override fun onReceive(receiverContext: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
if (id == downloadId) {
installApk(fileName)
fun downloadApk(url: String, fileName: String): Flow<DownloadState> = flow {
try {
receiverContext.unregisterReceiver(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
val destinationFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
fileName
)
if (destinationFile.exists()) {
destinationFile.delete()
}
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
ContextCompat.registerReceiver(context, onComplete, filter, ContextCompat.RECEIVER_EXPORTED)
return downloadId
client.prepareGet(url) {
header(HttpHeaders.UserAgent, "Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36")
}.execute { response ->
if (!response.status.isSuccess()) {
emit(DownloadState.Error("خطا در دانلود فایل (کد ${response.status.value})"))
return@execute
}
fun getDownloadProgress(downloadId: Long): Flow<Float> = flow {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
var isDownloading = true
while (isDownloading) {
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor = downloadManager.query(query)
if (cursor != null && cursor.moveToFirst()) {
val bytesDownloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesTotalIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val channel: ByteReadChannel = response.bodyAsChannel()
val contentLength = response.contentLength() ?: -1L
if (bytesDownloadedIndex != -1 && bytesTotalIndex != -1 && statusIndex != -1) {
val bytesDownloaded = cursor.getInt(bytesDownloadedIndex)
val bytesTotal = cursor.getInt(bytesTotalIndex)
val status = cursor.getInt(statusIndex)
destinationFile.outputStream().use { output ->
val buffer = ByteArray(16384)
var bytesCopied = 0L
while (!channel.isClosedForRead) {
val read = channel.readAvailable(buffer, 0, buffer.size)
if (read <= 0) break
output.write(buffer, 0, read)
bytesCopied += read
if (status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED) {
isDownloading = false
}
if (bytesTotal > 0) {
emit(bytesDownloaded.toFloat() / bytesTotal.toFloat())
}
}
cursor.close()
if (contentLength > 0) {
val progress = bytesCopied.toFloat() / contentLength.toFloat()
emit(DownloadState.Progress(progress.coerceIn(0f, 1f)))
} else {
isDownloading = false
cursor?.close()
emit(DownloadState.Progress(0f))
}
if (isDownloading) delay(500.milliseconds)
}
}
emit(DownloadState.Progress(1f))
emit(DownloadState.Success(destinationFile))
}
} catch (e: Exception) {
emit(DownloadState.Error(e.localizedMessage ?: "خطای ارتباط با سرور"))
}
}.flowOn(Dispatchers.IO)
private fun installApk(fileName: String) {
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
fun installApk(apkFile: File) {
if (!apkFile.exists()) return
try {
@@ -179,6 +145,18 @@ class UpdateManager(private val context: Context) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, "خطا در اجرای فایل نصب: ${e.message}", Toast.LENGTH_LONG).show()
}
}
fun openInBrowser(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}