From ebf9798c1668fd5af12b851ec1792b386349fd0c Mon Sep 17 00:00:00 2001 From: Oqba Date: Mon, 21 Sep 2026 18:08:35 +0330 Subject: [PATCH] =?UTF-8?q?=D8=A7=D8=B5=D9=84=D8=A7=D8=AD=20=D8=B3=DB=8C?= =?UTF-8?q?=D8=B3=D8=AA=D9=85=20=D8=AF=D8=A7=D9=86=D9=84=D9=88=D8=AF=20?= =?UTF-8?q?=D8=A8=D9=87=E2=80=8C=D8=B1=D9=88=D8=B2=D8=B1=D8=B3=D8=A7=D9=86?= =?UTF-8?q?=DB=8C=20=D8=A8=D8=A7=20Ktor=20=D9=88=20=D8=AD=D9=84=20=D9=85?= =?UTF-8?q?=D8=B4=DA=A9=D9=84=20=DA=AF=DB=8C=D8=B1=20=DA=A9=D8=B1=D8=AF?= =?UTF-8?q?=D9=86=20=D8=AF=D8=B1=20=DB=B0=D9=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oqba26/monthlypaymentapp/MainActivity.kt | 69 ++++++--- .../monthlypaymentapp/utils/UpdateManager.kt | 146 ++++++++---------- 2 files changed, 111 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt index 2b2ab67..ca1f95d 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt @@ -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,23 +217,40 @@ fun MainAppHost(viewModel: PersonViewModel) { updateInfo = info, onDismiss = { updateInfo = null }, onConfirm = { - val id = updateManager.downloadAndInstall(info.url, "MonthlyPaymentApp_v${info.versionName}.apk") - if (id != -1L) { - isDownloading = true - scope.launch { - updateManager.getDownloadProgress(id).collect { progress -> - downloadProgress = progress - if (progress >= 1f) { + 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 { + 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) ) - LinearProgressIndicator( - progress = { downloadProgress }, - modifier = Modifier.fillMaxWidth().height(8.dp), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, - ) + 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 ) } diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/UpdateManager.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/UpdateManager.kt index 066d2e6..7b59d6e 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/UpdateManager.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/UpdateManager.kt @@ -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" @@ -60,10 +63,10 @@ class UpdateManager(private val context: Context) { try { val timestamp = System.currentTimeMillis() val urlWithParams = if (updateUrl.contains("?")) "$updateUrl&t=$timestamp" else "$updateUrl?t=$timestamp" - + val responseText: String = client.get(urlWithParams).bodyAsText() val updateInfo: UpdateInfo = json.decodeFromString(responseText) - + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { pInfo.longVersionCode.toInt() @@ -81,95 +84,58 @@ 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 + fun downloadApk(url: String, fileName: String): Flow = flow { + try { + val destinationFile = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + fileName + ) + if (destinationFile.exists()) { + destinationFile.delete() } - } - 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() + 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 + } - 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 channel: ByteReadChannel = response.bodyAsChannel() + val contentLength = response.contentLength() ?: -1L - val downloadId = downloadManager.enqueue(request) + 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 - 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) - try { - receiverContext.unregisterReceiver(this) - } catch (e: Exception) { - e.printStackTrace() + if (contentLength > 0) { + val progress = bytesCopied.toFloat() / contentLength.toFloat() + emit(DownloadState.Progress(progress.coerceIn(0f, 1f))) + } else { + emit(DownloadState.Progress(0f)) + } } } + emit(DownloadState.Progress(1f)) + emit(DownloadState.Success(destinationFile)) } - } - - val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - ContextCompat.registerReceiver(context, onComplete, filter, ContextCompat.RECEIVER_EXPORTED) - - return downloadId - } - - fun getDownloadProgress(downloadId: Long): Flow = 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) - - if (bytesDownloadedIndex != -1 && bytesTotalIndex != -1 && statusIndex != -1) { - val bytesDownloaded = cursor.getInt(bytesDownloadedIndex) - val bytesTotal = cursor.getInt(bytesTotalIndex) - val status = cursor.getInt(statusIndex) - - if (status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED) { - isDownloading = false - } - - if (bytesTotal > 0) { - emit(bytesDownloaded.toFloat() / bytesTotal.toFloat()) - } - } - cursor.close() - } else { - isDownloading = false - cursor?.close() - } - if (isDownloading) delay(500.milliseconds) + } 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 { val contentUri = FileProvider.getUriForFile( - context, + context, "${context.packageName}.fileprovider", apkFile ) @@ -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() }