feat(update): 已下载 APK 直接复用 + 安装失败可重试不重下
Some checks are pending
Release / build (push) Waiting to run
Some checks are pending
Release / build (push) Waiting to run
- downloadApk 检测同版本已完成文件,存在则跳过下载 - 下载先写 .part 再改名,避免半截文件被误用 - install 失败保留 Ready(file, error),UI 显示重试安装 - NeedInstallPermission 携带文件,开启权限后可继续安装
This commit is contained in:
parent
571b270cb6
commit
481c57c535
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 20
|
versionCode = 21
|
||||||
versionName = "1.7.0"
|
versionName = "1.7.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class UpdateRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 下载 APK 到缓存目录。onProgress 回调下载字节数/总字节数(total 可能为 -1)。 */
|
/** 下载 APK 到缓存目录;同版本已下载完成时直接复用,不重复下载。 */
|
||||||
suspend fun downloadApk(
|
suspend fun downloadApk(
|
||||||
info: UpdateInfo,
|
info: UpdateInfo,
|
||||||
onProgress: (downloaded: Long, total: Long) -> Unit
|
onProgress: (downloaded: Long, total: Long) -> Unit
|
||||||
@ -64,6 +64,11 @@ class UpdateRepository @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
val dir = File(appContext.cacheDir, "apk_downloads").apply { mkdirs() }
|
val dir = File(appContext.cacheDir, "apk_downloads").apply { mkdirs() }
|
||||||
val file = File(dir, "rainytoken-" + info.versionName + ".apk")
|
val file = File(dir, "rainytoken-" + info.versionName + ".apk")
|
||||||
|
if (file.exists() && file.length() > 0L) {
|
||||||
|
return@withContext Result.success(file)
|
||||||
|
}
|
||||||
|
// 先写 .part,下载完整后再替换正式文件名,避免半截文件被当成完整包
|
||||||
|
val part = File(dir, file.name + ".part")
|
||||||
val request = Request.Builder().url(info.apkUrl).get().build()
|
val request = Request.Builder().url(info.apkUrl).get().build()
|
||||||
okHttpClient.newCall(request).execute().use { resp ->
|
okHttpClient.newCall(request).execute().use { resp ->
|
||||||
if (!resp.isSuccessful) {
|
if (!resp.isSuccessful) {
|
||||||
@ -71,7 +76,7 @@ class UpdateRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
val body = resp.body ?: return@withContext Result.failure(IOException("empty body"))
|
val body = resp.body ?: return@withContext Result.failure(IOException("empty body"))
|
||||||
val total = body.contentLength()
|
val total = body.contentLength()
|
||||||
file.outputStream().buffered().use { output ->
|
part.outputStream().buffered().use { output ->
|
||||||
body.byteStream().use { input ->
|
body.byteStream().use { input ->
|
||||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
var downloaded = 0L
|
var downloaded = 0L
|
||||||
@ -84,6 +89,8 @@ class UpdateRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
part.renameTo(file)
|
||||||
Result.success(file)
|
Result.success(file)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
|
|||||||
@ -508,9 +508,23 @@ private fun UpdateCard(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = InkMuted
|
color = InkMuted
|
||||||
)
|
)
|
||||||
|
if (phase.error != null) {
|
||||||
|
Spacer(modifier = Modifier.padding(top = 4.dp))
|
||||||
|
Text(
|
||||||
|
text = phase.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||||
Button(onClick = { viewModel.install(phase.file) }) {
|
Button(onClick = { viewModel.install(phase.file) }) {
|
||||||
Text(stringResource(R.string.update_install))
|
Text(
|
||||||
|
if (phase.error == null) {
|
||||||
|
stringResource(R.string.update_install)
|
||||||
|
} else {
|
||||||
|
"重试安装"
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UpdateViewModel.Phase.NeedInstallPermission -> {
|
is UpdateViewModel.Phase.NeedInstallPermission -> {
|
||||||
@ -529,6 +543,9 @@ private fun UpdateCard(
|
|||||||
}) {
|
}) {
|
||||||
Text(stringResource(R.string.update_enable_permission))
|
Text(stringResource(R.string.update_enable_permission))
|
||||||
}
|
}
|
||||||
|
TextButton(onClick = { viewModel.install(phase.file) }) {
|
||||||
|
Text("已开启,继续安装")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is UpdateViewModel.Phase.Error -> {
|
is UpdateViewModel.Phase.Error -> {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@ -30,8 +30,8 @@ class UpdateViewModel @Inject constructor(
|
|||||||
object Latest : Phase
|
object Latest : Phase
|
||||||
data class Available(val info: UpdateRepository.UpdateInfo) : Phase
|
data class Available(val info: UpdateRepository.UpdateInfo) : Phase
|
||||||
data class Downloading(val progress: Float) : Phase
|
data class Downloading(val progress: Float) : Phase
|
||||||
data class Ready(val file: File) : Phase
|
data class Ready(val file: File, val error: String? = null) : Phase
|
||||||
object NeedInstallPermission : Phase
|
data class NeedInstallPermission(val file: File) : Phase
|
||||||
data class Error(val message: String) : Phase
|
data class Error(val message: String) : Phase
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ class UpdateViewModel @Inject constructor(
|
|||||||
val canInstall = runCatching { context.packageManager.canRequestPackageInstalls() }
|
val canInstall = runCatching { context.packageManager.canRequestPackageInstalls() }
|
||||||
.getOrDefault(false)
|
.getOrDefault(false)
|
||||||
if (!canInstall) {
|
if (!canInstall) {
|
||||||
_uiState.update { it.copy(phase = Phase.NeedInstallPermission) }
|
_uiState.update { it.copy(phase = Phase.NeedInstallPermission(file)) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(phase = Phase.Ready(file)) }
|
_uiState.update { it.copy(phase = Phase.Ready(file)) }
|
||||||
@ -91,7 +91,7 @@ class UpdateViewModel @Inject constructor(
|
|||||||
val uri = try {
|
val uri = try {
|
||||||
FileProvider.getUriForFile(context, authority, file)
|
FileProvider.getUriForFile(context, authority, file)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_uiState.update { it.copy(phase = Phase.Error("安装文件无效:" + (e.message ?: "未知错误"))) }
|
_uiState.update { it.copy(phase = Phase.Ready(file, "安装文件无效:" + (e.message ?: "未知错误"))) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val actionView = Intent(Intent.ACTION_VIEW).apply {
|
val actionView = Intent(Intent.ACTION_VIEW).apply {
|
||||||
@ -105,7 +105,7 @@ class UpdateViewModel @Inject constructor(
|
|||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
runCatching { context.startActivity(actionInstall) }.onFailure { e ->
|
runCatching { context.startActivity(actionInstall) }.onFailure { e ->
|
||||||
_uiState.update { it.copy(phase = Phase.Error(e.message ?: "无法启动安装器")) }
|
_uiState.update { it.copy(phase = Phase.Ready(file, "无法启动安装器:" + (e.message ?: "未知错误"))) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user