diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt index 309bb956..290eea50 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt @@ -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() } } } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt index 0a673ae1..e58155ef 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt @@ -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 { diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt index a19294ed..52843c01 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItem.kt @@ -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() + } + } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt index 6ca59f66..a7d2060b 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemAdapter.kt @@ -35,6 +35,7 @@ class PluginCenterItemAdapter( fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) fun onUninstall(item: PluginCenterItem) fun onDetails(item: PluginCenterItem) + fun onUpdate(item: PluginCenterItem) } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt index fe573e17..906e7adf 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterItemViewHolder.kt @@ -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) diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt index e72959f1..81559bc1 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterViewModel.kt @@ -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, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt index 63fa88e2..a6385fab 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexRepository.kt @@ -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, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt index 509f5c85..b3181246 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt @@ -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 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, 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, 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?, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt index 5fad3a5a..fcf911ed 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt @@ -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 } + } \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a3b1f784..05dc0375 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1118,5 +1118,8 @@ فشل التحقق من السلامة عدم تطابق SHA-256.\n\nالمتوقّع: %1$s\nالفعلي: %2$s لم يتم توفير عنوان URL متاح للملحق الحالي + فشل التحديث + هل تريد بالتأكيد إلغاء التثبيت؟ + تفاصيل diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 150b7003..862d9fe7 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1113,5 +1113,8 @@ Integrity verification failed SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s No available URL provided for current plugin + Failed to update + Are you sure to uninstall? + Details diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e7439041..01507ae2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1116,5 +1116,8 @@ Discordancia de SHA-256.\n\nEsperado: %1$s\nReal: %2$s La verificación de integridad falló No se proporcionó ninguna URL disponible para el complemento actual + Error al actualizar + ¿Seguro que deseas desinstalar? + Detalles diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 25085e44..d29afd5b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1116,5 +1116,8 @@ Échec de la vérification de l\'intégrité Incohérence du SHA-256.\n\nAttendu : %1$s\nObtenu : %2$s Aucune URL disponible n\'a été fournie pour le plug-in actuel + Échec de la mise à jour + Voulez-vous vraiment désinstaller ? + Détails diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2b8eacd6..2026ecc4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1117,5 +1117,8 @@ 整合性の検証に失敗しました SHA-256 が一致しません.\n\n期待値: %1$s\n実際: %2$s 現在のプラグインに利用可能な URL は提供されていません + 更新に失敗しました + アンインストールしてもよろしいですか? + 詳細 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ffe9a744..d1caeba1 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1118,5 +1118,8 @@ 무결성 검증에 실패했습니다 SHA-256 값이 일치하지 않습니다.\n\n예상: %1$s\n실제: %2$s 현재 플러그인에 사용 가능한 URL 이 제공되지 않았습니다 + 업데이트 실패 + 정말 제거하시겠습니까? + 자세히 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0db6a97d..b825009f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1116,5 +1116,8 @@ Сбой проверки целостности Несовпадение SHA-256.\n\nОжидалось: %1$s\nФактически: %2$s Для текущего плагина не указан доступный URL-адрес + Не удалось обновить + Вы действительно хотите удалить? + Детали diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f50ae375..6f6cb15d 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1114,5 +1114,8 @@ 完整性驗證失敗 SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s 當前插件未提供可用的 URL + 更新失敗 + 是否確定卸載 + 異常詳情 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2fd426d6..d90d8148 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1114,5 +1114,8 @@ 完整性驗證失敗 SHA-256 驗證不一致.\n\n期望值: %1$s\n實際值: %2$s 當前外掛未提供可用的 URL + 更新失敗 + 是否確定解除安裝 + 異常詳情 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1c27454a..1c4f2b88 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1114,5 +1114,8 @@ 完整性验证失败 SHA-256 验证不一致.\n\n期望值: %1$s\n实际值: %2$s 当前插件未提供可用的 URL + 更新失败 + 是否确定卸载 + 异常详情 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0ff9668..92bc0067 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,8 +50,8 @@ 繁體中文 (香港) 繁體中文 (台灣) org.autojs.autojs6.%s - %.1fKB/%.1fKB - %.2fMB/%.2fMB + %.1fKiB/%.1fKiB + %.2fMiB/%.2fMiB shortcut_$_docs shortcut_$_log shortcut_$_plugin_center @@ -1369,5 +1369,8 @@ Integrity verification failed SHA-256 mismatch.\n\nExpected: %1$s\nActual: %2$s No available URL provided for current plugin + Failed to update + Are you sure to uninstall? + Details diff --git a/version.properties b/version.properties index 70c2fec5..cd739cd7 100644 --- a/version.properties +++ b/version.properties @@ -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