6.7.0 - Alpha12 - 插件中心 M1 - 插件中心完善更新流程 (更新提示/下载/校验/安装/失败重试)

This commit is contained in:
SuperMonster003
2025-12-09 11:34:21 +08:00
parent 40443ae62a
commit d3a6212f99
21 changed files with 320 additions and 140 deletions

View File

@@ -13,7 +13,6 @@ import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.launch
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs.ui.error.ErrorDialogActivity
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance
@@ -74,15 +73,16 @@ class PluginCenterActivity : BaseActivity() {
positiveButton.setOnClickListener {
d.dismiss()
val url = input.trim().toString()
val context = this@PluginCenterActivity
lifecycleScope.launch {
runCatching {
PluginInstaller.installFromUrlWithPrompt(this@PluginCenterActivity, url)
PluginInstaller.installFromUrlWithPrompt(context, url)
}.onFailure { e ->
ErrorDialogActivity.showErrorDialog(
this@PluginCenterActivity,
R.string.text_failed_to_retrieve,
e.message ?: e.toString(),
)
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_retrieve)
.content(e.message ?: e.toString())
.positiveText(R.string.dialog_button_dismiss)
.show()
}
}
}

View File

@@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar
@@ -48,14 +49,42 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
}
override fun onUninstall(item: PluginCenterItem) {
val uri = Uri.parse("package:${item.packageName}")
val intent = Intent(Intent.ACTION_DELETE, uri)
uninstallLauncher.launch(intent)
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.text_confirm_to_uninstall)
.negativeText(R.string.dialog_button_cancel)
.neutralColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
val uri = Uri.parse("package:${item.packageName}")
val intent = Intent(Intent.ACTION_DELETE, uri)
uninstallLauncher.launch(intent)
}
.show()
}
override fun onDetails(item: PluginCenterItem) {
PluginInfoDialogManager.showPluginInfoDialog(contextRef, item)
}
override fun onUpdate(item: PluginCenterItem) {
val url = item.installableApkUrl
when {
url.isNullOrBlank() -> MaterialDialog.Builder(contextRef)
.title(R.string.text_failed_to_update)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
else -> viewLifecycleOwner.lifecycleScope.launch {
PluginInstaller.installFromUrlWithPrompt(
context = contextRef,
url = url,
expectedSha256 = item.installableApkSha256,
)
}
}
}
})
binding.pluginCenterRecyclerView.apply {

View File

@@ -1,6 +1,11 @@
package org.autojs.autojs.core.plugin.center
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import androidx.core.net.toUri
import com.afollestad.materialdialogs.MaterialDialog
import org.autojs.autojs6.R
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
@@ -37,8 +42,12 @@ data class PluginCenterItem(
val versionSummary: String
get() = formatVersionInfo(versionName, versionCode, versionDate)
val updatableVersionSummary: String?
get() = updatableVersionName?.let { formatVersionInfo(it, updatableVersionCode, updatableVersionDate) }
val updatableVersionSummary: String
get() = formatVersionInfo(
updatableVersionName ?: versionName,
updatableVersionCode ?: versionCode,
updatableVersionDate,
)
val isUpdatable: Boolean
get() = updatableVersionName != null
@@ -63,4 +72,23 @@ data class PluginCenterItem(
}
}
fun uninstall(context: Context) {
context.startActivity(Intent(Intent.ACTION_DELETE, "package:$packageName".toUri()))
}
fun uninstallWithPrompt(context: Context, dialog: MaterialDialog? = null) {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.text_confirm_to_uninstall)
.negativeText(R.string.dialog_button_cancel)
.neutralColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
runCatching { uninstall(context) }
dialog?.dismiss()
}
.show()
}
}

View File

@@ -35,6 +35,7 @@ class PluginCenterItemAdapter(
fun onToggleEnable(item: PluginCenterItem, enabled: Boolean)
fun onUninstall(item: PluginCenterItem)
fun onDetails(item: PluginCenterItem)
fun onUpdate(item: PluginCenterItem)
}
}

