From 9864d2010c7d4ba5d2ce443e7138ec4bc3579bc0 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Fri, 12 Dec 2025 11:58:01 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha12=20-=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20M2=20-=20=E6=8F=92=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E6=9B=B4=E6=96=B0=E6=94=AF=E6=8C=81=20ETag/=E7=BC=93?= =?UTF-8?q?=E5=AD=98/=E9=80=80=E9=81=BF;=20WorkManager=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=A3=80=E6=9F=A5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 10 +- .../plugin/center/PluginCenterFragment.kt | 128 ++++++-- .../core/plugin/center/PluginCenterItem.kt | 17 +- .../center/PluginCenterItemViewHolder.kt | 12 +- .../plugin/center/PluginCenterViewModel.kt | 123 +++++-- .../plugin/center/PluginIndexRepository.kt | 301 ++++++++++++++++-- .../plugin/center/PluginIndexSyncScheduler.kt | 38 +++ .../plugin/center/PluginIndexSyncWorker.kt | 29 ++ .../plugin/center/PluginInfoDialogManager.kt | 12 +- .../core/plugin/center/PluginInstaller.kt | 125 ++++---- .../core/plugin/ocr/PaddleOcrPluginHost.kt | 2 +- .../runtime/api/augment/global/Global.kt | 8 +- .../res/layout/fragment_plugin_center.xml | 45 ++- app/src/main/res/values-ar/strings.xml | 10 + app/src/main/res/values-en/strings.xml | 10 + app/src/main/res/values-es/strings.xml | 10 + app/src/main/res/values-fr/strings.xml | 10 + app/src/main/res/values-ja/strings.xml | 10 + app/src/main/res/values-ko/strings.xml | 10 + app/src/main/res/values-ru/strings.xml | 10 + app/src/main/res/values-zh-rHK/strings.xml | 10 + app/src/main/res/values-zh-rTW/strings.xml | 10 + app/src/main/res/values-zh/strings.xml | 10 + app/src/main/res/values/strings.xml | 10 + version.properties | 6 +- 25 files changed, 802 insertions(+), 164 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncScheduler.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginIndexSyncWorker.kt 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