Merge branch 'dev'
All checks were successful
Android Kernel Build / build (push) Successful in 16h26m43s
All checks were successful
Android Kernel Build / build (push) Successful in 16h26m43s
This commit is contained in:
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.1.8"
|
val rosettaVersionName = "1.1.9"
|
||||||
val rosettaVersionCode = 20 // Increment on each release
|
val rosettaVersionCode = 21 // Increment on each release
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.rosetta.messenger"
|
namespace = "com.rosetta.messenger"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class RosettaApplication : Application() {
|
|||||||
TransportManager.init(this)
|
TransportManager.init(this)
|
||||||
|
|
||||||
// Инициализируем менеджер обновлений (SDU)
|
// Инициализируем менеджер обновлений (SDU)
|
||||||
UpdateManager.init()
|
UpdateManager.init(this)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,22 +17,23 @@ object ReleaseNotes {
|
|||||||
val RELEASE_NOTICE = """
|
val RELEASE_NOTICE = """
|
||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Полноэкранное фото из медиапикера
|
Интерфейс
|
||||||
- Переработан fullscreen-оверлей: фото открывается поверх чата и перекрывает интерфейс
|
- Исправлен цвет галочки верификации в сайдбаре в зависимости от темы
|
||||||
- Добавлены свайпы влево/вправо для перехода по фото внутри выбранной галереи
|
- Исправлен цвет галочки верификации в профилях и попапах в светлой теме
|
||||||
- Добавлено закрытие свайпом вверх/вниз с плавной анимацией
|
- Исправлен черный gesture navigation bar при fullscreen фото
|
||||||
- Убраны рывки, мигание и лишнее уменьшение фото при перелистывании
|
|
||||||
|
|
||||||
Редактирование и отправка
|
Фото и редактор
|
||||||
- Инструменты редактирования фото перенесены в полноэкранный оверлей медиапикера
|
- При рисовании на фото теперь скрываются инпут и лишние оверлеи
|
||||||
- Улучшена пересылка фото через optimistic UI: сообщение отображается сразу
|
- Синхронизировано поведение системных баров в полноэкранном фото-режиме
|
||||||
- Исправлена множественная пересылка сообщений, включая сценарий после смены forwarding options
|
|
||||||
- Исправлено копирование пересланных сообщений: теперь корректно копируется текст forward/reply
|
|
||||||
|
|
||||||
Группы
|
Сообщения
|
||||||
- В списках участников групп отображается только статус online/offline
|
- Логика read-индикаторов внутри диалога приведена к логике чат-листа
|
||||||
- На экране создания группы у текущего пользователя статус отображается как online
|
- Убраны неверные переходы статусов сообщений при read/delivered событиях
|
||||||
- Поиск участников по username сохранен
|
|
||||||
|
Обновления приложения
|
||||||
|
- Загрузка апдейта переведена на системный DownloadManager
|
||||||
|
- Скачивание обновления продолжается после сворачивания/выхода из приложения
|
||||||
|
- Прогресс и состояние установки апдейта восстанавливаются после повторного запуска
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.rosetta.messenger.update
|
package com.rosetta.messenger.update
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.rosetta.messenger.BuildConfig
|
import com.rosetta.messenger.BuildConfig
|
||||||
@@ -16,9 +18,9 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.io.File
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +35,14 @@ import java.util.concurrent.TimeUnit
|
|||||||
*/
|
*/
|
||||||
object UpdateManager {
|
object UpdateManager {
|
||||||
private const val TAG = "UpdateManager"
|
private const val TAG = "UpdateManager"
|
||||||
|
private const val PREFS_NAME = "rosetta_update_state"
|
||||||
|
private const val KEY_DOWNLOAD_ID = "download_id"
|
||||||
|
private const val KEY_APK_PATH = "apk_path"
|
||||||
|
private const val KEY_APK_VERSION = "apk_version"
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
@Volatile
|
||||||
|
private var appContext: Context? = null
|
||||||
|
|
||||||
// ═══ Debug log buffer ═══
|
// ═══ Debug log buffer ═══
|
||||||
private val _debugLogs = MutableStateFlow<List<String>>(emptyList())
|
private val _debugLogs = MutableStateFlow<List<String>>(emptyList())
|
||||||
@@ -66,7 +74,13 @@ object UpdateManager {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var latestUpdateInfo: UpdateInfo? = null
|
private var latestUpdateInfo: UpdateInfo? = null
|
||||||
|
|
||||||
private var downloadJob: Job? = null
|
private var downloadMonitorJob: Job? = null
|
||||||
|
@Volatile
|
||||||
|
private var activeDownloadId: Long? = null
|
||||||
|
@Volatile
|
||||||
|
private var activeApkPath: String? = null
|
||||||
|
@Volatile
|
||||||
|
private var activeApkVersion: String? = null
|
||||||
|
|
||||||
private val httpClient = OkHttpClient.Builder()
|
private val httpClient = OkHttpClient.Builder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
@@ -74,10 +88,13 @@ object UpdateManager {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инициализация: регистрируем обработчик пакета 0x0A и запрашиваем SDU сервер
|
* Инициализация: регистрируем обработчик пакета 0x0A и восстанавливаем состояние скачивания.
|
||||||
*/
|
*/
|
||||||
fun init() {
|
fun init(context: Context? = null) {
|
||||||
|
context?.applicationContext?.let { appContext = it }
|
||||||
sduLog("init() called, appVersion=$appVersion")
|
sduLog("init() called, appVersion=$appVersion")
|
||||||
|
appContext?.let { restorePersistedState(it) }
|
||||||
|
|
||||||
sduLog("Registering waitPacket(0x0A) listener...")
|
sduLog("Registering waitPacket(0x0A) listener...")
|
||||||
ProtocolManager.waitPacket(0x0A) { packet ->
|
ProtocolManager.waitPacket(0x0A) { packet ->
|
||||||
sduLog("Received packet 0x0A, type=${packet::class.simpleName}")
|
sduLog("Received packet 0x0A, type=${packet::class.simpleName}")
|
||||||
@@ -125,6 +142,20 @@ object UpdateManager {
|
|||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeDownloadId != null) {
|
||||||
|
sduLog("checkForUpdates skipped: download is already in progress")
|
||||||
|
appContext?.let { startDownloadMonitor(it) }
|
||||||
|
return@withContext latestUpdateInfo
|
||||||
|
}
|
||||||
|
val readyPath = activeApkPath
|
||||||
|
if (_updateState.value is UpdateState.ReadyToInstall &&
|
||||||
|
!readyPath.isNullOrBlank() &&
|
||||||
|
File(readyPath).exists()
|
||||||
|
) {
|
||||||
|
sduLog("checkForUpdates skipped: downloaded APK is ready to install")
|
||||||
|
return@withContext latestUpdateInfo
|
||||||
|
}
|
||||||
|
|
||||||
_updateState.value = UpdateState.Checking
|
_updateState.value = UpdateState.Checking
|
||||||
sduLog("State -> Checking")
|
sduLog("State -> Checking")
|
||||||
|
|
||||||
@@ -175,73 +206,78 @@ object UpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачать и установить обновление
|
* Скачать обновление или открыть установщик, если APK уже скачан.
|
||||||
|
*
|
||||||
|
* Используем системный DownloadManager, чтобы загрузка продолжалась,
|
||||||
|
* даже если пользователь свернул или закрыл приложение.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(context: Context) {
|
fun downloadAndInstall(context: Context) {
|
||||||
|
val appCtx = context.applicationContext
|
||||||
|
appContext = appCtx
|
||||||
|
restorePersistedState(appCtx)
|
||||||
|
|
||||||
|
val currentState = _updateState.value
|
||||||
|
val readyPath = activeApkPath
|
||||||
|
if (currentState is UpdateState.ReadyToInstall && !readyPath.isNullOrBlank()) {
|
||||||
|
val readyFile = File(readyPath)
|
||||||
|
if (readyFile.exists()) {
|
||||||
|
installApk(appCtx, readyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeApkPath = null
|
||||||
|
activeApkVersion = null
|
||||||
|
persistState(appCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeDownloadId != null) {
|
||||||
|
startDownloadMonitor(appCtx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val info = latestUpdateInfo ?: return
|
val info = latestUpdateInfo ?: return
|
||||||
val sdu = sduServerUrl ?: return
|
val sdu = sduServerUrl ?: return
|
||||||
val url = info.servicePackUrl ?: return
|
val url = info.servicePackUrl ?: return
|
||||||
|
val fullUrl = if (url.startsWith("http")) url else "$sdu$url"
|
||||||
|
val apkFile = buildApkFile(appCtx, info.version)
|
||||||
|
if (apkFile.exists()) {
|
||||||
|
apkFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
downloadJob?.cancel()
|
try {
|
||||||
downloadJob = scope.launch {
|
val request =
|
||||||
_updateState.value = UpdateState.Downloading(0)
|
DownloadManager.Request(Uri.parse(fullUrl))
|
||||||
_downloadProgress.value = 0
|
.setTitle("Rosetta update ${info.version}")
|
||||||
|
.setDescription("Downloading update")
|
||||||
try {
|
.setMimeType("application/vnd.android.package-archive")
|
||||||
val fullUrl = if (url.startsWith("http")) url else "$sdu$url"
|
.setAllowedOverMetered(true)
|
||||||
Log.i(TAG, "Downloading update from: $fullUrl")
|
.setAllowedOverRoaming(true)
|
||||||
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||||
val request = Request.Builder().url(fullUrl).get().build()
|
if (appCtx.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) != null) {
|
||||||
val response = httpClient.newCall(request).execute()
|
request.setDestinationInExternalFilesDir(
|
||||||
|
appCtx,
|
||||||
if (!response.isSuccessful) {
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
_updateState.value = UpdateState.Error("Download failed: ${response.code}")
|
apkFile.name
|
||||||
return@launch
|
)
|
||||||
}
|
} else {
|
||||||
|
request.setDestinationUri(Uri.fromFile(apkFile))
|
||||||
val responseBody = response.body ?: run {
|
|
||||||
_updateState.value = UpdateState.Error("Empty download response")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalBytes = responseBody.contentLength()
|
|
||||||
val apkFile = File(context.cacheDir, "Rosetta-${info.version}.apk")
|
|
||||||
|
|
||||||
responseBody.byteStream().use { input ->
|
|
||||||
apkFile.outputStream().use { output ->
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
var bytesRead: Long = 0
|
|
||||||
var read: Int
|
|
||||||
|
|
||||||
while (input.read(buffer).also { read = it } != -1) {
|
|
||||||
if (!isActive) {
|
|
||||||
apkFile.delete()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
bytesRead += read
|
|
||||||
if (totalBytes > 0) {
|
|
||||||
val progress = ((bytesRead * 100) / totalBytes).toInt()
|
|
||||||
_downloadProgress.value = progress
|
|
||||||
_updateState.value = UpdateState.Downloading(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Download complete: ${apkFile.absolutePath}")
|
|
||||||
_updateState.value = UpdateState.ReadyToInstall(apkFile.absolutePath)
|
|
||||||
|
|
||||||
// Запускаем установку APK
|
|
||||||
installApk(context, apkFile)
|
|
||||||
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
Log.i(TAG, "Download cancelled")
|
|
||||||
_updateState.value = UpdateState.UpdateAvailable(info.version)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Download failed", e)
|
|
||||||
_updateState.value = UpdateState.Error("Download failed: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val downloadManager =
|
||||||
|
appCtx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
val downloadId = downloadManager.enqueue(request)
|
||||||
|
activeDownloadId = downloadId
|
||||||
|
activeApkPath = apkFile.absolutePath
|
||||||
|
activeApkVersion = info.version
|
||||||
|
persistState(appCtx)
|
||||||
|
|
||||||
|
_downloadProgress.value = 0
|
||||||
|
_updateState.value = UpdateState.Downloading(0)
|
||||||
|
sduLog("DownloadManager enqueue id=$downloadId, file=${apkFile.absolutePath}")
|
||||||
|
startDownloadMonitor(appCtx)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to enqueue update download", e)
|
||||||
|
sduLog("EXCEPTION enqueue: ${e::class.simpleName}: ${e.message}")
|
||||||
|
_updateState.value = UpdateState.Error("Failed to start download: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,11 +308,32 @@ object UpdateManager {
|
|||||||
* Отменить текущее скачивание
|
* Отменить текущее скачивание
|
||||||
*/
|
*/
|
||||||
fun cancelDownload() {
|
fun cancelDownload() {
|
||||||
downloadJob?.cancel()
|
val ctx = appContext
|
||||||
downloadJob = null
|
val id = activeDownloadId
|
||||||
val info = latestUpdateInfo
|
if (ctx != null && id != null) {
|
||||||
_updateState.value = if (info?.servicePackUrl != null)
|
runCatching {
|
||||||
UpdateState.UpdateAvailable(info.version) else UpdateState.Idle
|
val dm = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
dm.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadMonitorJob?.cancel()
|
||||||
|
downloadMonitorJob = null
|
||||||
|
activeDownloadId = null
|
||||||
|
activeApkPath?.let { path ->
|
||||||
|
runCatching {
|
||||||
|
val file = File(path)
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeApkPath = null
|
||||||
|
activeApkVersion = null
|
||||||
|
if (ctx != null) {
|
||||||
|
persistState(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
val version = latestUpdateInfo?.version
|
||||||
|
_updateState.value = if (version != null) UpdateState.UpdateAvailable(version) else UpdateState.Idle
|
||||||
_downloadProgress.value = 0
|
_downloadProgress.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,11 +341,165 @@ object UpdateManager {
|
|||||||
* Сбросить состояние
|
* Сбросить состояние
|
||||||
*/
|
*/
|
||||||
fun reset() {
|
fun reset() {
|
||||||
|
cancelDownload()
|
||||||
_updateState.value = UpdateState.Idle
|
_updateState.value = UpdateState.Idle
|
||||||
_downloadProgress.value = 0
|
_downloadProgress.value = 0
|
||||||
latestUpdateInfo = null
|
latestUpdateInfo = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prefs(context: Context) =
|
||||||
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private fun persistState(context: Context) {
|
||||||
|
prefs(context)
|
||||||
|
.edit()
|
||||||
|
.putLong(KEY_DOWNLOAD_ID, activeDownloadId ?: -1L)
|
||||||
|
.putString(KEY_APK_PATH, activeApkPath)
|
||||||
|
.putString(KEY_APK_VERSION, activeApkVersion)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpdateAvailableOrIdleState() {
|
||||||
|
val version = latestUpdateInfo?.version ?: activeApkVersion
|
||||||
|
_updateState.value = if (!version.isNullOrBlank()) {
|
||||||
|
UpdateState.UpdateAvailable(version)
|
||||||
|
} else {
|
||||||
|
UpdateState.Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restorePersistedState(context: Context) {
|
||||||
|
val preferences = prefs(context)
|
||||||
|
val restoredId = preferences.getLong(KEY_DOWNLOAD_ID, -1L).takeIf { it >= 0L }
|
||||||
|
val restoredPath = preferences.getString(KEY_APK_PATH, null)?.takeIf { it.isNotBlank() }
|
||||||
|
val restoredVersion = preferences.getString(KEY_APK_VERSION, null)?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
activeDownloadId = restoredId
|
||||||
|
activeApkPath = restoredPath
|
||||||
|
activeApkVersion = restoredVersion
|
||||||
|
|
||||||
|
if (restoredId != null) {
|
||||||
|
sduLog("Restored active update download id=$restoredId")
|
||||||
|
if (_updateState.value !is UpdateState.Downloading) {
|
||||||
|
_updateState.value = UpdateState.Downloading(_downloadProgress.value.coerceIn(0, 100))
|
||||||
|
}
|
||||||
|
startDownloadMonitor(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!restoredPath.isNullOrBlank()) {
|
||||||
|
val apk = File(restoredPath)
|
||||||
|
if (apk.exists()) {
|
||||||
|
_downloadProgress.value = 100
|
||||||
|
_updateState.value = UpdateState.ReadyToInstall(apk.absolutePath)
|
||||||
|
} else {
|
||||||
|
activeApkPath = null
|
||||||
|
activeApkVersion = null
|
||||||
|
persistState(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildApkFile(context: Context, version: String): File {
|
||||||
|
val updatesDir =
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
?: File(context.filesDir, "updates")
|
||||||
|
if (!updatesDir.exists()) {
|
||||||
|
updatesDir.mkdirs()
|
||||||
|
}
|
||||||
|
return File(updatesDir, "Rosetta-$version.apk")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDownloadMonitor(context: Context) {
|
||||||
|
val downloadId = activeDownloadId ?: return
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
downloadMonitorJob?.cancel()
|
||||||
|
downloadMonitorJob =
|
||||||
|
scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
downloadManager.query(query).use { cursor ->
|
||||||
|
if (cursor == null || !cursor.moveToFirst()) {
|
||||||
|
activeDownloadId = null
|
||||||
|
persistState(context)
|
||||||
|
setUpdateAvailableOrIdleState()
|
||||||
|
_downloadProgress.value = 0
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val status =
|
||||||
|
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
||||||
|
val downloadedBytes =
|
||||||
|
cursor.getLong(
|
||||||
|
cursor.getColumnIndexOrThrow(
|
||||||
|
DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val totalBytes =
|
||||||
|
cursor.getLong(
|
||||||
|
cursor.getColumnIndexOrThrow(
|
||||||
|
DownloadManager.COLUMN_TOTAL_SIZE_BYTES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_PENDING,
|
||||||
|
DownloadManager.STATUS_RUNNING,
|
||||||
|
DownloadManager.STATUS_PAUSED -> {
|
||||||
|
val progress =
|
||||||
|
if (totalBytes > 0L) {
|
||||||
|
((downloadedBytes * 100L) / totalBytes).toInt()
|
||||||
|
.coerceIn(0, 100)
|
||||||
|
} else {
|
||||||
|
_downloadProgress.value.coerceIn(0, 99)
|
||||||
|
}
|
||||||
|
_downloadProgress.value = progress
|
||||||
|
_updateState.value = UpdateState.Downloading(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
val localUri =
|
||||||
|
cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)
|
||||||
|
)
|
||||||
|
val resolvedPath =
|
||||||
|
runCatching { Uri.parse(localUri).path }.getOrNull()
|
||||||
|
?.takeIf { !it.isNullOrBlank() }
|
||||||
|
?: activeApkPath
|
||||||
|
if (!resolvedPath.isNullOrBlank() && File(resolvedPath).exists()) {
|
||||||
|
activeApkPath = resolvedPath
|
||||||
|
activeDownloadId = null
|
||||||
|
persistState(context)
|
||||||
|
_downloadProgress.value = 100
|
||||||
|
_updateState.value = UpdateState.ReadyToInstall(resolvedPath)
|
||||||
|
} else {
|
||||||
|
activeDownloadId = null
|
||||||
|
persistState(context)
|
||||||
|
_updateState.value =
|
||||||
|
UpdateState.Error("Downloaded file is missing")
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
val reason =
|
||||||
|
cursor.getInt(
|
||||||
|
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON)
|
||||||
|
)
|
||||||
|
activeDownloadId = null
|
||||||
|
persistState(context)
|
||||||
|
_updateState.value =
|
||||||
|
UpdateState.Error("Download failed (reason=$reason)")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(500L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Парсинг JSON ответа от SDU /updates/all
|
* Парсинг JSON ответа от SDU /updates/all
|
||||||
* Формат: {"items":[{"platform":"android","arch":"x64","version":"1.0.7","downloadUrl":"/kernel/android/x64/Rosetta-1.0.7.apk"}, ...]}
|
* Формат: {"items":[{"platform":"android","arch":"x64","version":"1.0.7","downloadUrl":"/kernel/android/x64/Rosetta-1.0.7.apk"}, ...]}
|
||||||
|
|||||||
Reference in New Issue
Block a user