diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 287e3770..5acbde22 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,12 @@
+
+
+
+
@@ -132,10 +138,6 @@
android:protectionLevel="signature"
tools:ignore="ProtectedPermissions" />
-
-
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 e58155ef..d6af9151 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
@@ -4,10 +4,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.net.Uri
import android.os.Bundle
import android.view.View
-import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
@@ -15,6 +14,8 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar
@@ -30,12 +31,14 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
private lateinit var adapter: PluginCenterItemAdapter
private lateinit var contextRef: Context
- private val uninstallLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- vm.load(requireContext())
- }
-
private var pkgReceiver: BroadcastReceiver? = null
+ // Job for delaying the display of empty state hint.
+ // zh-CN: 用于延迟显示空态提示的 Job.
+ private var emptyHintJob: Job? = null
+
+ private var isFirstEnter: Boolean = true
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentPluginCenterBinding.bind(view)
@@ -57,9 +60,9 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { _, _ ->
- val uri = Uri.parse("package:${item.packageName}")
+ val uri = "package:${item.packageName}".toUri()
val intent = Intent(Intent.ACTION_DELETE, uri)
- uninstallLauncher.launch(intent)
+ startActivity(intent)
}
.show()
}
@@ -94,23 +97,106 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
excludePaddingClippableViewFromBottomNavigationBar()
}
- // Load data.
- // zh-CN: 加载数据.
- vm.load(context)
+ // Pull-to-refresh: force refresh index every time (still respects backoff window).
+ // zh-CN: 下拉刷新: 每次都强制刷新索引 (仍遵守退避窗口).
+ binding.pluginCenterSwipeRefresh.setOnRefreshListener {
+ vm.load(contextRef, forceRefreshIndex = true)
+ }
- // Subscribe data updates.
- // zh-CN: 订阅数据更新.
+ // First entry: local priority + index async.
+ // zh-CN: 首次进入: 本地优先 + 索引异步.
+ vm.load(context, forceRefreshIndex = false)
+
+ // Subscribe to list data.
+ // zh-CN: 订阅列表数据.
viewLifecycleOwner.lifecycleScope.launch {
vm.items.collectLatest { list ->
adapter.updateData(list)
+ updateEmptyHint(list, vm.indexLoaded.value)
+ }
+ }
+
+ // Subscribe to index loading completion status, used to update pull-to-refresh animation and empty state text.
+ // zh-CN: 订阅索引加载完成状态, 用于更新下拉刷新动画与空态文案.
+ viewLifecycleOwner.lifecycleScope.launch {
+ vm.indexLoaded.collectLatest { loaded ->
+ binding.pluginCenterSwipeRefresh.isRefreshing = false
+ updateEmptyHint(adapter.items(), loaded)
+ }
+ }
+
+ // Subscribe to locally discovered fatal exceptions.
+ // zh-CN: 订阅本地发现致命异常.
+ viewLifecycleOwner.lifecycleScope.launch {
+ vm.fatalError.collectLatest { msg ->
+ MaterialDialog.Builder(contextRef)
+ .title(R.string.text_error)
+ .content(msg)
+ .positiveText(R.string.dialog_button_dismiss)
+ .onPositive { _, _ -> requireActivity().finish() }
+ .cancelable(false)
+ .show()
+ }
+ }
+ }
+
+ private fun updateEmptyHint(items: List, indexLoaded: Boolean) {
+ val hintView = binding.pluginCenterEmptyHint
+
+ when {
+ items.isNotEmpty() -> {
+ // Has data: hide hint immediately and cancel any waiting tasks.
+ // zh-CN: 有数据: 立即隐藏提示, 并取消任何等待任务.
+ emptyHintJob?.cancel()
+ emptyHintJob = null
+ hintView.visibility = View.GONE
+ isFirstEnter = false
+ }
+ indexLoaded -> {
+ // Local and index stages have ended, list is still empty, immediately show "no plugins" hint.
+ // zh-CN: 本地与索引阶段已结束, 列表仍为空, 立即显示"没有插件"提示.
+ emptyHintJob?.cancel()
+ emptyHintJob = null
+ hintView.visibility = View.VISIBLE
+ hintView.setText(R.string.text_no_plugins_installed_hint)
+ isFirstEnter = false
+ }
+ else -> {
+ // Local is empty, index still being fetched.
+ // zh-CN: 本地为空, 索引仍在获取.
+ hintView.visibility = View.GONE
+
+ if (isFirstEnter) {
+ // First entry: wait at most 0.5 seconds, if still no data then show "retrieving..." hint.
+ // zh-CN: 首次进入: 最多等待 0.5 秒, 若仍无数据再显示 "正在获取..." 提示.
+ if (emptyHintJob == null || emptyHintJob?.isCancelled == true) {
+ emptyHintJob = viewLifecycleOwner.lifecycleScope.launch {
+ delay(500L)
+ if (isAdded && _binding != null) {
+ val latestItems = adapter.items()
+ val latestIndexLoaded = vm.indexLoaded.value
+ if (latestItems.isEmpty() && !latestIndexLoaded) {
+ hintView.visibility = View.VISIBLE
+ hintView.setText(R.string.text_retrieving_plugin_list_data)
+ }
+ }
+ }
+ }
+ } else {
+ // Not first time (including pull-to-refresh/auto refresh when returning to page): show loading hint immediately without delay.
+ // zh-CN: 非首次 (包括下拉刷新/返回页面自动刷新): 立即显示加载提示, 不做延迟.
+ emptyHintJob?.cancel()
+ emptyHintJob = null
+ hintView.visibility = View.VISIBLE
+ hintView.setText(R.string.text_retrieving_plugin_list_data)
+ }
}
}
}
override fun onStart() {
super.onStart()
- run registerPackageReceiver@{
- pkgReceiver ?: return@registerPackageReceiver
+ pkgReceiver ?: run registerPackageReceiver@{
pkgReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val data = intent.data ?: return
@@ -121,13 +207,13 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
// Package installation (update) does not record "Recently installed" when replacing, but will refresh.
// zh-CN: 替换安装 (更新) 不记录 "最近安装", 但会刷新.
if (!replacing) PluginRecentStore.setLastInstalled(packageName)
- vm.load(context)
+ vm.load(context, forceRefreshIndex = false)
}
Intent.ACTION_PACKAGE_REMOVED -> {
// Package uninstallation (pre-update phase) does not record "Recently uninstalled" when replacing.
// zh-CN: 替换卸载 (更新前阶段) 不记录 "最近卸载".
if (!replacing) PluginRecentStore.setLastUninstalled(packageName)
- vm.load(context)
+ vm.load(context, forceRefreshIndex = false)
}
}
}
@@ -149,15 +235,17 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
override fun onResume() {
super.onResume()
- // Refresh once when returning to the page to update install/uninstall status.
+ // Refresh once when returning to page, to cover state after installation/uninstallation.
// zh-CN: 回到页面时刷新一次, 覆盖安装/卸载后的状态.
- if (::contextRef.isInitialized) {
- vm.load(contextRef)
+ if (!isFirstEnter && ::contextRef.isInitialized) {
+ vm.load(contextRef, forceRefreshIndex = false)
}
}
override fun onDestroyView() {
super.onDestroyView()
+ emptyHintJob?.cancel()
+ emptyHintJob = null
_binding = null
}
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 52843c01..927784be 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
@@ -20,7 +20,7 @@ data class PluginCenterItem(
var updatableVersionDate: String? = null,
val author: String? = null,
val collaborators: List = emptyList(),
- val description: String,
+ val description: String? = null,
// Size of installed package (aggregated base + splits), 0 for uninstalled.
// zh-CN: 已安装包大小 (聚合 base + splits), 未安装为 0.
@@ -42,12 +42,15 @@ data class PluginCenterItem(
val versionSummary: String
get() = formatVersionInfo(versionName, versionCode, versionDate)
- val updatableVersionSummary: String
- get() = formatVersionInfo(
- updatableVersionName ?: versionName,
- updatableVersionCode ?: versionCode,
- updatableVersionDate,
- )
+ val updatableVersionSummary: String?
+ get() = when {
+ isUpdatable -> formatVersionInfo(
+ updatableVersionName ?: versionName,
+ updatableVersionCode ?: versionCode,
+ updatableVersionDate,
+ )
+ else -> null
+ }
val isUpdatable: Boolean
get() = updatableVersionName != null
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 906e7adf..ed89c244 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
@@ -68,10 +68,14 @@ class PluginCenterItemViewHolder(
switchView.setOnCheckedChangeListener(null)
switchView.isChecked = item.isEnabled
- titleView.text = item.title
- versionInfoView.text = item.versionSummary
- authorView.text = item.author
- descriptionView.text = item.description
+ titleView.text = item.title.takeUnless { it.isBlank() }
+ ?: "[ ${context.getString(R.string.text_unknown_title_for_plugin)} ]"
+ versionInfoView.text = item.versionSummary.takeUnless { it.isBlank() }
+ ?: "[ ${context.getString(R.string.text_unknown_version_for_plugin)} ]"
+ authorView.text = item.author.takeUnless { it.isNullOrBlank() }
+ ?: "[ ${context.getString(R.string.text_unknown_author_for_plugin)} ]"
+ descriptionView.text = item.description.takeUnless { it.isNullOrBlank() }
+ ?: "[ ${context.getString(R.string.text_unknown_description_for_plugin)} ]"
if (item.isInstalled) {
btnDeleteView.setButtonState(true) {
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 81559bc1..cc5b67fa 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
@@ -1,10 +1,12 @@
package org.autojs.autojs.core.plugin.center
import android.content.Context
+import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.autojs.autojs6.R
@@ -13,7 +15,19 @@ import org.autojs.autojs6.R
* Loads both index plugins and locally installed plugins,
* merging them into a PluginCenterItem list.
*
- * zh-CN: 统一加载索引插件与本地已安装插件, 合并为 PluginCenterItem 列表.
+ * Behavior strategy:
+ * - First load local plugins and immediately push the list (local priority).
+ * - Then load the index in the background, merge with local plugins and push again after success.
+ * - If local discovery fails, notify UI to show a dialog and exit Activity through fatalError.
+ *
+ * zh-CN:
+ *
+ * 统一加载索引插件与本地已安装插件, 合并为 PluginCenterItem 列表.
+ *
+ * 行为策略:
+ * - 先加载本地插件并立即推送列表 (本地优先).
+ * - 紧接着在后台加载索引, 成功后与本地合并再推送一次.
+ * - 若本地发现失败, 通过 fatalError 通知 UI 弹窗并退出 Activity.
*/
class PluginCenterViewModel : ViewModel() {
@@ -24,33 +38,91 @@ class PluginCenterViewModel : ViewModel() {
private val _items = MutableStateFlow>(emptyList())
val items: StateFlow> = _items
- fun load(context: Context) {
- viewModelScope.launch {
- val idxDeferred = async { runCatching { indexRepo.fetchOfficialIndex(context) }.getOrElse { emptyList() } }
- val insDeferred = async { runCatching { installedRepo.discoverInstalled(context) }.getOrElse { emptyList() } }
+ // Whether index loading is completed (includes "success/failure/empty" results, only means "no longer loading").
+ // zh-CN: 索引加载是否已完成 (包含 "成功/失败/空" 三种结果, 仅代表 "不再加载").
+ private val _indexLoaded = MutableStateFlow(false)
+ val indexLoaded: StateFlow = _indexLoaded
- val indexEntries = idxDeferred.await()
- val installed = insDeferred.await()
+ // Fatal exception discovered locally (requires user notification and Activity exit).
+ // zh-CN: 本地发现的致命异常 (需要提示用户并退出 Activity).
+ private val _fatalError = MutableSharedFlow(extraBufferCapacity = 1)
+ val fatalError: SharedFlow = _fatalError
+
+ /**
+ * Load plugin list.
+ *
+ * zh-CN: 加载插件列表.
+ *
+ * @param forceRefreshIndex Whether to force refresh index.
+ * - false: Use 1 minute throttling + ETag/backoff (normal page entry).
+ * - true: Attempt to refresh index every time (still respects backoff window, for pull-to-refresh usage).
+ *
+ * zh-CN: 是否强制刷新索引.
+ * - false: 使用 1 分钟节流 + ETag/退避 (普通进入页面).
+ * - true: 每次都尝试刷新索引 (仍遵守退避窗口, 供下拉刷新使用).
+ */
+ fun load(context: Context, forceRefreshIndex: Boolean = false) {
+ Log.d(TAG, "load: forceRefreshIndex = $forceRefreshIndex")
+ viewModelScope.launch {
+ _indexLoaded.value = false
+
+ // First load local plugins (local priority).
+ // zh-CN: 先加载本地插件 (本地优先).
+ val installed = runCatching {
+ installedRepo.discoverInstalled(context)
+ }.onFailure { e ->
+ // Treat "local discovery" failure as a serious exception.
+ // zh-CN: 将 "本地发现" 失败视为严重异常.
+ val message = context.getString(
+ R.string.error_failed_to_load_plugins_with_reason,
+ e.message ?: e.toString(),
+ )
+ _fatalError.tryEmit(message)
+ }.getOrNull() ?: run {
+ // Do not continue subsequent logic when unable to obtain local list.
+ // zh-CN: 无法获取本地列表时不再继续后续逻辑.
+ _items.value = emptyList()
+ _indexLoaded.value = true
+ return@launch
+ }
+
+ // Render list using "local only".
+ // zh-CN: 使用 "仅本地" 渲染列表.
+ val onlyLocalItems = installed.mapNotNull { local ->
+ toPluginCenterItem(context, index = null, local = local)
+ }
+ _items.value = onlyLocalItems
+
+ // Asynchronously load index and merge.
+ // zh-CN: 异步加载索引并合并.
+ val indexEntries = runCatching {
+ indexRepo.fetchOfficialIndex(context, forceRefresh = forceRefreshIndex)
+ }.onFailure {
+ // Index fetch exception is not fatal, just log it.
+ // zh-CN: 索引获取异常不算致命, 使用日志记录即可.
+ Log.w(TAG, "fetchOfficialIndex failed: ${it.message}")
+ }.getOrElse { emptyList() }
val installedByPkg = installed.associateBy { it.packageName }
- // Use index to drive UI, to display installable and updatable items.
- // zh-CN: 用索引驱动 UI, 展示可安装及可更新的项.
- val fromIndex = indexEntries.map { e ->
+ // Use index to drive UI, showing installable/updatable.
+ // zh-CN: 用索引驱动 UI, 展示 installable/updatable.
+ val fromIndex = indexEntries.mapNotNull { e ->
val local = installedByPkg[e.packageName]
toPluginCenterItem(context, index = e, local = local)
}
- // Add items that "exist locally but not in index" (third-party or not indexed yet).
- // zh-CN: 补充 "本地存在但索引里暂时没有" 的项 (第三方或暂未入索引).
+ // Supplement plugins that "exist locally but not in index" (third-party/not yet in index).
+ // zh-CN: 补充 "本地有但索引没有" 的插件 (第三方/暂未入索引).
val extraLocals = installed
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
- .map { local -> toPluginCenterItem(context, index = null, local = local) }
+ .mapNotNull { local -> toPluginCenterItem(context, index = null, local = local) }
_items.value = fromIndex + extraLocals
+ _indexLoaded.value = true
- // Refresh dialog if showing when returning to the page.
- // zh-CN: 返回页面时, 对话框如果正在显示则刷新.
+ // When returning to the page, refresh if the details dialog is being displayed.
+ // zh-CN: 返回页面时, 若详情对话框正在显示则刷新.
PluginInfoDialogManager.refreshIfShowing(context, _items.value)
}
}
@@ -63,10 +135,12 @@ class PluginCenterViewModel : ViewModel() {
PluginInfoDialogManager.refreshIfShowing(context, _items.value)
}
- private fun toPluginCenterItem(context: Context, index: PluginIndexEntry?, local: InstalledPluginRepository.InstalledPlugin?): PluginCenterItem {
- val packageName = local?.packageName ?: index?.packageName.orEmpty()
+ private fun toPluginCenterItem(context: Context, index: PluginIndexEntry?, local: InstalledPluginRepository.InstalledPlugin?): PluginCenterItem? {
+ val packageName = local?.packageName ?: index?.packageName
+ if (packageName.isNullOrBlank()) return null
+
val title = local?.title ?: index?.title ?: packageName
- val description = local?.description ?: index?.description.orEmpty()
+ val description = local?.description ?: index?.description
val author = local?.author ?: index?.author
val collaborators = index?.collaborators ?: emptyList()
@@ -74,10 +148,10 @@ class PluginCenterViewModel : ViewModel() {
val versionCodeLocal = local?.versionCode
val isInstalled = local != null
- // Mark as updatable and populate update target information only when "installed and index version is higher".
+ // Only mark as updatable when "installed and index version is higher", and fill in updatable target information.
// zh-CN: 仅当 "已安装且索引版本更高" 时, 标记可更新, 并填充可更新目标信息.
val (updatableName, updatableCode, updatableDate) = run {
- val defaultVersionInfo = Triple(null, null, null)
+ val defaultVersionInfo = Triple(null, null, null)
versionCodeLocal ?: return@run defaultVersionInfo
val versionCodeIndex = index?.versionCode ?: return@run defaultVersionInfo
if (versionCodeIndex <= versionCodeLocal) return@run defaultVersionInfo
@@ -112,4 +186,9 @@ class PluginCenterViewModel : ViewModel() {
settings = null,
)
}
-}
+
+ companion object {
+ private const val TAG = "PluginCenterViewModel"
+ }
+
+}
\ No newline at end of file
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 a6385fab..bf97edb2 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
@@ -1,35 +1,282 @@
package org.autojs.autojs.core.plugin.center
import android.content.Context
+import android.util.Log
+import androidx.core.content.edit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.autojs.autojs6.BuildConfig
+import org.autojs.autojs6.R
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.File
+import java.net.ConnectException
+import java.net.HttpURLConnection
+import java.net.NoRouteToHostException
+import java.net.URL
+import java.net.UnknownHostException
+import kotlin.math.min
-/**
- * 官方插件索引仓库 (M1: 内置静态; M2 再接入网络/ETag/缓存).
- */
class PluginIndexRepository {
- suspend fun fetchOfficialIndex(context: Context): List {
- // TODO M1 先预置 1 条官方样例 "Paddle OCR (PP-OCRv5)", 便于与本地已安装合并显示.
- return listOf(
- PluginIndexEntry(
- packageName = "io.github.supermonster003.autojs6.plugin.paddleocr.v5",
- title = "Paddle OCR (PP-OCRv5)",
- description = "百度飞桨光学字符识别插件",
- author = "SuperMonster003",
- collaborators = emptyList(),
- versionName = "0.1.5",
- versionCode = 15L,
- versionDate = "2025-11-25",
- // TODO M1 若索引的下载地址/哈希/尺寸未知, 则先设置为 null.
- apkUrl = null,
- apkSha256 = null,
- apkSizeBytes = null,
- // TODO M1 暂不拉网图标, 使用应用图标或默认图标.
- iconUrl = null,
- tags = listOf("official", "ocr", "paddle", "v5"),
- engine = "paddle-ocr",
- variant = "v5",
- engineId = "paddle-ocr-pp-ocrv5",
- ),
- )
+ companion object {
+ private const val TAG = "PluginIndexRepository"
+
+ private const val INDEX_URL = "https://raw.githubusercontent.com/SuperMonster005/autojs6-plugin-index/main/index.json"
+
+ private const val SP = "plugin_center_index"
+ private const val KEY_ETAG = "etag"
+ private const val KEY_LAST_SUCCESS_TS = "last_success_ts"
+ private const val KEY_LAST_FAILURE_TS = "last_failure_ts"
+ private const val KEY_RETRY_ATTEMPTS = "retry_attempts"
+
+ private const val CACHE_FILE_NAME = "plugin_center_index_cache.json"
+
+ private const val MIN_RETRY_INTERVAL_MS = 30_000L // 30 sec
+ private const val MAX_RETRY_INTERVAL_MS = 10 * 60_000L // 10 min
+ private const val MIN_REFRESH_INTERVAL_MS = 1 * 60_000L // 1 min
}
+
+ @Volatile
+ private var memoryCache: List? = null
+
+ suspend fun fetchOfficialIndex(context: Context, forceRefresh: Boolean = false): List =
+ withContext(Dispatchers.IO) {
+ memoryCache?.let { return@withContext it }
+
+ val sp = context.getSharedPreferences(SP, Context.MODE_PRIVATE)
+ val cacheFile = File(context.cacheDir, CACHE_FILE_NAME)
+
+ val now = System.currentTimeMillis()
+ val lastFailureTs = sp.getLong(KEY_LAST_FAILURE_TS, 0L)
+ val retryAttempts = sp.getInt(KEY_RETRY_ATTEMPTS, 0)
+ val lastSuccessTs = sp.getLong(KEY_LAST_SUCCESS_TS, 0L)
+
+ val cachedEntries: List? = readCacheSafely(cacheFile)
+
+ // Throttle for MIN_REFRESH_INTERVAL_MS time.
+ // zh-CN: 节流 MIN_REFRESH_INTERVAL_MS 时间.
+ if (!forceRefresh && lastSuccessTs > 0 && now - lastSuccessTs < MIN_REFRESH_INTERVAL_MS) {
+ cachedEntries?.let {
+ memoryCache = it
+ Log.i(TAG, "Using cached index (within 1 minutes).")
+ return@withContext it
+ }
+ }
+
+ val withinBackoffWindow = !forceRefresh && isWithinBackoffWindow(now, lastFailureTs, retryAttempts)
+ if (withinBackoffWindow) {
+ cachedEntries?.let { fromCache ->
+ memoryCache = fromCache
+ Log.i(TAG, "Within backoff window, skip network and use cache.")
+ return@withContext fromCache
+ }
+ }
+
+ val etag = sp.getString(KEY_ETAG, null)
+ val netResult = runCatching { fetchFromNetwork(context, etag) }
+
+ when (val r = netResult.getOrNull()) {
+ is NetResult.NotModified -> {
+ resetFailureState(sp, now)
+ cachedEntries?.let { fromCache ->
+ memoryCache = fromCache
+ Log.i(TAG, "Index not modified, use cached.")
+ return@withContext fromCache
+ }
+
+ // Server returned 304, but no local cache file exists.
+ // In this case, need to re-initiate a complete request without ETag to fetch the latest index data.
+ // zh-CN:
+ // 服务器返回 304, 但本地无缓存文件.
+ // 此时需要重新发起一次无 ETag 的完整请求, 拉取最新索引数据.
+ Log.w(TAG, "Index not modified, but no cache found. Force fetching fresh data without ETag.")
+ val freshResult = runCatching { fetchFromNetwork(context, null) }
+
+ when (val fr = freshResult.getOrNull()) {
+ is NetResult.Fresh -> {
+ cacheFile.writeText(fr.body)
+ sp.edit {
+ putString(KEY_ETAG, fr.etag)
+ putLong(KEY_LAST_SUCCESS_TS, now)
+ }
+ val parsed = parseIndexJson(fr.body)
+ memoryCache = parsed
+ Log.i(TAG, "Fetched fresh index after missing cache on 304.")
+ return@withContext parsed
+ }
+ is NetResult.NotModified -> {
+ Log.w(TAG, "Got 304 again even without ETag, fall back to empty list.")
+ }
+ null -> {
+ val th = freshResult.exceptionOrNull()
+ Log.w(TAG, "Force fetch after missing cache failed: ${th?.message}")
+ }
+ }
+ }
+ is NetResult.Fresh -> {
+ resetFailureState(sp, now)
+ cacheFile.writeText(r.body)
+ sp.edit {
+ putString(KEY_ETAG, r.etag)
+ putLong(KEY_LAST_SUCCESS_TS, now)
+ }
+ val parsed = parseIndexJson(r.body)
+ memoryCache = parsed
+ Log.i(TAG, "Fetched fresh index from network.")
+ return@withContext parsed
+ }
+ null -> {
+ val throwable = netResult.exceptionOrNull()
+ Log.w(TAG, "fetchOfficialIndex network failed: ${throwable?.message}")
+
+ // Check if it is a "no network connection" type exception,
+ // if so, do not increment the backoff counter.
+ // zh-CN: 判断是否为 "无网络连接" 类型异常, 若是则不累加退避计数.
+ // @formatter:off
+ val isOffline = throwable is UnknownHostException
+ || throwable is ConnectException
+ || throwable is NoRouteToHostException
+ // @formatter:on
+
+ if (!isOffline) {
+ increaseFailureState(sp, now, retryAttempts)
+ } else {
+ Log.i(TAG, "Detected offline state, skip increasing backoff counters.")
+ }
+
+ cachedEntries?.let { fallback ->
+ memoryCache = fallback
+ return@withContext fallback
+ }
+ }
+ }
+ return@withContext emptyList()
+ }
+
+ private sealed interface NetResult {
+ data class Fresh(val body: String, val etag: String?) : NetResult
+ data object NotModified : NetResult
+ }
+
+ private fun fetchFromNetwork(context: Context, etag: String?): NetResult {
+ val conn = (URL(INDEX_URL).openConnection() as HttpURLConnection).apply {
+ connectTimeout = 10_000
+ readTimeout = 15_000
+ requestMethod = "GET"
+ etag?.let { setRequestProperty("If-None-Match", it) }
+ setRequestProperty("Accept", "application/json")
+ setRequestProperty("User-Agent", "${context.getString(R.string.app_name)}/${BuildConfig.VERSION_NAME} (PluginCenter)")
+ }
+ return try {
+ conn.connect()
+ when (val code = conn.responseCode) {
+ HttpURLConnection.HTTP_NOT_MODIFIED -> NetResult.NotModified
+ HttpURLConnection.HTTP_OK -> {
+ val body = conn.inputStream.bufferedReader().use { it.readText() }
+ val newEtag = conn.getHeaderField("ETag")
+ NetResult.Fresh(body, newEtag)
+ }
+ else -> throw IllegalStateException("HTTP $code: ${conn.responseMessage}")
+ }
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ private fun readCacheSafely(file: File): List? =
+ runCatching {
+ if (!file.exists() || !file.canRead()) return null
+ val text = file.readText()
+ parseIndexJson(text)
+ }.getOrNull()
+
+ private fun isWithinBackoffWindow(now: Long, lastFailureTs: Long, attempts: Int): Boolean {
+ if (lastFailureTs <= 0 || attempts <= 0) return false
+
+ // Avoid overflow.
+ // zh-CN: 避免溢出.
+ val exp = min(attempts - 1, 6)
+ val base = MIN_RETRY_INTERVAL_MS
+
+ // Backoff duration = MIN_RETRY_INTERVAL * 2^(attempts-1), capped at MAX_RETRY_INTERVAL.
+ // zh-CN: 退避时长 = MIN_RETRY_INTERVAL * 2^(attempts-1), 最大不超过 MAX_RETRY_INTERVAL.
+ val delay = min(base shl exp, MAX_RETRY_INTERVAL_MS)
+
+ return now - lastFailureTs < delay
+ }
+
+ private fun resetFailureState(sp: android.content.SharedPreferences, now: Long) {
+ sp.edit {
+ putLong(KEY_LAST_FAILURE_TS, 0L)
+ putInt(KEY_RETRY_ATTEMPTS, 0)
+ putLong(KEY_LAST_SUCCESS_TS, now)
+ }
+ }
+
+ private fun increaseFailureState(sp: android.content.SharedPreferences, now: Long, prevAttempts: Int) {
+ val attempts = (prevAttempts + 1).coerceAtMost(10)
+ sp.edit {
+ putLong(KEY_LAST_FAILURE_TS, now)
+ putInt(KEY_RETRY_ATTEMPTS, attempts)
+ }
+ }
+
+ private fun parseIndexJson(json: String): List {
+ val root = JSONObject(json)
+ val list = mutableListOf()
+
+ // The index structure is { "plugins/items": PluginIndexEntry[] }.
+ // zh-CN: 索引结构为 { "plugins/items": PluginIndexEntry[] }.
+ val arr: JSONArray = when {
+ root.has("plugins") -> root.getJSONArray("plugins")
+ root.has("items") -> root.getJSONArray("items")
+ else -> JSONArray().also {
+ Log.w(TAG, "Invalid index JSON: missing plugins/items field.")
+ }
+ }
+
+ for (i in 0 until arr.length()) {
+ val obj = arr.optJSONObject(i) ?: continue
+ val pkg = obj.optString("packageName").takeIf { it.isNotBlank() } ?: continue
+ val title = obj.optString("title", pkg)
+ val desc = obj.optString("description", "")
+ val author = obj.optString("author").takeIf { it.isNotBlank() }
+ val collaborators = obj.optJSONArray("collaborators")?.let { ja ->
+ List(ja.length()) { idx -> ja.optString(idx) }.filter { it.isNotBlank() }
+ } ?: emptyList()
+ val engine = obj.optString("engine").takeIf { it.isNotBlank() }
+ val variant = obj.optString("variant").takeIf { it.isNotBlank() }
+ val engineId = obj.optString("engineId").takeIf { it.isNotBlank() }
+ val versionName = obj.optString("versionName", "0.0.0")
+ val versionCode = obj.optLong("versionCode", -1L).takeIf { it > 0 }
+ val versionDate = obj.optString("versionDate").takeIf { it.isNotBlank() }
+ val apkUrl = obj.optString("apkUrl").takeIf { it.isNotBlank() }
+ val apkSha256 = obj.optString("apkSha256").takeIf { it.isNotBlank() }
+ val apkSize = obj.optLong("apkSizeBytes", -1L).takeIf { it > 0 }
+
+ list += PluginIndexEntry(
+ packageName = pkg,
+ // TODO M2: 若索引提供 iconUrl 再解析为 Uri.
+ iconUrl = null,
+ title = title,
+ description = desc,
+ author = author,
+ collaborators = collaborators,
+ engine = engine,
+ variant = variant,
+ engineId = engineId,
+ versionName = versionName,
+ versionCode = versionCode,
+ versionDate = versionDate,
+ apkUrl = apkUrl,
+ apkSha256 = apkSha256,
+ apkSizeBytes = apkSize,
+ tags = emptyList(),
+ )
+ }
+
+ return list
+ }
+
}
diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncScheduler.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncScheduler.kt
new file mode 100644
index 00000000..1174b006
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncScheduler.kt
@@ -0,0 +1,38 @@
+package org.autojs.autojs.core.plugin.center
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import java.util.concurrent.TimeUnit
+
+object PluginIndexSyncScheduler {
+
+ private const val UNIQUE_WORK_NAME = "plugin_index_auto_sync"
+
+ fun schedulePeriodicSync(context: Context) {
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val request = PeriodicWorkRequestBuilder(
+ repeatInterval = 24,
+ repeatIntervalTimeUnit = TimeUnit.HOURS,
+ ).setConstraints(constraints)
+ .build()
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ UNIQUE_WORK_NAME,
+ ExistingPeriodicWorkPolicy.KEEP,
+ request,
+ )
+ }
+
+ fun cancelPeriodicSync(context: Context) {
+ WorkManager.getInstance(context)
+ .cancelUniqueWork(UNIQUE_WORK_NAME)
+ }
+
+}
diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncWorker.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncWorker.kt
new file mode 100644
index 00000000..a280e1a2
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncWorker.kt
@@ -0,0 +1,29 @@
+package org.autojs.autojs.core.plugin.center
+
+import android.content.Context
+import android.util.Log
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+
+class PluginIndexSyncWorker(
+ appContext: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+
+ private val repo = PluginIndexRepository()
+
+ override suspend fun doWork(): Result =
+ try {
+ repo.fetchOfficialIndex(applicationContext, forceRefresh = true)
+ Log.i(TAG, "Index sync succeeded.")
+ Result.success()
+ } catch (e: Exception) {
+ Log.w(TAG, "Index sync failed: ${e.message}")
+ Result.retry()
+ }
+
+ companion object {
+ private const val TAG = "PluginIndexSyncWorker"
+ }
+
+}
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 b3181246..92c47e56 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
@@ -43,8 +43,8 @@ object PluginInfoDialogManager {
val pkg = currentPackageName ?: return
if (!dialog.isShowing) return
val target = allItems.firstOrNull { it.packageName == pkg } ?: return
- dialog.dismiss()
showPluginInfoDialog(context, target)
+ dialog.dismiss()
}
@JvmStatic
@@ -128,7 +128,7 @@ object PluginInfoDialogManager {
}
else -> {
d.dismiss()
- CoroutineScope(Dispatchers.IO).launch {
+ CoroutineScope(Dispatchers.Main).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
@@ -154,7 +154,7 @@ object PluginInfoDialogManager {
}
else -> {
d.dismiss()
- CoroutineScope(Dispatchers.IO).launch {
+ CoroutineScope(Dispatchers.Main).launch {
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
}
}
@@ -315,7 +315,7 @@ object PluginInfoDialogManager {
val version: String
val author: String?
val collaborators: List
- val description: String
+ val description: String?
val packageSize: Long
val apkUrl: String?
val sha256: String?
@@ -328,7 +328,7 @@ object PluginInfoDialogManager {
override val version: String,
override val author: String?,
override val collaborators: List,
- override val description: String,
+ override val description: String?,
override val packageSize: Long,
override val apkUrl: String?,
override val sha256: String?,
@@ -343,7 +343,7 @@ object PluginInfoDialogManager {
override val version: String,
override val author: String?,
override val collaborators: List,
- override val description: String,
+ override val description: String?,
override val packageSize: Long,
override val apkUrl: String?,
override val sha256: String?,
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 fcf911ed..15c47e6c 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
@@ -9,13 +9,16 @@ import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.autojs.autojs.network.download.DownloadManager
import org.autojs.autojs.runtime.api.Mime
import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager
+import org.autojs.autojs.util.ClipboardUtils
import org.autojs.autojs.util.FileUtils
import org.autojs.autojs.util.FileUtils.toCacheFile
import org.autojs.autojs.util.UpdateUtils
+import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R
import java.io.EOFException
import java.io.File
@@ -83,27 +86,20 @@ object PluginInstaller {
}
suspend fun installFromUrlWithPrompt(context: Context, url: String, expectedSha256: String? = null) {
- 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
- }
+ when (val result = downloadWithProgress(context, url, expectedSha256)) {
+ is DownloadResult.Success -> {
+ installFromFileUriWithPrompt(context, result.uri)
+ }
+ is DownloadResult.Cancelled -> {
+ // User cancelled, no need to prompt.
+ // zh-CN: 用户取消, 无需提示.
+ }
+ is DownloadResult.Failure -> {
+ // Wait for user action in the failure dialog (retry/quit).
+ // zh-CN: 等待用户在失败对话框中的操作 (重试/放弃).
+ val wantRetry = showFailureDialogAndAwaitDecision(context, result)
+ if (wantRetry) {
+ installFromUrlWithPrompt(context, url, expectedSha256)
}
}
}
@@ -114,46 +110,21 @@ object PluginInstaller {
is DownloadResult.Success -> {
installFromFileUri(context, result.uri)
}
+ is DownloadResult.Cancelled -> {
+ // User cancelled, no need to prompt.
+ // zh-CN: 用户取消, 无需提示.
+ }
is DownloadResult.Failure -> {
- MaterialDialog.Builder(context)
- .title(result.titleRes)
- .content(result.message)
- .positiveText(R.string.dialog_button_dismiss)
- .show()
+ // Wait for user action in the failure dialog (retry/quit).
+ // zh-CN: 等待用户在失败对话框中的操作 (重试/放弃).
+ val wantRetry = showFailureDialogAndAwaitDecision(context, result)
+ if (wantRetry) {
+ installFromUrl(context, url, expectedSha256)
+ }
}
- 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) {
@@ -322,6 +293,48 @@ object PluginInstaller {
return if (last.endsWith(".apk", ignoreCase = true)) last else "$last.apk"
}
+ private suspend fun showFailureDialogAndAwaitDecision(
+ context: Context,
+ result: DownloadResult.Failure,
+ ): Boolean = withContext(Dispatchers.Main) {
+ suspendCancellableCoroutine { cont ->
+ var resumed = false
+
+ fun tryResume(value: Boolean) {
+ if (resumed) return
+ resumed = true
+ if (cont.isActive) {
+ cont.resume(value) { _, _, _ -> }
+ }
+ }
+
+ MaterialDialog.Builder(context)
+ .title(result.titleRes)
+ .content(result.message)
+ .neutralText(R.string.text_copy_all)
+ .neutralColorRes(R.color.dialog_button_hint)
+ .onNeutral { d, _ ->
+ ClipboardUtils.setClip(context, result.message)
+ ViewUtils.showSnack(d.view, R.string.text_already_copied_to_clip, false)
+ }
+ .negativeText(R.string.dialog_button_quit)
+ .negativeColorRes(R.color.dialog_button_default)
+ .onNegative { d, _ ->
+ d.dismiss()
+ tryResume(false)
+ }
+ .positiveText(R.string.dialog_button_retry)
+ .positiveColorRes(R.color.dialog_button_attraction)
+ .onPositive { d, _ ->
+ d.dismiss()
+ tryResume(true)
+ }
+ .cancelable(false)
+ .autoDismiss(false)
+ .show()
+ }
+ }
+
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")
diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt b/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt
index e4fd360c..b71c49ad 100644
--- a/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt
+++ b/app/src/main/java/org/autojs/autojs/core/plugin/ocr/PaddleOcrPluginHost.kt
@@ -182,7 +182,7 @@ object PaddleOcrPluginHost {
cont.resumeWith(
Result.failure(
IllegalStateException(
- "bindService SecurityException: $cn. Please make sure the plugin declares and the Service uses this permission.", se
+ "bindService SecurityException: $cn. Please make sure the plugin declares and the Service uses this permission.", se
)
)
)
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/global/Global.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/global/Global.kt
index e7594b53..4ff510f7 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/global/Global.kt
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/global/Global.kt
@@ -500,22 +500,22 @@ class Global(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRunti
when (version) {
is Number -> coerceNumber(version, 0).let { num ->
when {
- num.toInt() == 6 -> {
+ num.toInt() in 2..9 -> {
requiresAutojsVersion(scriptRuntime, arrayOf(num.toString()))
}
else -> {
require(num.toInt() >= 461) {
- "指定的 AutoJs6 应用版本号需大于 461"
+ globalContext.getString(R.string.error_specified_autojs6_version_number_must_be_greater_than_461)
}
require(BuildConfig.VERSION_CODE >= num.toInt()) {
- "AutoJs6 应用版本号需不低于 ${num.jsString}"
+ globalContext.getString(R.string.error_autojs6_version_number_must_not_be_lower_than_num, num.jsString)
}
}
}
}
else -> coerceString(version).let { ver ->
require(Version(BuildConfig.VERSION_NAME).isAtLeast(Version(ver))) {
- "AutoJs6 应用版本需不低于 $ver"
+ globalContext.getString(R.string.error_autojs6_version_must_not_be_lower_than_ver, ver)
}
}
}
diff --git a/app/src/main/res/layout/fragment_plugin_center.xml b/app/src/main/res/layout/fragment_plugin_center.xml
index 9e16f858..5474b7a0 100644
--- a/app/src/main/res/layout/fragment_plugin_center.xml
+++ b/app/src/main/res/layout/fragment_plugin_center.xml
@@ -1,15 +1,40 @@
-
+
+
-
+
-
\ 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 05dc0375..cf2497e6 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1121,5 +1121,15 @@
فشل التحديث
هل تريد بالتأكيد إلغاء التثبيت؟
تفاصيل
+ فشل تحميل المكونات الإضافية.\nالسبب: %1$s.
+ جارٍ جلب بيانات قائمة المكونات الإضافية...
+ لا توجد حاليًا أي مكونات إضافية مثبتة.\nانقر على زر \"+\" لتثبيت المكونات الإضافية.
+ مؤلف غير معروف
+ وصف غير معروف
+ إصدار غير معروف
+ عنوان غير معروف
+ يجب أن يكون رقم إصدار AutoJs6 المحدد أكبر من 461
+ يجب ألا يكون رقم إصدار AutoJs6 أقل من %1$s
+ يجب ألا يكون إصدار AutoJs6 أقل من %1$s
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index 862d9fe7..6cf72693 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -1116,5 +1116,15 @@
Failed to update
Are you sure to uninstall?
Details
+ Failed to load plugins.\nReason: %1$s.
+ Retrieving plugin list data...
+ No plugins currently installed.\nClick the \"+\" button to install plugins.
+ Unknown Author
+ Unknown Description
+ Unknown Version
+ Unknown Title
+ Specified AutoJs6 version number must be greater than 461
+ AutoJs6 version number must not be lower than %1$s
+ AutoJs6 version must not be lower than %1$s
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 01507ae2..2b825327 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1119,5 +1119,15 @@
Error al actualizar
¿Seguro que deseas desinstalar?
Detalles
+ Error al cargar los complementos.\nMotivo: %1$s.
+ Obteniendo datos de la lista de complementos...
+ Actualmente no hay complementos instalados.\nPulsa el botón \"+\" para instalar complementos.
+ Autor Desconocido
+ Descripción Desconocida
+ Versión Desconocida
+ Título Desconocido
+ El número de versión especificado de AutoJs6 debe ser mayor que 461
+ El número de versión de AutoJs6 no debe ser inferior a %1$s
+ La versión de AutoJs6 no debe ser inferior a %1$s
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index d29afd5b..f253d51f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -1119,5 +1119,15 @@
Échec de la mise à jour
Voulez-vous vraiment désinstaller ?
Détails
+ Échec du chargement des plugins.\nRaison : %1$s.
+ Récupération des données de la liste des plugins...
+ Aucun plugin n\'est actuellement installé.\nCliquez sur le bouton \"+\" pour installer des plugins.
+ Auteur Inconnu
+ Description Inconnue
+ Version Inconnue
+ Titre Inconnu
+ Le numéro de version AutoJs6 spécifié doit être supérieur à 461
+ Le numéro de version d\'AutoJs6 ne doit pas être inférieur à %1$s
+ La version d\'AutoJs6 ne doit pas être inférieure à %1$s
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 2026ecc4..1ba019dd 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1120,5 +1120,15 @@
更新に失敗しました
アンインストールしてもよろしいですか?
詳細
+ プラグインの読み込みに失敗しました.\n理由: %1$s.
+ プラグイン一覧のデータを取得しています...
+ 現在インストールされているプラグインはありません.\n\"+\" ボタンをタップしてプラグインをインストールしてください.
+ 不明な作者
+ 不明な説明
+ 不明なバージョン
+ 不明なタイトル
+ 指定された AutoJs6 のバージョン番号は 461 より大きくなければなりません
+ AutoJs6 のバージョン番号は %1$s 未満であってはなりません
+ AutoJs6 のバージョンは %1$s 未満であってはなりません
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index d1caeba1..6cf0cb56 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -1121,5 +1121,15 @@
업데이트 실패
정말 제거하시겠습니까?
자세히
+ 플러그인을 불러오지 못했습니다.\n사유: %1$s.
+ 플러그인 목록 데이터를 가져오는 중입니다...
+ 현재 설치된 플러그인이 없습니다.\n\"+\" 버튼을 눌러 플러그인을 설치하세요.
+ 알 수 없는 작성자
+ 알 수 없는 설명
+ 알 수 없는 버전
+ 알 수 없는 제목
+ 지정된 AutoJs6 버전 번호는 461보다 커야 합니다
+ AutoJs6 버전 번호는 %1$s보다 낮아서는 안 됩니다
+ AutoJs6 버전은 %1$s보다 낮아서는 안 됩니다
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index b825009f..de8912ad 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1119,5 +1119,15 @@
Не удалось обновить
Вы действительно хотите удалить?
Детали
+ Не удалось загрузить плагины.\nПричина: %1$s.
+ Получение данных списка плагинов...
+ В настоящее время плагины не установлены.\nНажмите кнопку \"+\" чтобы установить плагины.
+ Неизвестный Автор
+ Неизвестное Описание
+ Неизвестная Версия
+ Неизвестный Заголовок
+ Указанный номер версии AutoJs6 должен быть больше 461
+ Номер версии AutoJs6 не должен быть ниже %1$s
+ Версия AutoJs6 не должна быть ниже %1$s
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 6f6cb15d..4611e840 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -1117,5 +1117,15 @@
更新失敗
是否確定卸載
異常詳情
+ 插件讀取失敗.\n原因: %1$s.
+ 正在獲取插件列表數據...
+ 當前沒有已安裝的插件.\n可點擊 \"+\" 按鈕安裝插件.
+ 未知開發者
+ 未知描述
+ 未知版本
+ 未知標題
+ 指定的 AutoJs6 應用版本號需大於 461
+ AutoJs6 應用版本號需不低於 %1$s
+ AutoJs6 應用版本需不低於 %1$s
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index d90d8148..b7e423d8 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -1117,5 +1117,15 @@
更新失敗
是否確定解除安裝
異常詳情
+ 外掛讀取失敗.\n原因: %1$s.
+ 正在獲取外掛列表資料...
+ 當前沒有已安裝的外掛.\n可點選 \"+\" 按鈕安裝外掛.
+ 未知開發者
+ 未知描述
+ 未知版本
+ 未知標題
+ 指定的 AutoJs6 應用版本號需大於 461
+ AutoJs6 應用版本號需不低於 %1$s
+ AutoJs6 應用版本需不低於 %1$s
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 1c4f2b88..86a7f5fd 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -1117,5 +1117,15 @@
更新失败
是否确定卸载
异常详情
+ 插件读取失败.\n原因: %1$s.
+ 正在获取插件列表数据...
+ 当前没有已安装的插件.\n可点击 \"+\" 按钮安装插件.
+ 未知开发者
+ 未知描述
+ 未知版本
+ 未知标题
+ 指定的 AutoJs6 应用版本号需大于 461
+ AutoJs6 应用版本号需不低于 %1$s
+ AutoJs6 应用版本需不低于 %1$s
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 92bc0067..ed7805db 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1372,5 +1372,15 @@
Failed to update
Are you sure to uninstall?
Details
+ Failed to load plugins.\nReason: %1$s.
+ Retrieving plugin list data...
+ No plugins currently installed.\nClick the \"+\" button to install plugins.
+ Unknown Author
+ Unknown Description
+ Unknown Version
+ Unknown Title
+ Specified AutoJs6 version number must be greater than 461
+ AutoJs6 version number must not be lower than %1$s
+ AutoJs6 version must not be lower than %1$s
diff --git a/version.properties b/version.properties
index 63465c60..4d0adcc3 100644
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
-#Tue Dec 09 12:09:37 CST 2025
-BUILD_TIME=1765253377144
+#Thu Dec 11 10:33:50 CST 2025
+BUILD_TIME=1765420430709
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=3530
+VERSION_BUILD=3539
VERSION_NAME=6.7.0 Alpha12
VSCODE_EXT_REQUIRED_VERSION=1.0.8