اصلاح سیستم دانلود بهروزرسانی با Ktor و حل مشکل گیر کردن در ۰٪
This commit is contained in:
1 parent
ed1d3cecbb
commit
ebf9798c16
2 files changed
+111
-104
No files matched your search
@@ -1,10 +1,13 @@
|
|||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
package com.oqba26.monthlypaymentapp
|
package com.oqba26.monthlypaymentapp
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
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.PersonScreen
|
||||||
import com.oqba26.monthlypaymentapp.ui.screens.SettingsScreen
|
import com.oqba26.monthlypaymentapp.ui.screens.SettingsScreen
|
||||||
import com.oqba26.monthlypaymentapp.ui.theme.MonthlyPaymentManagement2Theme
|
import com.oqba26.monthlypaymentapp.ui.theme.MonthlyPaymentManagement2Theme
|
||||||
|
import com.oqba26.monthlypaymentapp.utils.DownloadState
|
||||||
import com.oqba26.monthlypaymentapp.utils.UpdateInfo
|
import com.oqba26.monthlypaymentapp.utils.UpdateInfo
|
||||||
import com.oqba26.monthlypaymentapp.utils.UpdateManager
|
import com.oqba26.monthlypaymentapp.utils.UpdateManager
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.ContactViewModel
|
import com.oqba26.monthlypaymentapp.viewmodel.ContactViewModel
|
||||||
@@ -213,23 +217,40 @@ fun MainAppHost(viewModel: PersonViewModel) {
|
|||||||
updateInfo = info,
|
updateInfo = info,
|
||||||
onDismiss = { updateInfo = null },
|
onDismiss = { updateInfo = null },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
val id = updateManager.downloadAndInstall(info.url, "MonthlyPaymentApp_v${info.versionName}.apk")
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.packageManager.canRequestPackageInstalls()) {
|
||||||
if (id != -1L) {
|
Toast.makeText(context, "لطفاً اجازه نصب برنامههای ناشناخته را بدهید", Toast.LENGTH_LONG).show()
|
||||||
isDownloading = true
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||||
scope.launch {
|
data = "package:${context.packageName}".toUri()
|
||||||
updateManager.getDownloadProgress(id).collect { progress ->
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
downloadProgress = progress
|
}
|
||||||
if (progress >= 1f) {
|
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
|
isDownloading = false
|
||||||
if (!info.isForceUpdate) {
|
updateManager.installApk(state.file)
|
||||||
updateInfo = null
|
}
|
||||||
}
|
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,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
LinearProgressIndicator(
|
if (downloadProgress > 0f) {
|
||||||
progress = { downloadProgress },
|
LinearProgressIndicator(
|
||||||
modifier = Modifier.fillMaxWidth().height(8.dp),
|
progress = { downloadProgress },
|
||||||
color = MaterialTheme.colorScheme.primary,
|
modifier = Modifier.fillMaxWidth().height(8.dp),
|
||||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${(downloadProgress * 100).toInt()}%",
|
text = if (downloadProgress > 0f) "${(downloadProgress * 100).toInt()}%" else "در حال اتصال...",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
|
@file:OptIn(InternalSerializationApi::class)
|
||||||
package com.oqba26.monthlypaymentapp.utils
|
package com.oqba26.monthlypaymentapp.utils
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.utils.io.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
@@ -39,6 +36,12 @@ data class UpdateInfo(
|
|||||||
val isForceUpdate: Boolean = false,
|
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) {
|
class UpdateManager(private val context: Context) {
|
||||||
|
|
||||||
private val updateUrl = "https://raw.githubusercontent.com/oqba26/MonthlyPaymentApp/main/update.json"
|
private val updateUrl = "https://raw.githubusercontent.com/oqba26/MonthlyPaymentApp/main/update.json"
|
||||||
@@ -60,10 +63,10 @@ class UpdateManager(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
val urlWithParams = if (updateUrl.contains("?")) "$updateUrl&t=$timestamp" else "$updateUrl?t=$timestamp"
|
val urlWithParams = if (updateUrl.contains("?")) "$updateUrl&t=$timestamp" else "$updateUrl?t=$timestamp"
|
||||||
|
|
||||||
val responseText: String = client.get(urlWithParams).bodyAsText()
|
val responseText: String = client.get(urlWithParams).bodyAsText()
|
||||||
val updateInfo: UpdateInfo = json.decodeFromString(responseText)
|
val updateInfo: UpdateInfo = json.decodeFromString(responseText)
|
||||||
|
|
||||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||||
val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
pInfo.longVersionCode.toInt()
|
pInfo.longVersionCode.toInt()
|
||||||
@@ -81,95 +84,58 @@ class UpdateManager(private val context: Context) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadAndInstall(url: String, fileName: String): Long {
|
fun downloadApk(url: String, fileName: String): Flow<DownloadState> = flow {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
try {
|
||||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
val destinationFile = File(
|
||||||
Toast.makeText(context, "لطفاً اجازه نصب برنامههای ناشناخته را بدهید", Toast.LENGTH_LONG).show()
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
fileName
|
||||||
data = "package:${context.packageName}".toUri()
|
)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
if (destinationFile.exists()) {
|
||||||
}
|
destinationFile.delete()
|
||||||
context.startActivity(intent)
|
|
||||||
return -1L
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
client.prepareGet(url) {
|
||||||
val uri = url.toUri()
|
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 ->
|
||||||
val oldFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
|
if (!response.status.isSuccess()) {
|
||||||
if (oldFile.exists()) oldFile.delete()
|
emit(DownloadState.Error("خطا در دانلود فایل (کد ${response.status.value})"))
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(uri)
|
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||||
.setTitle("دریافت بهروزرسانی")
|
val contentLength = response.contentLength() ?: -1L
|
||||||
.setDescription("نسخه جدید در حال دانلود است...")
|
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
||||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName)
|
|
||||||
.setAllowedOverMetered(true)
|
|
||||||
.setAllowedOverRoaming(true)
|
|
||||||
|
|
||||||
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() {
|
if (contentLength > 0) {
|
||||||
override fun onReceive(receiverContext: Context, intent: Intent) {
|
val progress = bytesCopied.toFloat() / contentLength.toFloat()
|
||||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
emit(DownloadState.Progress(progress.coerceIn(0f, 1f)))
|
||||||
if (id == downloadId) {
|
} else {
|
||||||
installApk(fileName)
|
emit(DownloadState.Progress(0f))
|
||||||
try {
|
}
|
||||||
receiverContext.unregisterReceiver(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
emit(DownloadState.Progress(1f))
|
||||||
|
emit(DownloadState.Success(destinationFile))
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
|
emit(DownloadState.Error(e.localizedMessage ?: "خطای ارتباط با سرور"))
|
||||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
||||||
ContextCompat.registerReceiver(context, onComplete, filter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
|
|
||||||
return downloadId
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
private fun installApk(fileName: String) {
|
fun installApk(apkFile: File) {
|
||||||
val apkFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
|
|
||||||
if (!apkFile.exists()) return
|
if (!apkFile.exists()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val contentUri = FileProvider.getUriForFile(
|
val contentUri = FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
"${context.packageName}.fileprovider",
|
"${context.packageName}.fileprovider",
|
||||||
apkFile
|
apkFile
|
||||||
)
|
)
|
||||||
@@ -179,6 +145,18 @@ class UpdateManager(private val context: Context) {
|
|||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user