View File

@@ -18,8 +18,6 @@ import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
class PluginCenterItemViewHolder(
itemViewBinding: PluginCenterRecyclerViewItemBinding,
@@ -83,32 +81,15 @@ class PluginCenterItemViewHolder(
btnDeleteView.setButtonState(false)
}
if (item.isInstalled && item.isUpdatable) {
updatableBadgeView.isVisible = true
versionInfoForUpdateView.isVisible = true
// test
val updatableVersionName = item.versionName.also {
item.updatableVersionName = it
}
// test
val updatableVersionCode = item.versionCode?.let { it + 16 }?.also {
item.updatableVersionCode = it
}
// test
val updatableVersionDate = item.versionDate?.let {
DateTime.parse(it).plusDays(3).toString("yyyy-MM-dd")
}?.also {
item.updatableVersionDate = it
}
versionInfoForUpdateView.text = formatVersionInfo(updatableVersionName, updatableVersionCode, updatableVersionDate)
val showUpdate = item.isInstalled && item.isUpdatable
updatableBadgeView.isVisible = showUpdate
versionInfoForUpdateView.isVisible = showUpdate
if (showUpdate) {
versionInfoForUpdateView.text = item.updatableVersionSummary
btnUpdateView.setButtonState(true) {
ViewUtils.showToast(context, R.string.text_under_development)
listener.onUpdate(currentItem)
}
} else {
updatableBadgeView.isVisible = false
versionInfoForUpdateView.isVisible = false
btnUpdateView.setButtonState(false)
}
@@ -132,18 +113,6 @@ class PluginCenterItemViewHolder(
}
}
private fun formatVersionInfo(versionName: String, versionCode: Long?, versionDate: String?): String {
val code = versionCode?.takeIf { it > 0 }
val date = versionDate?.runCatching {
DateTimeFormat.forPattern("yyyy-MM-dd").print(DateTime.parse(this))
}?.getOrNull()
return buildString {
append(versionName)
code?.let { append(" ($it)") }
date?.let { append(" | $it") }
}
}
private fun LinearLayout.setButtonState(enabled: Boolean, onClickListener: View.OnClickListener? = null) {
this.isEnabled = enabled
this.setOnClickListener(onClickListener)

View File

@@ -12,7 +12,7 @@ import org.autojs.autojs6.R
/**
* Loads both index plugins and locally installed plugins,
* merging them into a PluginCenterItem list.
*
*
* zh-CN: 统一加载索引插件与本地已安装插件, 合并为 PluginCenterItem 列表.
*/
class PluginCenterViewModel : ViewModel() {
@@ -34,15 +34,15 @@ class PluginCenterViewModel : ViewModel() {
val installedByPkg = installed.associateBy { it.packageName }
// 1. Use index to drive UI first (ensuring "installable but not installed" items are displayed).
// 1. [ zh-CN ] 优先用索引驱动 UI (确保 "未安装但可安装" 的项也能显示).
// Use index to drive UI, to display installable and updatable items.
// zh-CN: 用索引驱动 UI, 展示可安装及可更新的项.
val fromIndex = indexEntries.map { e ->
val local = installedByPkg[e.packageName]
toPluginCenterItem(context, index = e, local = local)
}
// 2. Add items that "exist locally but not in index" (third-party or not indexed yet).
// 2. [ zh-CN ] 补充 "本地存在但索引里暂时没有" 的项 (第三方或暂未入索引).
// Add items that "exist locally but not in index" (third-party or not indexed yet).
// zh-CN: 补充 "本地存在但索引里暂时没有" 的项 (第三方或暂未入索引).
val extraLocals = installed
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
.map { local -> toPluginCenterItem(context, index = null, local = local) }
@@ -57,8 +57,6 @@ class PluginCenterViewModel : ViewModel() {
fun setEnabled(context: Context, packageName: String, enabled: Boolean) {
enableStore.setEnabled(context, packageName, enabled)
// Update in-memory state too, to avoid a second full refresh.
// zh-CN: 内存态也更新, 避免二次全量刷新.
_items.value = _items.value.map {
if (it.packageName == packageName) it.copy(isEnabled = enabled) else it
}
@@ -72,20 +70,31 @@ class PluginCenterViewModel : ViewModel() {
val author = local?.author ?: index?.author
val collaborators = index?.collaborators ?: emptyList()
val versionName = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown)
val versionNameLocal = local?.versionName ?: index?.versionName ?: context.getString(R.string.text_unknown)
val versionCodeLocal = local?.versionCode
val isInstalled = local != null
// Mark as updatable and populate update target information only when "installed and index version is higher".
// zh-CN: 仅当 "已安装且索引版本更高" 时, 标记可更新, 并填充可更新目标信息.
val (updatableName, updatableCode, updatableDate) = run {
val defaultVersionInfo = Triple(null, null, null)
versionCodeLocal ?: return@run defaultVersionInfo
val versionCodeIndex = index?.versionCode ?: return@run defaultVersionInfo
if (versionCodeIndex <= versionCodeLocal) return@run defaultVersionInfo
Triple(index.versionName, index.versionCode, index.versionDate)
}
val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled)
return PluginCenterItem(
title = title,
packageName = packageName,
versionName = versionName,
versionCode = local?.versionCode ?: index?.versionCode,
// TODO M1: 显示索引日期; 仅本地项时可为空.
versionName = versionNameLocal,
versionCode = versionCodeLocal ?: index?.versionCode,
versionDate = index?.versionDate,
updatableVersionName = index?.versionName,
updatableVersionCode = index?.versionCode,
updatableVersionDate = index?.versionDate,
updatableVersionName = updatableName,
updatableVersionCode = updatableCode,
updatableVersionDate = updatableDate,
author = author,
collaborators = collaborators,
description = description,

View File

@@ -16,9 +16,9 @@ class PluginIndexRepository {
description = "百度飞桨光学字符识别插件",
author = "SuperMonster003",
collaborators = emptyList(),
versionName = "0.1.0",
versionCode = 17L,
versionDate = "2025-11-21",
versionName = "0.1.5",
versionCode = 15L,
versionDate = "2025-11-25",
// TODO M1 若索引的下载地址/哈希/尺寸未知, 则先设置为 null.
apkUrl = null,
apkSha256 = null,

View File

@@ -2,7 +2,6 @@ package org.autojs.autojs.core.plugin.center
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View.MeasureSpec.UNSPECIFIED
@@ -10,7 +9,6 @@ import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
@@ -72,6 +70,7 @@ object PluginInfoDialogManager {
lastInstallTime = item.lastInstallTime,
lastUninstallTime = item.lastUninstallTime,
apkUrl = item.installableApkUrl,
sha256 = item.installableApkSha256,
)
showPluginInfoDialogInternal(context, item, info)
}
@@ -93,6 +92,8 @@ object PluginInfoDialogManager {
updatableVersion = item.updatableVersionSummary,
firstInstallTime = item.firstInstallTime,
lastUpdateTime = item.lastUpdateTime,
apkUrl = item.installableApkUrl,
sha256 = item.installableApkSha256,
)
showPluginInfoDialogInternal(context, item, info)
}
@@ -114,34 +115,50 @@ object PluginInfoDialogManager {
positiveText(R.string.text_install)
positiveColorRes(R.color.dialog_button_attraction)
onPositive { d, _ ->
info.apkUrl?.let { url ->
d.dismiss()
CoroutineScope(Dispatchers.IO).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, item.installableApkSha256)
val url = info.apkUrl
when {
url.isNullOrBlank() -> {
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_install)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
}
else -> {
d.dismiss()
CoroutineScope(Dispatchers.IO).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
} ?: run {
val positiveButton = d.getActionButton(DialogAction.POSITIVE)
positiveButton.setTextColor(d.context.getColor(R.color.dialog_button_unavailable))
MaterialDialog.Builder(d.context)
.title(R.string.text_failed_to_install)
.content(d.context.getString(R.string.error_no_available_url_provided_for_current_plugin))
}
}
}
is PluginInfoInstalled -> {
positiveText(R.string.text_uninstall)
positiveColorRes(R.color.dialog_button_warn)
onPositive { d, _ ->
// TODO 确定卸载对话框.
d.dismiss()
context.startActivity(Intent(Intent.ACTION_DELETE, "package:${item.packageName}".toUri()))
}
onPositive { d, _ -> item.uninstallWithPrompt(context, d) }
if (item.isUpdatable) {
neutralText(R.string.text_update)
neutralColorRes(R.color.dialog_button_attraction)
onNeutral { d, _ ->
d.dismiss()
// TODO 后续实现更新逻辑 (下载新包 -> 安装).
val url = info.apkUrl
when {
url.isNullOrBlank() -> {
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_update)
.content(R.string.error_no_available_url_provided_for_current_plugin)
.positiveText(R.string.dialog_button_dismiss)
.show()
}
else -> {
d.dismiss()
CoroutineScope(Dispatchers.IO).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
}
}
}
}
@@ -194,17 +211,17 @@ object PluginInfoDialogManager {
}
}
// Installable package: If the index does not provide size, try to HEAD request to get it, update display after success.
// zh-CN: 可安装包: 若索引未给 size, 尝试 HEAD 获取, 成功后更新显示.
if (info is PluginInfoInstallable && info.packageSize <= 0 && !info.apkUrl.isNullOrBlank()) {
// Asynchronously probe the size and refresh the view.
// zh-CN: 异步探测大小并刷新视图.
CoroutineScope(Dispatchers.IO).launch {
val size = PluginInstaller.probeContentLength(info.apkUrl)
if (size != null && size > 0 && currentDialog?.get() === dialog) {
// TODO 更新当前 item 的 "可安装包大小" 仅用于对话框展示 (持久化可留到 M2).
withContext(Dispatchers.Main) {
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, formatSize(size))
// If the index does not provide size, try to HEAD request to get it, update display after success.
// zh-CN: 若索引未给 size, 尝试 HEAD 获取, 成功后更新显示.
if (info.packageSize <= 0) {
info.apkUrl.takeUnless { it.isNullOrBlank() }?.let { url ->
CoroutineScope(Dispatchers.IO).launch {
val size = PluginInstaller.probeContentLength(url)
if (size != null && size > 0 && currentDialog?.get() === dialog) {
// TODO 更新当前 item 的 "可安装包大小" 仅用于对话框展示 (持久化可留到 M2).
withContext(Dispatchers.Main) {
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoPackageSizeValue, formatSize(size))
}
}
}
}
@@ -300,6 +317,8 @@ object PluginInfoDialogManager {
val collaborators: List<String>
val description: String
val packageSize: Long
val apkUrl: String?
val sha256: String?
}
private data class PluginInfoInstallable(
@@ -311,7 +330,8 @@ object PluginInfoDialogManager {
override val collaborators: List<String>,
override val description: String,
override val packageSize: Long,
val apkUrl: String?,
override val apkUrl: String?,
override val sha256: String?,
val lastInstallTime: Long?,
val lastUninstallTime: Long?,
) : PluginInfoBase
@@ -325,6 +345,8 @@ object PluginInfoDialogManager {
override val collaborators: List<String>,
override val description: String,
override val packageSize: Long,
override val apkUrl: String?,
override val sha256: String?,
val updatableVersion: String? = null,
val firstInstallTime: Long?,
val lastUpdateTime: Long?,

View File

@@ -12,12 +12,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.autojs.autojs.network.download.DownloadManager
import org.autojs.autojs.runtime.api.Mime
import org.autojs.autojs.ui.error.ErrorDialogActivity
import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager
import org.autojs.autojs.util.FileUtils
import org.autojs.autojs.util.FileUtils.toCacheFile
import org.autojs.autojs.util.UpdateUtils
import org.autojs.autojs6.R
import org.spongycastle.pqc.math.linearalgebra.IntegerFunctions.pow
import java.io.EOFException
import java.io.File
import java.io.IOException
@@ -30,14 +29,14 @@ import kotlin.math.roundToInt
/**
* Installer:
* - Local file: Uri installation (display APK info dialog before installation)
* - URL: Install after downloading to cache (display progress dialog during download)
*
* - Local file: Uri installation
* - URL: Install after downloading to cache
*
* zh-CN:
*
*
* 安装器:
* - 本地文件: Uri 安装 (安装前显示 APK 信息对话框)
* - URL: 下载到缓存后安装 (下载时显示进度对话框)
* - 本地文件: Uri 安装
* - URL: 下载到缓存后安装
*/
object PluginInstaller {
@@ -67,11 +66,11 @@ object PluginInstaller {
}
}
}.onFailure { e ->
ErrorDialogActivity.showErrorDialog(
context.applicationContext,
R.string.text_failed_to_install,
e.message ?: e.toString(),
)
MaterialDialog.Builder(context)
.title(R.string.text_failed_to_install)
.content(e.message ?: e.toString())
.positiveText(R.string.dialog_button_dismiss)
.show()
}
fun installFromFileUri(context: Context, uri: Uri) {
@@ -84,29 +83,77 @@ object PluginInstaller {
}
suspend fun installFromUrlWithPrompt(context: Context, url: String, expectedSha256: String? = null) {
when (val result = downloadWithProgress(context, url, expectedSha256)) {
is DownloadResult.Success -> installFromFileUriWithPrompt(context, result.uri)
is DownloadResult.Failure -> ErrorDialogActivity.showErrorDialog(
context,
result.titleRes,
result.message,
)
is DownloadResult.Cancelled -> {
// User cancelled, no need to prompt.
// zh-CN: 用户取消, 无需提示.
var attempt = 0
while (true) {
when (val result = downloadWithProgress(context, url, expectedSha256)) {
is DownloadResult.Success -> {
installFromFileUriWithPrompt(context, result.uri)
return
}
is DownloadResult.Cancelled -> {
// User cancelled, no need to prompt.
// zh-CN: 用户取消, 无需提示.
return
}
is DownloadResult.Failure -> {
val retry = showRetryDialog(context, result)
if (retry) {
attempt++
// TODO M1: 不做自动退避, 交给用户控制重试节奏; M2 可引入指数退避/网络可用性判断
continue
} else {
return
}
}
}
}
}
suspend fun installFromUrl(context: Context, url: String, expectedSha256: String? = null) {
val result = downloadWithProgress(context, url, expectedSha256)
if (result is DownloadResult.Success) {
installFromFileUri(context, result.uri)
} else if (result is DownloadResult.Failure) {
ErrorDialogActivity.showErrorDialog(context, result.titleRes, result.message)
when (val result = downloadWithProgress(context, url, expectedSha256)) {
is DownloadResult.Success -> {
installFromFileUri(context, result.uri)
}
is DownloadResult.Failure -> {
MaterialDialog.Builder(context)
.title(result.titleRes)
.content(result.message)
.positiveText(R.string.dialog_button_dismiss)
.show()
}
else -> Unit
}
}
private fun showRetryDialog(context: Context, failure: DownloadResult.Failure): Boolean {
var wantRetry = false
MaterialDialog.Builder(context)
.title(failure.titleRes)
.content(failure.message)
.neutralText(R.string.dialog_button_exception_details)
.neutralColorRes(R.color.dialog_button_hint)
.onNeutral { _, _ ->
MaterialDialog.Builder(context)
.title(failure.titleRes)
.content(failure.message)
.positiveText(R.string.dialog_button_dismiss)
.show()
}
.negativeText(R.string.dialog_button_quit)
.negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ -> d.dismiss() }
.positiveText(R.string.dialog_button_retry)
.positiveColorRes(R.color.dialog_button_attraction)
.onPositive { d, _ ->
wantRetry = true
d.dismiss()
}
.cancelable(false)
.autoDismiss(true)
.show()
return wantRetry
}
// Probe URL size (HEAD).
// zh-CN: 探测 URL 大小 (HEAD).
suspend fun probeContentLength(url: String): Long? = withContext(Dispatchers.IO) {
@@ -136,11 +183,29 @@ object PluginInstaller {
val dialog = MaterialDialog.Builder(context)
.title(R.string.text_downloading)
.progress(false, 100, true)
.negativeText(R.string.text_cancel)
.negativeColorRes(R.color.dialog_button_default)
.onNegative { d, _ ->
.neutralText(R.string.dialog_button_download_with_browser)
.neutralColorRes(R.color.dialog_button_hint)
.onNeutral { d, _ ->
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.text_download_interruption_warning)
.negativeText(R.string.dialog_button_back)
.negativeColorRes(R.color.dialog_button_hint)
.positiveText(R.string.dialog_button_continue)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
d.getActionButton(DialogAction.POSITIVE).performClick()
UpdateUtils.openUrl(context, url)
}
.cancelable(false)
.build()
.show()
}
.positiveText(R.string.dialog_button_cancel_download)
.positiveColorRes(R.color.dialog_button_default)
.onPositive { d, _ ->
cancelFlag.set(true)
d.getActionButton(DialogAction.NEGATIVE).isEnabled = false
d.getActionButton(DialogAction.POSITIVE).isEnabled = false
}
.cancelable(false)
.autoDismiss(false)
@@ -168,6 +233,16 @@ object PluginInstaller {
if (code !in 200..299) throw HttpStatusException(code, conn.responseMessage ?: "HTTP error")
val total = conn.contentLengthLong.takeIf { it > 0 } ?: -1L
if (total > 0) {
dialog.setProgressNumberFormat(
DownloadManager.getProgressMegaBytesFormat(
context,
/* downloadedMiB */ 0f,
/* totalMiB */ total / (1024f * 1024f),
)
)
}
conn.inputStream.use { input ->
val md = MessageDigest.getInstance("SHA-256")
DigestInputStream(input, md).use { din ->
@@ -183,17 +258,26 @@ object PluginInstaller {
fos.write(buf, 0, read)
downloaded += read
val now = System.currentTimeMillis()
if (total > 0 && (now - lastUpdateTs > 80)) {
val pct = ((downloaded * 100f) / total).coerceIn(0f, 100f)
withContext(Dispatchers.Main) {
dialog.setProgressNumberFormat(DownloadManager.getProgressMegaBytesFormat(
context,
downloaded.toFloat() / pow(2, 20),
total.toFloat() / pow(2, 20),
))
dialog.setProgressNumberFormat(
DownloadManager.getProgressMegaBytesFormat(
context,
downloaded / (1024f * 1024f),
total / (1024f * 1024f),
)
)
dialog.setProgress(pct.roundToInt())
}
lastUpdateTs = now
} else if (total <= 0 && (now - lastUpdateTs > 300)) {
withContext(Dispatchers.Main) {
val cur = dialog.currentProgress
dialog.setProgress((cur + 1).coerceAtMost(99))
}
lastUpdateTs = now
}
}
fos.flush()
@@ -215,7 +299,10 @@ object PluginInstaller {
} catch (he: HttpStatusException) {
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "HTTP ${he.code}: ${he.message}")
} catch (me: ChecksumMismatchException) {
return DownloadResult.Failure(R.string.text_integrity_verification_failed, context.getString(R.string.text_sha256_mismatch_multiline_expected_actual, me.expected, me.actual))
return DownloadResult.Failure(
R.string.text_integrity_verification_failed,
context.getString(R.string.text_sha256_mismatch_multiline_expected_actual, me.expected, me.actual),
)
} catch (ioe: EOFException) {
return DownloadResult.Failure(R.string.text_failed_to_retrieve, "Unexpected EOF: ${ioe.message}")
} catch (ioe: IOException) {
@@ -236,6 +323,7 @@ object PluginInstaller {
}
private data class HttpStatusException(val code: Int, override val message: String) : RuntimeException(message)
private data class ChecksumMismatchException(val expected: String, val actual: String) : RuntimeException("sha256 mismatch")
sealed interface DownloadResult {
@@ -243,4 +331,5 @@ object PluginInstaller {
data class Failure(val titleRes: Int, val message: String) : DownloadResult
data object Cancelled : DownloadResult
}
}

View File

@@ -1118,5 +1118,8 @@
<string name="text_integrity_verification_failed">فشل التحقق من السلامة</string>
<string name="text_sha256_mismatch_multiline_expected_actual">عدم تطابق SHA-256.\n\nالمتوقّع: %1$s\nالفعلي: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">لم يتم توفير عنوان URL متاح للملحق الحالي</string>
<string name="text_failed_to_update">فشل التحديث</string>
<string name="text_confirm_to_uninstall">هل تريد بالتأكيد إلغاء التثبيت؟</string>
<string name="dialog_button_exception_details">تفاصيل</string>
</resources>

View File

@@ -1113,5 +1113,8 @@
<string name="text_integrity_verification_failed">Integrity verification failed</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
<string name="text_failed_to_update">Failed to update</string>
<string name="text_confirm_to_uninstall">Are you sure to uninstall?</string>
<string name="dialog_button_exception_details">Details</string>
</resources>

View File

@@ -1116,5 +1116,8 @@
<string name="text_sha256_mismatch_multiline_expected_actual">Discordancia de SHA-256.\n\nEsperado: %1$s\nReal: %2$s</string>
<string name="text_integrity_verification_failed">La verificación de integridad falló</string>
<string name="error_no_available_url_provided_for_current_plugin">No se proporcionó ninguna URL disponible para el complemento actual</string>
<string name="text_failed_to_update">Error al actualizar</string>
<string name="text_confirm_to_uninstall">¿Seguro que deseas desinstalar?</string>
<string name="dialog_button_exception_details">Detalles</string>
</resources>

View File

@@ -1116,5 +1116,8 @@
<string name="text_integrity_verification_failed">Échec de la vérification de l\'intégrité</string>
<string name="text_sha256_mismatch_multiline_expected_actual">Incohérence du SHA-256.\n\nAttendu : %1$s\nObtenu : %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">Aucune URL disponible n\'a été fournie pour le plug-in actuel</string>
<string name="text_failed_to_update">Échec de la mise à jour</string>
<string name="text_confirm_to_uninstall">Voulez-vous vraiment désinstaller ?</string>
<string name="dialog_button_exception_details">Détails</string>
</resources>

View File

@@ -1117,5 +1117,8 @@
<string name="text_integrity_verification_failed">整合性の検証に失敗しました</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 が一致しません.\n\n期待値: %1$s\n実際: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">現在のプラグインに利用可能な URL は提供されていません</string>
<string name="text_failed_to_update">更新に失敗しました</string>
<string name="text_confirm_to_uninstall">アンインストールしてもよろしいですか?</string>
<string name="dialog_button_exception_details">詳細</string>
</resources>

View File

@@ -1118,5 +1118,8 @@
<string name="text_integrity_verification_failed">무결성 검증에 실패했습니다</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 값이 일치하지 않습니다.\n\n예상: %1$s\n실제: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">현재 플러그인에 사용 가능한 URL 이 제공되지 않았습니다</string>
<string name="text_failed_to_update">업데이트 실패</string>
<string name="text_confirm_to_uninstall">정말 제거하시겠습니까?</string>
<string name="dialog_button_exception_details">자세히</string>
</resources>

View File

@@ -1116,5 +1116,8 @@
<string name="text_integrity_verification_failed">Сбой проверки целостности</string>
<string name="text_sha256_mismatch_multiline_expected_actual">Несовпадение SHA-256.\n\nОжидалось: %1$s\nФактически: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">Для текущего плагина не указан доступный URL-адрес</string>
<string name="text_failed_to_update">Не удалось обновить</string>
<string name="text_confirm_to_uninstall">Вы действительно хотите удалить?</string>
<string name="dialog_button_exception_details">Детали</string>
</resources>

View File

@@ -1114,5 +1114,8 @@
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">當前插件未提供可用的 URL</string>
<string name="text_failed_to_update">更新失敗</string>
<string name="text_confirm_to_uninstall">是否確定卸載</string>
<string name="dialog_button_exception_details">異常詳情</string>
</resources>

View File

@@ -1114,5 +1114,8 @@
<string name="text_integrity_verification_failed">完整性驗證失敗</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">當前外掛未提供可用的 URL</string>
<string name="text_failed_to_update">更新失敗</string>
<string name="text_confirm_to_uninstall">是否確定解除安裝</string>
<string name="dialog_button_exception_details">異常詳情</string>
</resources>

View File

@@ -1114,5 +1114,8 @@
<string name="text_integrity_verification_failed">完整性验证失败</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 验证不一致.\n\n期望值: %1$s\n实际值: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">当前插件未提供可用的 URL</string>
<string name="text_failed_to_update">更新失败</string>
<string name="text_confirm_to_uninstall">是否确定卸载</string>
<string name="dialog_button_exception_details">异常详情</string>
</resources>

View File

@@ -50,8 +50,8 @@
<string name="entry_app_language_zh_hant_hk" translatable="false">繁體中文 (香港)</string>
<string name="entry_app_language_zh_hant_tw" translatable="false">繁體中文 (台灣)</string>
<string name="format_default_package_name" formatted="true" translatable="false">org.autojs.autojs6.%s</string>
<string name="format_dialog_progress_number_format_kilo_bytes" translatable="false">%.1fKB/%.1fKB</string>
<string name="format_dialog_progress_number_format_mega_bytes" translatable="false">%.2fMB/%.2fMB</string>
<string name="format_dialog_progress_number_format_kilo_bytes" translatable="false">%.1fKiB/%.1fKiB</string>
<string name="format_dialog_progress_number_format_mega_bytes" translatable="false">%.2fMiB/%.2fMiB</string>
<string name="id_launcher_shortcut_docs" translatable="false">shortcut_$_docs</string>
<string name="id_launcher_shortcut_log" translatable="false">shortcut_$_log</string>
<string name="id_launcher_shortcut_plugin_center" translatable="false">shortcut_$_plugin_center</string>
@@ -1369,5 +1369,8 @@
<string name="text_integrity_verification_failed">Integrity verification failed</string>
<string name="text_sha256_mismatch_multiline_expected_actual">SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s</string>
<string name="error_no_available_url_provided_for_current_plugin">No available URL provided for current plugin</string>
<string name="text_failed_to_update">Failed to update</string>
<string name="text_confirm_to_uninstall">Are you sure to uninstall?</string>
<string name="dialog_button_exception_details">Details</string>
</resources>

View File

@@ -1,5 +1,5 @@
#Mon Dec 08 20:22:09 CST 2025
BUILD_TIME=1765196529144
#Tue Dec 09 11:33:43 CST 2025
BUILD_TIME=1765251223946
COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3528
VERSION_BUILD=3530
VERSION_NAME=6.7.0 Alpha12
VSCODE_EXT_REQUIRED_VERSION=1.0.8