6.7.0 - Alpha12 - 插件中心 M1 - 插件中心完善更新流程 (更新提示/下载/校验/安装/失败重试)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class PluginCenterItemAdapter(
|
||||
fun onToggleEnable(item: PluginCenterItem, enabled: Boolean)
|
||||
fun onUninstall(item: PluginCenterItem)
|
||||
fun onDetails(item: PluginCenterItem)
|
||||
fun onUpdate(item: PluginCenterItem)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user