From 238784faa096aa597ee172c0cef5ff9d3d3fe17b Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sun, 1 Mar 2026 19:41:12 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha22=20-=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20M3=20-=20=E6=8F=92=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E6=94=AF=E6=8C=81=E4=BC=A0=E7=BB=9F=20SDK=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8F=91=E7=8E=B0=E5=B9=B6=E5=A2=9E=E5=8A=A0=E4=BF=A1?= =?UTF-8?q?=E4=BB=BB=E6=9C=BA=E5=88=B6=E5=92=8C=E6=8E=88=E6=9D=83=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../center/InstalledPluginRepository.kt | 2 +- .../center/LegacyInstalledPluginRepository.kt | 87 +++++ .../plugin/center/PluginActivationStore.kt | 22 ++ .../plugin/center/PluginAuthorizationStore.kt | 29 ++ .../plugin/center/PluginCenterActivity.kt | 60 ++-- .../plugin/center/PluginCenterFragment.kt | 297 +++++++++++++----- .../core/plugin/center/PluginCenterItem.kt | 8 + .../center/PluginCenterItemViewHolder.kt | 17 +- .../core/plugin/center/PluginCenterState.kt | 49 +++ .../plugin/center/PluginCenterViewModel.kt | 145 ++++++++- .../center/PluginDescriptionResolver.kt | 27 ++ .../core/plugin/center/PluginErrorMapper.kt | 38 +++ .../core/plugin/center/PluginFilterStore.kt | 43 +++ .../plugin/center/PluginInfoDialogManager.kt | 82 ++++- .../plugin/center/PluginSignatureUtils.kt | 42 +++ .../core/plugin/center/PluginSortStore.kt | 43 +++ .../core/plugin/center/PluginTrustManager.kt | 104 ++++++ .../core/plugin/ocr/PaddleOcrPluginHost.kt | 11 + .../org/autojs/autojs/runtime/api/Plugins.kt | 16 +- .../plugin_center_recycler_view_item.xml | 44 +-- .../res/layout/plugin_info_dialog_items.xml | 116 ++++++- app/src/main/res/values-ar/strings.xml | 15 + app/src/main/res/values-en/strings.xml | 15 + app/src/main/res/values-es/strings.xml | 15 + app/src/main/res/values-fr/strings.xml | 15 + app/src/main/res/values-ja/strings.xml | 15 + app/src/main/res/values-ko/strings.xml | 15 + app/src/main/res/values-ru/strings.xml | 15 + app/src/main/res/values-zh-rHK/strings.xml | 15 + app/src/main/res/values-zh-rTW/strings.xml | 15 + app/src/main/res/values-zh/strings.xml | 15 + app/src/main/res/values/strings.xml | 21 +- version.properties | 8 +- 33 files changed, 1290 insertions(+), 171 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/LegacyInstalledPluginRepository.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginActivationStore.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginAuthorizationStore.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterState.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginDescriptionResolver.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginErrorMapper.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginFilterStore.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSignatureUtils.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSortStore.kt create mode 100644 app/src/main/java/org/autojs/autojs/core/plugin/center/PluginTrustManager.kt diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt index 489ae061..7e036a92 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/InstalledPluginRepository.kt @@ -68,7 +68,7 @@ class InstalledPluginRepository { InstalledPlugin( packageName = packageName, title = d.pluginInfo?.name ?: appLabel ?: packageName, - description = d.pluginInfo?.description, + description = PluginDescriptionResolver.resolve(context, packageName, d.pluginInfo?.description), author = d.pluginInfo?.author, versionName = versionName, versionCode = versionCode, diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/LegacyInstalledPluginRepository.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/LegacyInstalledPluginRepository.kt new file mode 100644 index 00000000..8bd7a28a --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/LegacyInstalledPluginRepository.kt @@ -0,0 +1,87 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.graphics.drawable.Drawable +import android.os.Build +import androidx.core.content.pm.PackageInfoCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.autojs.autojs6.R +import java.io.File + +class LegacyInstalledPluginRepository { + + data class LegacyInstalledPlugin( + val packageName: String, + val title: String, + val description: String?, + val author: String?, + val versionName: String, + val versionCode: Long?, + val packageSize: Long, + val firstInstallTime: Long?, + val lastUpdateTime: Long?, + val icon: Drawable?, + val registryClass: String?, + val isStopped: Boolean, + ) + + @Suppress("DEPRECATION") + suspend fun discoverInstalled(context: Context): List = withContext(Dispatchers.IO) { + val pm = context.packageManager + val flags = PackageManager.GET_META_DATA + val apps = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getInstalledApplications(ApplicationInfoFlags.of(flags.toLong())) + } else { + pm.getInstalledApplications(flags) + } + apps.mapNotNull { appInfo -> + val meta = appInfo.metaData ?: return@mapNotNull null + val registry = meta.getString(KEY_REGISTRY) ?: return@mapNotNull null + val packageName = appInfo.packageName + val label = appInfo.loadLabel(pm)?.toString() + val icon = appInfo.loadIcon(pm) + val isStopped = (appInfo.flags and ApplicationInfo.FLAG_STOPPED) != 0 + + val pkgInfo = runCatching { pm.getPackageInfo(packageName, 0) }.getOrNull() + val versionName = pkgInfo?.versionName ?: context.getString(R.string.text_unknown) + val versionCode = pkgInfo?.let { PackageInfoCompat.getLongVersionCode(it) } + val firstInstallTime = pkgInfo?.firstInstallTime + val lastUpdateTime = pkgInfo?.lastUpdateTime + val packageSize = calcPackageSize(appInfo) + + LegacyInstalledPlugin( + packageName = packageName, + title = label ?: packageName, + description = null, + author = null, + versionName = versionName, + versionCode = versionCode, + packageSize = packageSize, + firstInstallTime = firstInstallTime, + lastUpdateTime = lastUpdateTime, + icon = icon, + registryClass = registry, + isStopped = isStopped, + ) + } + } + + private fun calcPackageSize(appInfo: ApplicationInfo?): Long { + val baseApkSize = appInfo?.publicSourceDir?.let { File(it).length() } + ?: appInfo?.sourceDir?.let { File(it).length() } + val splitApkTotalSize = when { + appInfo?.splitPublicSourceDirs != null -> appInfo.splitPublicSourceDirs!!.sumOf { File(it).length() } + appInfo?.splitSourceDirs != null -> appInfo.splitSourceDirs!!.sumOf { File(it).length() } + else -> 0L + } + return baseApkSize?.let { it + splitApkTotalSize } ?: 0L + } + + companion object { + private const val KEY_REGISTRY = "org.autojs.plugin.sdk.registry" + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginActivationStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginActivationStore.kt new file mode 100644 index 00000000..7a11299f --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginActivationStore.kt @@ -0,0 +1,22 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import androidx.core.content.edit + +object PluginActivationStore { + + private const val SP_NAME = "plugin_center_activation" + + fun markActivated(context: Context, pluginId: String, timeMillis: Long = System.currentTimeMillis()) { + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + sp.edit { putLong(key(pluginId), timeMillis) } + } + + fun getLastActivatedAt(context: Context, pluginId: String): Long? { + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + val value = sp.getLong(key(pluginId), -1L) + return value.takeIf { it > 0L } + } + + private fun key(pluginId: String) = "key_\$_plugin_activated_\$_$pluginId" +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginAuthorizationStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginAuthorizationStore.kt new file mode 100644 index 00000000..946499ff --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginAuthorizationStore.kt @@ -0,0 +1,29 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import androidx.core.content.edit + +object PluginAuthorizationStore { + + private const val SP_NAME = "plugin_center_authorization" + + fun isGranted(context: Context, packageName: String, fingerprints: List): Boolean { + if (fingerprints.isEmpty()) return false + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + return fingerprints.any { fp -> sp.getBoolean(key(packageName, fp), false) } + } + + fun grant(context: Context, packageName: String, fingerprint: String?) { + fingerprint ?: return + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + sp.edit { putBoolean(key(packageName, fingerprint), true) } + } + + fun revoke(context: Context, packageName: String, fingerprint: String?) { + fingerprint ?: return + val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + sp.edit { remove(key(packageName, fingerprint)) } + } + + private fun key(packageName: String, fingerprint: String) = "key_\$_plugin_auth_\$_$packageName\$_$fingerprint" +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt index 30ac43d4..d2b640cb 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt @@ -13,8 +13,8 @@ import com.afollestad.materialdialogs.MaterialDialog import kotlinx.coroutines.launch import org.autojs.autojs.ui.BaseActivity import org.autojs.autojs.ui.widget.SearchViewItem -import org.autojs.autojs.util.IntentUtils.startSafely import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.IntentUtils.startSafely import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils.onceGlobalLayout import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance @@ -118,60 +118,46 @@ class PluginCenterActivity : BaseActivity() { private fun showSortDialog(center: PluginCenterFragment?) { if (center == null) return + val entries = PluginCenterFragment.Sort.values() + MaterialDialog.Builder(this) .title(R.string.text_sort) - .items( - listOf( - getString(R.string.text_sort_by_name), - getString(R.string.text_sort_by_last_update_time), - getString(R.string.text_sort_by_package_size), - ) - ) - .itemsCallback { d, _, which, _ -> + .items(entries.map { getString(it.titleRes) }) + .itemsCallbackSingleChoice(PluginSortStore.getSortOrdinal(this)) { d, _, which, _ -> d.dismiss() - when (which) { - 0 -> center.setSort(PluginCenterFragment.Sort.TITLE_ASC) - 1 -> center.setSort(PluginCenterFragment.Sort.LAST_UPDATE_DESC) - 2 -> center.setSort(PluginCenterFragment.Sort.PACKAGE_SIZE_DESC) - else -> Unit - } + val selectedSort = entries[which] + // center.setSort(selectedSort) + PluginSortStore.setSort(this, selectedSort) + true } .choiceWidgetThemeColor() - .negativeText(R.string.text_cancel) + .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_attraction) .show() } private fun showFilterDialog(center: PluginCenterFragment?) { if (center == null) return + val entries = PluginCenterFragment.Filter.values() + MaterialDialog.Builder(this) .title(R.string.text_filter) - .items( - listOf( - getString(R.string.text_all), - getString(R.string.text_installed), - getString(R.string.text_not_installed), - getString(R.string.text_enabled), - getString(R.string.text_disabled), - getString(R.string.text_updatable), - ) - ) - .itemsCallback { d, _, which, _ -> + .items(entries.map { getString(it.titleRes) }) + .itemsCallbackSingleChoice(PluginFilterStore.getFilterOrdinal(this)) { d, _, which, _ -> d.dismiss() - when (which) { - 0 -> center.setFilter(PluginCenterFragment.Filter.ALL) - 1 -> center.setFilter(PluginCenterFragment.Filter.INSTALLED) - 2 -> center.setFilter(PluginCenterFragment.Filter.NOT_INSTALLED) - 3 -> center.setFilter(PluginCenterFragment.Filter.ENABLED) - 4 -> center.setFilter(PluginCenterFragment.Filter.DISABLED) - 5 -> center.setFilter(PluginCenterFragment.Filter.UPDATABLE) - else -> Unit - } + val selectedFilter = entries[which] + // center.setFilter(selectedFilter) + PluginFilterStore.setFilter(this, selectedFilter) + true } .choiceWidgetThemeColor() - .negativeText(R.string.text_cancel) + .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_attraction) .show() } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterFragment.kt index 5a652466..86d45368 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,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.core.net.toUri @@ -56,8 +57,24 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { // zh-CN: 当前用于 UI 过滤的查询串, null 表示 "不做过滤". private var currentQuery: String? = null - private var currentSort: Sort = Sort.TITLE_ASC - private var currentFilter: Filter = Filter.ALL + private lateinit var currentSort: Sort + private lateinit var currentFilter: Filter + + private val sortPrefListener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + if (!isAdded || _binding == null) return@OnSharedPreferenceChangeListener + val ctx = contextRef + viewLifecycleOwner.lifecycleScope.launch { + setSort(PluginSortStore.getSort(ctx)) + } + } + + private val filterPrefListener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + if (!isAdded || _binding == null) return@OnSharedPreferenceChangeListener + val ctx = contextRef + viewLifecycleOwner.lifecycleScope.launch { + setFilter(PluginFilterStore.getFilter(ctx)) + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -65,75 +82,34 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { val context = requireContext().also { contextRef = it } + currentSort = PluginSortStore.getSort(context) + currentFilter = PluginFilterStore.getFilter(context) + adapter = PluginCenterItemAdapter(object : PluginCenterItemAdapter.Listener { override fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) { if (!enabled) { vm.setEnabled(contextRef, item.packageName, false) item.isEnabled = false + item.enabledState = PluginEnabledState.DISABLED + item.lastError = null + adapter.notifyDataSetChanged() return } - vm.setEnabled(contextRef, item.packageName, true) - item.isEnabled = true + if (!item.isInstalled) { + vm.setEnabled(contextRef, item.packageName, false) + item.isEnabled = false + item.enabledState = PluginEnabledState.DISABLED + item.lastError = null + adapter.notifyDataSetChanged() + ViewUtils.showToast(contextRef, getString(R.string.text_unavailable), true) + return + } - viewLifecycleOwner.lifecycleScope.launch { - val error = runCatching { - PaddleOcrPluginHost.probe(contextRef, item.packageName) - }.exceptionOrNull() - if (error != null && error !is CancellationException) { - if (isAdded) { - val pm = contextRef.packageManager - val launchIntent = pm.getLaunchIntentForPackage(item.packageName) - val wakeIntent = PluginWakeManager.buildWakeIntent(contextRef, item.packageName) - - val errorBody = error.message ?: error.toString() - val message = listOf( - contextRef.getString(R.string.text_exception_info) + contextRef.getString(R.string.symbol_colon_with_blank), - errorBody, - contextRef.getString(R.string.text_hint) + contextRef.getString(R.string.symbol_colon_with_blank), - getString(R.string.hint_try_clicking_the_activate_button_to_activate_the_plugin) - ).joinToString("\n\n") - - MaterialDialog.Builder(contextRef) - .title(R.string.error_failed_to_enable_the_plugin) - .content(message) - .neutralText(R.string.dialog_button_copy) - .neutralColorRes(R.color.dialog_button_hint) - .onNeutral { d, _ -> - ClipboardUtils.setClip(contextRef, errorBody) - ViewUtils.showSnack(d.view, R.string.text_already_copied_to_clip, false) - } - .negativeText(R.string.dialog_button_dismiss) - .negativeColorRes(R.color.dialog_button_default) - .onNegative { d, _ -> d.dismiss() } - .apply positive@{ - positiveText(R.string.dialog_button_activate) - - val openIntent = wakeIntent ?: launchIntent ?: run { - positiveColorRes(R.color.dialog_button_unavailable) - onPositive { d, _ -> - ViewUtils.showSnack(d.view, R.string.text_unavailable, false) - } - return@positive - } - - positiveColorRes(R.color.dialog_button_attraction) - onPositive { d, _ -> - val started = openIntent.startSafely(contextRef, true) - d.dismiss() - if (started) { - ViewUtils.showToast(contextRef, getString(R.string.text_activated_successfully), true) - tryEnableAfterWake(item) - } - } - } - .cancelable(false) - .autoDismiss(false) - .show() - } - vm.setEnabled(contextRef, item.packageName, false) - item.isEnabled = false - adapter.notifyDataSetChanged() + ensureAuthorized(item) { authorizedItem -> + when (authorizedItem.mechanism) { + PluginMechanism.SDK -> enableLegacyPlugin(authorizedItem) + PluginMechanism.AIDL -> enableAidlPlugin(authorizedItem) } } } @@ -269,6 +245,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { Filter.ENABLED -> item.isEnabled Filter.DISABLED -> !item.isEnabled Filter.UPDATABLE -> item.updatableVersionCode != null + Filter.AIDL -> item.mechanism == PluginMechanism.AIDL + Filter.SDK -> item.mechanism == PluginMechanism.SDK } } @@ -364,6 +342,161 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { } } + private fun ensureAuthorized(item: PluginCenterItem, onAuthorized: (PluginCenterItem) -> Unit) { + if (item.authorizedState != PluginAuthorizedState.REQUIRED) { + onAuthorized(item) + return + } + + val authError = PluginError(PluginErrorCode.NOT_AUTHORIZED) + vm.setEnabled(contextRef, item.packageName, false, authError) + item.isEnabled = false + item.enabledState = PluginEnabledState.DISABLED + item.lastError = authError + adapter.notifyDataSetChanged() + + val fingerprint = item.signingFingerprintSha256 + if (fingerprint.isNullOrBlank()) { + ViewUtils.showToast(contextRef, getString(R.string.text_unavailable), true) + vm.setEnabled(contextRef, item.packageName, false, authError) + item.isEnabled = false + item.enabledState = PluginEnabledState.DISABLED + item.lastError = authError + adapter.notifyDataSetChanged() + return + } + + MaterialDialog.Builder(contextRef) + .title(R.string.text_authorize_plugin) + .content(R.string.text_authorize_plugin_content) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_authorize) + .positiveColorRes(R.color.dialog_button_attraction) + .onPositive { d, _ -> + PluginAuthorizationStore.grant(contextRef, item.packageName, fingerprint) + item.authorizedState = PluginAuthorizedState.USER_GRANTED + item.lastError = null + d.dismiss() + adapter.notifyDataSetChanged() + onAuthorized(item) + } + .onNegative { d, _ -> d.dismiss() } + .cancelable(true) + .show() + } + + private fun enableLegacyPlugin(item: PluginCenterItem) { + vm.setEnabled(contextRef, item.packageName, true) + item.isEnabled = true + item.enabledState = PluginEnabledState.READY + item.lastError = null + adapter.notifyDataSetChanged() + } + + private fun enableAidlPlugin(item: PluginCenterItem) { + vm.setEnabled(contextRef, item.packageName, true) + item.isEnabled = true + item.enabledState = PluginEnabledState.READY + item.lastError = null + adapter.notifyDataSetChanged() + + viewLifecycleOwner.lifecycleScope.launch { + val error = runCatching { + PaddleOcrPluginHost.probe(contextRef, item.packageName) + }.exceptionOrNull() + if (error != null && error !is CancellationException) { + val mapped = PluginErrorMapper.fromThrowable(error) + val shouldRecommend = item.canActivate && PluginErrorMapper.shouldRecommendActivation(mapped) + if (item.activatedState == PluginActivatedState.UNKNOWN && shouldRecommend) { + item.activatedState = PluginActivatedState.RECOMMENDED + } + val finalError = if (shouldRecommend) { + mapped.copy( + code = PluginErrorCode.ROM_FIRST_RUN_RESTRICTED_SUSPECTED, + recoverHint = getString(R.string.hint_try_clicking_the_activate_button_to_activate_the_plugin), + ) + } else mapped + showEnableErrorDialog(item, finalError, error) + vm.setEnabled(contextRef, item.packageName, false, finalError) + item.isEnabled = false + item.enabledState = PluginEnabledState.ERROR(finalError) + item.lastError = finalError + adapter.notifyDataSetChanged() + return@launch + } + + vm.setEnabled(contextRef, item.packageName, true) + item.isEnabled = true + item.enabledState = PluginEnabledState.READY + item.lastError = null + adapter.notifyDataSetChanged() + } + } + + private fun showEnableErrorDialog(item: PluginCenterItem, mapped: PluginError, raw: Throwable) { + if (!isAdded) return + + val pm = contextRef.packageManager + val launchIntent = pm.getLaunchIntentForPackage(item.packageName) + val wakeIntent = if (item.canActivate) PluginWakeManager.buildWakeIntent(contextRef, item.packageName) else null + + val errorBody = raw.message ?: raw.toString() + val messageParts = mutableListOf( + contextRef.getString(R.string.text_exception_info) + contextRef.getString(R.string.symbol_colon_with_blank), + errorBody, + ) + val hintText = mapped.recoverHint?.takeIf { it.isNotBlank() } + ?: if (item.canActivate && PluginErrorMapper.shouldRecommendActivation(mapped)) { + getString(R.string.hint_try_clicking_the_activate_button_to_activate_the_plugin) + } else null + if (!hintText.isNullOrBlank()) { + messageParts += contextRef.getString(R.string.text_hint) + contextRef.getString(R.string.symbol_colon_with_blank) + messageParts += hintText + } + val message = messageParts.joinToString("\n\n") + + MaterialDialog.Builder(contextRef) + .title(R.string.error_failed_to_enable_the_plugin) + .content(message) + .neutralText(R.string.dialog_button_copy) + .neutralColorRes(R.color.dialog_button_hint) + .onNeutral { d, _ -> + ClipboardUtils.setClip(contextRef, errorBody) + ViewUtils.showSnack(d.view, R.string.text_already_copied_to_clip, false) + } + .negativeText(R.string.dialog_button_dismiss) + .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> d.dismiss() } + .apply positive@{ + positiveText(R.string.dialog_button_activate) + + val openIntent = wakeIntent ?: launchIntent ?: run { + positiveColorRes(R.color.dialog_button_unavailable) + onPositive { d, _ -> + ViewUtils.showSnack(d.view, R.string.text_unavailable, false) + } + return@positive + } + + positiveColorRes(R.color.dialog_button_attraction) + onPositive { d, _ -> + val started = openIntent.startSafely(contextRef, true) + d.dismiss() + if (started) { + PluginActivationStore.markActivated(contextRef, item.packageName) + item.activatedState = PluginActivatedState.DONE + ViewUtils.showToast(contextRef, getString(R.string.text_activated_successfully), true) + adapter.notifyDataSetChanged() + tryEnableAfterWake(item) + } + } + } + .cancelable(false) + .autoDismiss(false) + .show() + } + private fun tryEnableAfterWake(item: PluginCenterItem) { viewLifecycleOwner.lifecycleScope.launch { val delays = longArrayOf(300L, 800L, 1500L) @@ -376,6 +509,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { if (isAdded && _binding != null) { vm.setEnabled(contextRef, item.packageName, true) item.isEnabled = true + item.enabledState = PluginEnabledState.READY + item.lastError = null adapter.notifyDataSetChanged() } return@launch @@ -389,6 +524,16 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { override fun onStart() { super.onStart() + if (::contextRef.isInitialized) { + PluginSortStore.registerOnSharedPreferenceChangeListener(contextRef, sortPrefListener) + PluginFilterStore.registerOnSharedPreferenceChangeListener(contextRef, filterPrefListener) + + // Sync with latest persisted state (e.g., changed while fragment was stopped). + // zh-CN: 同步最新的持久化状态 (如在 Fragment 停止期间被修改). + setSort(PluginSortStore.getSort(contextRef)) + setFilter(PluginFilterStore.getFilter(contextRef)) + } + pkgReceiver ?: run registerPackageReceiver@{ pkgReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -449,6 +594,10 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { override fun onStop() { super.onStop() + if (::contextRef.isInitialized) { + PluginSortStore.unregisterOnSharedPreferenceChangeListener(contextRef, sortPrefListener) + PluginFilterStore.unregisterOnSharedPreferenceChangeListener(contextRef, filterPrefListener) + } pkgChangeRefreshJob?.cancel() pkgChangeRefreshJob = null pkgReceiver?.let { runCatching { requireContext().unregisterReceiver(it) } } @@ -475,21 +624,23 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) { // Sort strategy for rendering list. // zh-CN: 用于渲染列表的排序策略. - enum class Sort { - TITLE_ASC, - LAST_UPDATE_DESC, - PACKAGE_SIZE_DESC, + enum class Sort(val titleRes: Int) { + TITLE_ASC(R.string.text_sort_by_name), + LAST_UPDATE_DESC(R.string.text_sort_by_last_update_time), + PACKAGE_SIZE_DESC(R.string.text_sort_by_package_size), } // Filter strategy for rendering list. // zh-CN: 用于渲染列表的筛选策略. - enum class Filter { - ALL, - INSTALLED, - NOT_INSTALLED, - ENABLED, - DISABLED, - UPDATABLE, + enum class Filter(val titleRes: Int) { + ALL(R.string.text_all), + INSTALLED(R.string.text_installed), + NOT_INSTALLED(R.string.text_not_installed), + ENABLED(R.string.text_enabled), + DISABLED(R.string.text_disabled), + UPDATABLE(R.string.text_updatable), + AIDL(R.string.text_plugin_mechanism_aidl), + SDK(R.string.text_plugin_mechanism_sdk), } } 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 a59b9e3f..9f01c254 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 @@ -47,6 +47,14 @@ data class PluginCenterItem( val firstInstallTime: Long? = null, val lastUpdateTime: Long? = null, val settings: PluginCenterItemSettings? = null, + val mechanism: PluginMechanism = PluginMechanism.AIDL, + var authorizedState: PluginAuthorizedState = PluginAuthorizedState.OFFICIAL, + var activatedState: PluginActivatedState = PluginActivatedState.NOT_SUPPORTED, + var enabledState: PluginEnabledState = PluginEnabledState.READY, + var lastError: PluginError? = null, + var signingFingerprintSha256: String? = null, + var isOfficialVerified: Boolean = false, + var canActivate: Boolean = false, ) { val versionSummary: String get() = formatVersionInfo(versionName, versionCode, versionDate) 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 ed89c244..2352569c 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 @@ -14,13 +14,14 @@ import androidx.recyclerview.widget.RecyclerView import de.hdodenhof.circleimageview.CircleImageView import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.util.ColorUtils +import org.autojs.autojs.util.IntentUtils import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull import org.autojs.autojs6.R import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding class PluginCenterItemViewHolder( - itemViewBinding: PluginCenterRecyclerViewItemBinding, + private val itemViewBinding: PluginCenterRecyclerViewItemBinding, private val listener: PluginCenterItemAdapter.Listener, ) : RecyclerView.ViewHolder(itemViewBinding.root) { @@ -65,6 +66,12 @@ class PluginCenterItemViewHolder( iconView.setImageDrawable(d) } ?: iconView.setImageResource(R.mipmap.ic_app_shortcut_plugin_center_adaptive_round) + iconView.setOnClickListener { + if (item.isInstalled) { + IntentUtils.launchAppDetailsSettings(context, item.packageName) + } + } + switchView.setOnCheckedChangeListener(null) switchView.isChecked = item.isEnabled @@ -109,6 +116,14 @@ class PluginCenterItemViewHolder( listener.onDetails(currentItem) } + listOf( + itemViewBinding.title, + itemViewBinding.itemMiddleArea, + itemViewBinding.description, + ).forEach { + it.setOnClickListener { listener.onDetails(currentItem) } + } + applyUiBySwitch(switchView.isChecked, item) switchView.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterState.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterState.kt new file mode 100644 index 00000000..d70f649c --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterState.kt @@ -0,0 +1,49 @@ +package org.autojs.autojs.core.plugin.center + +import org.autojs.autojs.util.StringUtils.str +import org.autojs.autojs6.R + +enum class PluginMechanism(val displayName: String) { + AIDL(str(R.string.text_plugin_mechanism_aidl)), + SDK(str(R.string.text_plugin_mechanism_sdk)), +} + +enum class PluginAuthorizedState { + OFFICIAL, + TRUSTED, + USER_GRANTED, + REQUIRED, + DENIED, +} + +enum class PluginActivatedState { + NOT_SUPPORTED, + UNKNOWN, + RECOMMENDED, + DONE, +} + +sealed class PluginEnabledState { + data object READY : PluginEnabledState() + data object DISABLED : PluginEnabledState() + data class ERROR(val error: PluginError) : PluginEnabledState() +} + +enum class PluginErrorCode { + NOT_AUTHORIZED, + BIND_FAILED, + BIND_SECURITY_EXCEPTION, + SERVICE_NOT_FOUND, + HANDSHAKE_TIMEOUT, + DEAD_OBJECT, + PROTOCOL_MISMATCH, + ROM_FIRST_RUN_RESTRICTED_SUSPECTED, + INTERNAL_ERROR, +} + +data class PluginError( + val code: PluginErrorCode, + val message: String? = null, + val recoverHint: String? = null, + val causeClass: String? = null, +) 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 61d05e55..09a073fc 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 @@ -40,6 +40,7 @@ class PluginCenterViewModel : ViewModel() { // private val indexRepo = PluginIndexRepository() private val installedRepo = InstalledPluginRepository() + private val legacyRepo = LegacyInstalledPluginRepository() private val enableStore = PluginEnableStore private val _items = MutableStateFlow>(emptyList()) @@ -93,6 +94,12 @@ class PluginCenterViewModel : ViewModel() { return@launch } + val legacyInstalled = runCatching { + legacyRepo.discoverInstalled(context) + }.onFailure { e -> + Log.w(TAG, "legacy discovery failed: ${e.message}") + }.getOrElse { emptyList() } + installed.forEach { local -> if (local.bindError != null) { enableStore.setEnabled(context, local.packageName, false) @@ -104,9 +111,13 @@ class PluginCenterViewModel : ViewModel() { // Render list using "local only". // zh-CN: 使用 "仅本地" 渲染列表. - val onlyLocalItems = installed.mapNotNull { local -> + val localAidlItems = installed.mapNotNull { local -> toPluginCenterItem(context, index = null, local = local) } + val localLegacyItems = legacyInstalled.mapNotNull { local -> + toLegacyPluginCenterItem(context, local) + } + val onlyLocalItems = localAidlItems + localLegacyItems _items.value = onlyLocalItems // Asynchronously load index and merge. @@ -132,9 +143,10 @@ class PluginCenterViewModel : ViewModel() { // Supplement plugins that "exist locally but not in index" (third-party/not yet in index). // zh-CN: 补充 "本地有但索引没有" 的插件 (第三方/暂未入索引). - val extraLocals = installed + val extraAidlLocals = installed .filter { ins -> indexEntries.none { it.packageName == ins.packageName } } .mapNotNull { local -> toPluginCenterItem(context, index = null, local = local) } + val extraLocals = extraAidlLocals + localLegacyItems _items.value = fromIndex + extraLocals _indexLoaded.value = true @@ -145,10 +157,21 @@ class PluginCenterViewModel : ViewModel() { } } - fun setEnabled(context: Context, packageName: String, enabled: Boolean) { + fun setEnabled(context: Context, packageName: String, enabled: Boolean, error: PluginError? = null) { enableStore.setEnabled(context, packageName, enabled) _items.value = _items.value.map { - if (it.packageName == packageName) it.copy(isEnabled = enabled) else it + if (it.packageName == packageName) { + val nextEnabledState = when { + !enabled -> PluginEnabledState.DISABLED + error != null -> PluginEnabledState.ERROR(error) + else -> PluginEnabledState.READY + } + it.copy( + isEnabled = enabled, + enabledState = nextEnabledState, + lastError = error, + ) + } else it } PluginInfoDialogManager.refreshIfShowing(context, _items.value) } @@ -184,7 +207,41 @@ class PluginCenterViewModel : ViewModel() { candidates.firstOrNull { !UpdateIgnoreStore.isIgnored(packageName, it.versionCode) } } - val enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled) + val trustInfo = runCatching { PluginTrustManager.resolveTrustInfo(context, packageName) }.getOrElse { + PluginTrustManager.TrustInfo( + authorizedState = PluginAuthorizedState.REQUIRED, + isOfficial = false, + isTrusted = false, + primaryFingerprintSha256 = null, + fingerprintsSha256 = emptyList(), + ) + } + + var enabled = enableStore.isEnabled(context, packageName, defaultEnabled = isInstalled) + if (trustInfo.authorizedState == PluginAuthorizedState.REQUIRED && enabled) { + enableStore.setEnabled(context, packageName, false) + enabled = false + } + + val canActivate = isInstalled && PluginWakeManager.buildWakeIntent(context, packageName) != null + var activatedState = when { + !canActivate -> PluginActivatedState.NOT_SUPPORTED + PluginActivationStore.getLastActivatedAt(context, packageName) != null -> PluginActivatedState.DONE + else -> PluginActivatedState.UNKNOWN + } + + val mappedError = local?.bindError?.let { PluginErrorMapper.fromThrowable(it) } + if (mappedError != null && canActivate && activatedState == PluginActivatedState.UNKNOWN && PluginErrorMapper.shouldRecommendActivation(mappedError)) { + activatedState = PluginActivatedState.RECOMMENDED + } + val authError = if (trustInfo.authorizedState == PluginAuthorizedState.REQUIRED) PluginError(PluginErrorCode.NOT_AUTHORIZED) else null + val lastError = authError ?: mappedError + val enabledState = when { + !enabled -> PluginEnabledState.DISABLED + mappedError != null -> PluginEnabledState.ERROR(mappedError) + else -> PluginEnabledState.READY + } + val readyEnabled = enabled && enabledState is PluginEnabledState.READY return PluginCenterItem( title = title, @@ -202,7 +259,7 @@ class PluginCenterViewModel : ViewModel() { updatableChangelogUrl = targetUpdate?.changelogUrl, updatableChangelogText = targetUpdate?.changelogText, - author = author, + author = author ?: trustInfo.developer, collaborators = collaborators, description = description, @@ -214,12 +271,86 @@ class PluginCenterViewModel : ViewModel() { // TODO 已安装优先用应用图标; 未安装走默认占位图. icon = local?.icon, - isEnabled = enabled, + isEnabled = readyEnabled, isInstalled = isInstalled, firstInstallTime = local?.firstInstallTime, lastUpdateTime = local?.lastUpdateTime, // TODO M1 暂不接入单插件设置入口. settings = null, + mechanism = PluginMechanism.AIDL, + authorizedState = trustInfo.authorizedState, + activatedState = activatedState, + enabledState = enabledState, + lastError = lastError, + signingFingerprintSha256 = trustInfo.primaryFingerprintSha256, + isOfficialVerified = trustInfo.isOfficial, + canActivate = canActivate, + ) + } + + private fun toLegacyPluginCenterItem(context: Context, local: LegacyInstalledPluginRepository.LegacyInstalledPlugin): PluginCenterItem? { + val packageName = local.packageName + if (packageName.isBlank()) return null + + val trustInfo = runCatching { PluginTrustManager.resolveTrustInfo(context, packageName) }.getOrElse { + PluginTrustManager.TrustInfo( + authorizedState = PluginAuthorizedState.REQUIRED, + isOfficial = false, + isTrusted = false, + primaryFingerprintSha256 = null, + fingerprintsSha256 = emptyList(), + ) + } + + var enabled = enableStore.isEnabled(context, packageName, defaultEnabled = true) + if (trustInfo.authorizedState == PluginAuthorizedState.REQUIRED && enabled) { + enableStore.setEnabled(context, packageName, false) + enabled = false + } + + val enabledState = if (enabled) PluginEnabledState.READY else PluginEnabledState.DISABLED + val lastError = if (trustInfo.authorizedState == PluginAuthorizedState.REQUIRED) PluginError(PluginErrorCode.NOT_AUTHORIZED) else null + + return PluginCenterItem( + title = local.title, + packageName = packageName, + versionName = local.versionName, + versionCode = local.versionCode, + versionDate = null, + + updatableVersionName = null, + updatableVersionCode = null, + updatableVersionDate = null, + updatableApkUrl = null, + updatableApkSha256 = null, + updatableApkSizeBytes = null, + updatableChangelogUrl = null, + updatableChangelogText = null, + + author = local.author ?: trustInfo.developer, + collaborators = emptyList(), + description = local.description, + + packageSize = local.packageSize, + + installableApkUrl = null, + installableApkSha256 = null, + installableApkSizeBytes = null, + + icon = local.icon, + isEnabled = enabled, + isInstalled = true, + firstInstallTime = local.firstInstallTime, + lastUpdateTime = local.lastUpdateTime, + settings = null, + mechanism = PluginMechanism.SDK, + authorizedState = trustInfo.authorizedState, + activatedState = PluginActivatedState.NOT_SUPPORTED, + enabledState = enabledState, + lastError = lastError, + signingFingerprintSha256 = trustInfo.primaryFingerprintSha256, + isOfficialVerified = trustInfo.isOfficial, + canActivate = false, ) } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginDescriptionResolver.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginDescriptionResolver.kt new file mode 100644 index 00000000..ec3d1aaf --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginDescriptionResolver.kt @@ -0,0 +1,27 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.res.Configuration +import org.autojs.autojs.core.pref.Language +import java.util.Locale + +object PluginDescriptionResolver { + + private const val DEFAULT_DESCRIPTION_RES_NAME = "plugin_description" + + fun resolve(context: Context, packageName: String, fallback: String?): String? { + val locale = Language.getPrefLanguage().locale + return getStringByName(context, packageName, DEFAULT_DESCRIPTION_RES_NAME, locale) ?: fallback + } + + private fun getStringByName(context: Context, packageName: String, resName: String, locale: Locale): String? { + val res = runCatching { context.packageManager.getResourcesForApplication(packageName) }.getOrNull() ?: return null + val resId = res.getIdentifier(resName, "string", packageName).takeIf { it != 0 } ?: return null + return runCatching { + val pkgCtx = context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY) + val config = Configuration(pkgCtx.resources.configuration).apply { setLocale(locale) } + val localizedCtx = pkgCtx.createConfigurationContext(config) + localizedCtx.getString(resId) + }.getOrNull() + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginErrorMapper.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginErrorMapper.kt new file mode 100644 index 00000000..e49b28d0 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginErrorMapper.kt @@ -0,0 +1,38 @@ +package org.autojs.autojs.core.plugin.center + +import android.os.DeadObjectException +import android.os.RemoteException + +object PluginErrorMapper { + + fun fromThrowable(t: Throwable): PluginError { + val message = t.message ?: t.toString() + val causeClass = t.javaClass.name + val code = when { + t is SecurityException -> PluginErrorCode.BIND_SECURITY_EXCEPTION + t is DeadObjectException -> PluginErrorCode.DEAD_OBJECT + t is RemoteException -> PluginErrorCode.DEAD_OBJECT + message.contains("No OCR service found", ignoreCase = true) -> PluginErrorCode.SERVICE_NOT_FOUND + message.contains("bindService SecurityException", ignoreCase = true) -> PluginErrorCode.BIND_SECURITY_EXCEPTION + message.contains("bindService failed", ignoreCase = true) -> PluginErrorCode.BIND_FAILED + message.contains("bindService timeout", ignoreCase = true) -> PluginErrorCode.HANDSHAKE_TIMEOUT + message.contains("timeout", ignoreCase = true) && message.contains("bind", ignoreCase = true) -> PluginErrorCode.HANDSHAKE_TIMEOUT + else -> PluginErrorCode.INTERNAL_ERROR + } + return PluginError( + code = code, + message = message, + causeClass = causeClass, + ) + } + + fun shouldRecommendActivation(error: PluginError): Boolean { + return when (error.code) { + PluginErrorCode.BIND_FAILED, + PluginErrorCode.HANDSHAKE_TIMEOUT, + PluginErrorCode.DEAD_OBJECT, + -> true + else -> false + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginFilterStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginFilterStore.kt new file mode 100644 index 00000000..fc779afe --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginFilterStore.kt @@ -0,0 +1,43 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import org.autojs.autojs.core.plugin.center.PluginCenterFragment.Filter + +object PluginFilterStore { + + private const val SP_NAME = "plugin_center_filter_state" + internal const val KEY = "key_\$_plugin_center_filter_state" + + private fun sp(context: Context) = context.applicationContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + + fun getFilter(context: Context, defaultFilter: Int = Filter.ALL.ordinal): Filter { + val ordinal = getFilterOrdinal(context, defaultFilter) + return Filter.values()[ordinal] + } + + fun getFilterOrdinal(context: Context, defaultFilterOrdinal: Int = Filter.ALL.ordinal): Int { + val sp = sp(context) + return sp.getInt(KEY, defaultFilterOrdinal) + } + + fun setFilter(context: Context, filter: Filter) { + setFilterOrdinal(context, filter.ordinal) + } + + fun setFilterOrdinal(context: Context, filterOrdinal: Int) { + val sp = sp(context) + sp.edit { putInt(KEY, filterOrdinal) } + } + + fun registerOnSharedPreferenceChangeListener(context: Context, onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener) { + val sp = sp(context) + sp.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + fun unregisterOnSharedPreferenceChangeListener(context: Context, onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener) { + val sp = sp(context) + sp.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } +} 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 7d88deed..1121bce2 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 @@ -17,13 +17,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.util.DialogUtils.showAdaptive -import org.autojs.autojs.util.DialogUtils.makeSettingsLaunchable -import org.autojs.autojs.util.DialogUtils.makeTextCopyable -import org.autojs.autojs.util.DialogUtils.setCopyableTextIfAbsent import org.autojs.autojs.runtime.api.augment.converter.core.Bytes import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.util.ColorUtils +import org.autojs.autojs.util.DialogUtils.makeSettingsLaunchable +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableTextIfAbsent +import org.autojs.autojs.util.DialogUtils.showAdaptive import org.autojs.autojs.util.DisplayUtils import org.autojs.autojs.util.TimeUtils import org.autojs.autojs.util.ViewUtils @@ -68,11 +68,11 @@ object PluginInfoDialogManager { lastUninstallTime = item.lastUninstallTime, ) showPluginInfoDialogInternal(context, info) { - positiveText(R.string.text_install) - positiveColorRes(R.color.dialog_button_attraction) - onPositive { d, _ -> + neutralText(R.string.text_install) + neutralColorRes(R.color.dialog_button_attraction) + onNeutral { d, _ -> d.dismiss() - val url = info.validateApkUrlAndPrompt(context, d) ?: return@onPositive + val url = info.validateApkUrlAndPrompt(context, d) ?: return@onNeutral CoroutineScope(Dispatchers.Main).launch { PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256) } @@ -81,21 +81,14 @@ object PluginInfoDialogManager { } private fun showInstalledPluginInfoDialog(context: Context, item: PluginCenterItem) { - val enabledRes = if (item.isEnabled) R.string.text_enabled else R.string.text_disabled - val states = mutableListOf(context.getString(enabledRes)).apply { - if (item.isUpdatable) add(context.getString(R.string.text_updatable)) - } val info = PluginInfoInstalled( item = item, - states = states, + states = parseStates(context, item), updatableVersion = item.updatableVersionSummary, firstInstallTime = item.firstInstallTime, lastUpdateTime = item.lastUpdateTime, ) showPluginInfoDialogInternal(context, info) { - positiveText(R.string.text_uninstall) - positiveColorRes(R.color.dialog_button_warn) - onPositive { d, _ -> item.uninstallWithPrompt(context, d) } if (item.isUpdatable) { neutralText(R.string.dialog_button_view_update) neutralColorRes(R.color.dialog_button_attraction) @@ -137,15 +130,32 @@ object PluginInfoDialogManager { } 1 -> { binding.stateValueFirst.text = info.states[0] + + binding.stateValueFirst.isVisible = true } - else -> { + 2 -> { + binding.stateValueFirst.text = info.states[0] binding.stateValueSecond.text = info.states[1] + + binding.stateValueFirst.isVisible = true binding.stateSpliterFirstSecond.isVisible = true binding.stateValueSecond.isVisible = true } + else -> { + binding.stateValueFirst.text = info.states[0] + binding.stateValueSecond.text = info.states[1] + binding.stateValueThird.text = info.states[2] + + binding.stateValueFirst.isVisible = true + binding.stateSpliterFirstSecond.isVisible = true + binding.stateValueSecond.isVisible = true + binding.stateSpliterSecondThird.isVisible = true + binding.stateValueThird.isVisible = true + } } dialog.setCopyableTextIfAbsent(binding.packageNameValue, info.packageName) + dialog.setCopyableTextIfAbsent(binding.mechanismValue, info.mechanism) dialog.setCopyableTextIfAbsent(binding.versionValue, info.version) dialog.setCopyableTextIfAbsent(binding.pluginItemInfoAuthorValue, info.author) dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description) @@ -237,6 +247,42 @@ object PluginInfoDialogManager { .cancelable(false) } + private fun parseStates(context: Context, item: PluginCenterItem): List { + if (!item.isInstalled) { + return listOf(context.getString(R.string.text_not_installed)) + } + + val states = mutableListOf() + + val authText = when (item.authorizedState) { + PluginAuthorizedState.OFFICIAL -> context.getString(R.string.text_plugin_official) + PluginAuthorizedState.TRUSTED -> context.getString(R.string.text_plugin_trusted) + PluginAuthorizedState.USER_GRANTED -> context.getString(R.string.text_plugin_authorized) + PluginAuthorizedState.REQUIRED -> context.getString(R.string.text_plugin_authorization_required) + PluginAuthorizedState.DENIED -> context.getString(R.string.text_plugin_authorization_denied) + } + states += authText + + val enabledText = when (item.enabledState) { + PluginEnabledState.READY -> context.getString(R.string.text_enabled) + PluginEnabledState.DISABLED -> context.getString(R.string.text_disabled) + is PluginEnabledState.ERROR -> context.getString(R.string.text_error) + } + states += enabledText + + if (item.isUpdatable) { + states += context.getString(R.string.text_updatable) + } + + when (item.activatedState) { + PluginActivatedState.RECOMMENDED -> states += context.getString(R.string.text_plugin_activation_recommended) + PluginActivatedState.DONE -> states += context.getString(R.string.text_plugin_activated) + else -> Unit + } + + return states.filter { it.isNotBlank() } + } + private fun PluginInfoBase.validateApkUrlAndPrompt(context: Context, parentDialog: MaterialDialog?): String? { val url = this.apkUrl return when { @@ -295,6 +341,7 @@ object PluginInfoDialogManager { private fun updateGuidelines(binding: PluginInfoDialogItemsBinding) { val filteredBindings = listOf( binding.stateLabel to binding.stateGuideline, + binding.mechanismLabel to binding.mechanismGuideline, binding.packageNameLabel to binding.packageNameGuideline, binding.versionLabel to binding.versionGuideline, binding.updatableVersionLabel to binding.updatableVersionGuideline, @@ -345,6 +392,7 @@ object PluginInfoDialogManager { val title: String get() = item.title val icon: Drawable? get() = item.icon val packageName: String get() = item.packageName + val mechanism: String get() = item.mechanism.displayName val version: String? get() = item.versionSummary val author: String? get() = item.author val collaborators: List get() = item.collaborators diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSignatureUtils.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSignatureUtils.kt new file mode 100644 index 00000000..51f9ea15 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSignatureUtils.kt @@ -0,0 +1,42 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import java.security.MessageDigest + +object PluginSignatureUtils { + + @Suppress("DEPRECATION") + fun getSha256Fingerprints(context: Context, packageName: String): List { + val pm = context.packageManager + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_SIGNING_CERTIFICATES + } else { + PackageManager.GET_SIGNATURES + } + val pkgInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) + } else { + pm.getPackageInfo(packageName, flags) + } + val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + pkgInfo.signingInfo?.apkContentsSigners + } else { + pkgInfo.signatures + } + val list = signatures?.mapNotNull { sig -> + runCatching { sha256Hex(sig.toByteArray()) }.getOrNull() + } ?: emptyList() + return list.distinct() + } + + private fun sha256Hex(bytes: ByteArray): String { + val digest = MessageDigest.getInstance("SHA-256").digest(bytes) + val sb = StringBuilder(digest.size * 2) + for (b in digest) { + sb.append(String.format("%02x", b)) + } + return sb.toString() + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSortStore.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSortStore.kt new file mode 100644 index 00000000..d3a086f2 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginSortStore.kt @@ -0,0 +1,43 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import org.autojs.autojs.core.plugin.center.PluginCenterFragment.Sort + +object PluginSortStore { + + private const val SP_NAME = "plugin_center_sort_state" + private const val KEY = "key_\$_plugin_center_sort_state" + + private fun sp(context: Context) = context.applicationContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + + fun getSort(context: Context, defaultSort: Int = Sort.LAST_UPDATE_DESC.ordinal): Sort { + val ordinal = getSortOrdinal(context, defaultSort) + return Sort.values()[ordinal] + } + + fun getSortOrdinal(context: Context, defaultSortOrdinal: Int = Sort.LAST_UPDATE_DESC.ordinal): Int { + val sp = sp(context) + return sp.getInt(KEY, defaultSortOrdinal) + } + + fun setSort(context: Context, sort: Sort) { + setSortOrdinal(context, sort.ordinal) + } + + fun setSortOrdinal(context: Context, sortOrdinal: Int) { + val sp = sp(context) + sp.edit { putInt(KEY, sortOrdinal) } + } + + fun registerOnSharedPreferenceChangeListener(context: Context, onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener) { + val sp = sp(context) + sp.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } + + fun unregisterOnSharedPreferenceChangeListener(context: Context, onSharedPreferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener) { + val sp = sp(context) + sp.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginTrustManager.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginTrustManager.kt new file mode 100644 index 00000000..7228e430 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginTrustManager.kt @@ -0,0 +1,104 @@ +package org.autojs.autojs.core.plugin.center + +import android.content.Context + +object PluginTrustManager { + + data class TrustInfo( + val authorizedState: PluginAuthorizedState, + val isOfficial: Boolean, + val isTrusted: Boolean, + val developer: String? = null, + val primaryFingerprintSha256: String?, + val fingerprintsSha256: List, + ) + + fun resolveTrustInfo(context: Context, packageName: String): TrustInfo { + val fingerprints = PluginSignatureUtils.getSha256Fingerprints(context, packageName) + val matchingPluginIdentifier = PLUGIN_IDENTIFIERS.firstOrNull { ids -> + fingerprints.any { ids.fingerprintsSha256.contains(it) } + } + + val isOfficial = fingerprints.any { it in OFFICIAL_SHA_256 } + val isTrusted = matchingPluginIdentifier?.state == PluginAuthorizedState.TRUSTED + val developer = matchingPluginIdentifier?.developer + + val authorizedState = when { + isOfficial -> PluginAuthorizedState.OFFICIAL + isTrusted -> PluginAuthorizedState.TRUSTED + PluginAuthorizationStore.isGranted(context, packageName, fingerprints) -> PluginAuthorizedState.USER_GRANTED + else -> PluginAuthorizedState.REQUIRED + } + return TrustInfo( + authorizedState = authorizedState, + isOfficial = isOfficial, + isTrusted = isTrusted, + developer = developer, + primaryFingerprintSha256 = fingerprints.firstOrNull(), + fingerprintsSha256 = fingerprints, + ) + } + + fun isAuthorized(context: Context, packageName: String): Boolean { + val info = resolveTrustInfo(context, packageName) + // @formatter:off + return info.authorizedState == PluginAuthorizedState.OFFICIAL + || info.authorizedState == PluginAuthorizedState.TRUSTED + || info.authorizedState == PluginAuthorizedState.USER_GRANTED + // @formatter:on + } + + val OFFICIAL_SHA_256 = setOf("31a681fcfffb3e428420cae280ded89292b12a3b0f59e19b7a73e32a8ae4c213") + + val PLUGIN_IDENTIFIERS = listOf( + PluginIdentifier( + OFFICIAL_SHA_256, + "SuperMonster003", PluginAuthorizedState.OFFICIAL, + ), + PluginIdentifier( + setOf( + "9cf34f732e0b93f78fe9f2ef662b4fd153db1dd1426cab9aefb9b9f6f8ace5f0", // Auto.js + "6840d437e677b627607768aec5f307e314af4f06f179de8bd9aea8b5963c6b3a", // Plugins + ), + "hyb1996", PluginAuthorizedState.TRUSTED, + ), + PluginIdentifier( + setOf( + "517c51b16bead916296eb3cadfd57cd4f871ae8a4f767094ddf635338bad21c1", // Auto.js M + "2e64822e13a6c80c12e1c4b47e8fb32d1e9334526289da75777b7a79145de4b8", // Plugins + "a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc", // Plugins + ), + "TonyJiangWJ", PluginAuthorizedState.TRUSTED, + ), + PluginIdentifier( + setOf( + "f4595765fb1928aabc3fc231451c5a2ab4c2f896e5cac2cbff08d40b4dcd1b77", // Autox.js v7 + ), + "aiselp", PluginAuthorizedState.TRUSTED, + ), + PluginIdentifier( + setOf( + "03c4fd8935c4e330a7553a0dc7c1e88ea5d38b42093422c0a05a9f72eab8bd43", // Plugins + ), + "LZX284", PluginAuthorizedState.TRUSTED, + ), + PluginIdentifier( + setOf( + "6325752bb7c5d6d9f147e53cb1cf743cc16db4f557d947ab0b8a597b81199d0c", // Plugins + ), + "HRan2004", PluginAuthorizedState.TRUSTED, + ), + PluginIdentifier( + setOf( + "8ff046d10b78f8cec3906e240866dcf75f70204acb3f1c8e4baf153cb971085c", // Plugins; Main APK + ), + "TomatoOCR", + ) + ) + + data class PluginIdentifier( + val fingerprintsSha256: Set, + val developer: String? = null, + val state: PluginAuthorizedState = PluginAuthorizedState.REQUIRED, + ) +} 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 8900b5bc..f701ac2d 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 @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.CancellableContinuation import org.autojs.autojs.core.plugin.center.PluginEnableStore +import org.autojs.autojs.core.plugin.center.PluginTrustManager import org.autojs.plugin.paddle.ocr.api.IOcrPlugin import org.autojs.plugin.paddle.ocr.api.OcrOptions import org.autojs.plugin.paddle.ocr.api.OcrResult @@ -85,6 +86,9 @@ object PaddleOcrPluginHost { } suspend fun probe(context: Context, packageName: String): PluginInfo { + if (!PluginTrustManager.isAuthorized(context, packageName)) { + error("Plugin not authorized: $packageName") + } val serviceInfo = queryOcrServices(context, packageName).firstOrNull() ?: error("No OCR service found for package: $packageName") return withService(context, serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { it.getInfo() } @@ -97,6 +101,9 @@ object PaddleOcrPluginHost { options: OcrOptions = OcrOptions(), callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS, ): List { + if (!PluginTrustManager.isAuthorized(context, target.serviceInfo.packageName)) { + error("Plugin not authorized: ${target.serviceInfo.packageName}") + } ensureRawSupport(target, options) val start = uptimeMillis() return createTempPfd(bitmap, options).use { pfd -> @@ -117,6 +124,9 @@ object PaddleOcrPluginHost { options: OcrOptions = OcrOptions(), callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS, ): List { + if (!PluginTrustManager.isAuthorized(context, target.serviceInfo.packageName)) { + error("Plugin not authorized: ${target.serviceInfo.packageName}") + } ensureRawSupport(target, options) val start = uptimeMillis() return createTempPfd(bitmap, options).use { pfd -> @@ -142,6 +152,7 @@ object PaddleOcrPluginHost { val list = discover(context) .filter { it.pluginInfo != null } .filter { PluginEnableStore.isEnabled(context, it.serviceInfo.packageName, true) } + .filter { PluginTrustManager.isAuthorized(context, it.serviceInfo.packageName) } if (list.isEmpty()) return null if (engineId != null) { list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt b/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt index c2fae5d3..79db4a3a 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Plugins.kt @@ -11,10 +11,13 @@ import android.os.Parcel import android.os.RemoteException import org.autojs.autojs.core.plugin.Plugin import org.autojs.autojs.core.plugin.Plugin.PluginLoadException +import org.autojs.autojs.core.plugin.center.PluginEnableStore +import org.autojs.autojs.core.plugin.center.PluginTrustManager import org.autojs.autojs.execution.ExecutionConfig import org.autojs.autojs.pio.PFiles.copyAssetDir import org.autojs.autojs.pio.PFiles.deleteRecursively import org.autojs.autojs.rhino.TopLevelScope +import org.autojs.autojs6.R import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -32,9 +35,16 @@ class Plugins(private val context: Context, private val runtime: PluginRuntime) fun load(packageName: String): Plugin { mPlugins[packageName]?.let { return it } + if (!PluginEnableStore.isEnabled(context, packageName, defaultEnabled = true)) { + throw PluginLoadException(context.getString(R.string.error_plugin_is_not_enabled_in_plugin_center, packageName)) + } + if (!PluginTrustManager.isAuthorized(context, packageName)) { + throw PluginLoadException(context.getString(R.string.error_plugin_is_not_authorized_in_plugin_center, packageName)) + } + var packageContext = packages[packageName] ?: loadInstalledPackage(packageName) ?: throw Resources.NotFoundException( // "Plugin $packageName not found in installed apps or directory ${File(runtime.pluginSearchDir)}" - "Plugin $packageName not found in installed apps" + context.getString(R.string.error_plugin_not_found_in_installed_apps, packageName) ) packages.putIfAbsent(packageName, packageContext)?.let { packageContext = it } @@ -101,9 +111,9 @@ class Plugins(private val context: Context, private val runtime: PluginRuntime) override fun getFilesDir(): File = hostContext.filesDir } - private class UnsupportedConnection : ServiceProxy(null), IRemoteCall { + private inner class UnsupportedConnection : ServiceProxy(null), IRemoteCall { override fun call(action: String, args: Map, callback: IRemoteCallback): Map { - throw UnsupportedOperationException("Unsupported plugin connection") + throw UnsupportedOperationException(context.getString(R.string.error_unsupported_plugin_connection)) } } diff --git a/app/src/main/res/layout/plugin_center_recycler_view_item.xml b/app/src/main/res/layout/plugin_center_recycler_view_item.xml index 0cbb817e..bc1c6ffc 100644 --- a/app/src/main/res/layout/plugin_center_recycler_view_item.xml +++ b/app/src/main/res/layout/plugin_center_recycler_view_item.xml @@ -73,9 +73,7 @@ android:textColor="#03A5EF" android:textSize="12sp" android:text="@string/text_updatable" /> - - - - - - + + + + + - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/plugin_info_dialog_items.xml b/app/src/main/res/layout/plugin_info_dialog_items.xml index d5cb3a2a..342e74cd 100644 --- a/app/src/main/res/layout/plugin_info_dialog_items.xml +++ b/app/src/main/res/layout/plugin_info_dialog_items.xml @@ -19,7 +19,7 @@ android:layout_marginStart="@dimen/ref_md_listitem_margin_left" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/package_name_parent" + app:layout_constraintBottom_toTopOf="@id/mechanism_parent" app:layout_constraintEnd_toEndOf="parent"> + + + + @@ -129,6 +170,73 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b1849ac6..bc9998cb 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1340,4 +1340,19 @@ جرّب الضغط على زر \"تفعيل\" لتفعيل الاضافة مرة واحدة.\nاذا نجح التفعيل، فسيتم تشغيل الزر تلقائيا خلال فترة زمنية معينة. تفعيل تم التفعيل بنجاح + تفويض الاضافة + هذه اضافة من طرف ثالث. يرجى الانتباه الى مصدر الاضافة وامان الاستخدام.\n\nاضغط زر \"تفويض\" لتمكين الاضافة. + تفويض + رسمي + مفوّض + يتطلب تفويضا + تم رفض التفويض + يوصى بالتفعيل + مفعّل + موثوق + الالية + الاضافة \"%1$s\" غير مُمكّنة في مركز الاضافات + الاضافة \"%1$s\" غير مُفوّضة في مركز الاضافات + لم يتم العثور على الاضافة \"%1$s\" ضمن التطبيقات المثبتة + طريقة اتصال الاضافة غير مدعومة \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 5c07fcaa..c141f7f5 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1335,4 +1335,19 @@ Try clicking the \"Activate\" button to activate the plugin once.\nIf activation is successful, the button will automatically turn on within a certain period of time. Activate Activated successfully + Authorize Plugin + This is a third-party plugin. Please pay attention to the plugin source and usage safety.\n\nClick the \"Authorize\" button to enable the plugin. + Authorize + Official + Authorized + Authorization required + Authorization denied + Activation recommended + Activated + Trusted + Mechanism + Plugin \"%1$s\" is not enabled in Plugin Center + Plugin \"%1$s\" is not authorized in Plugin Center + Plugin \"%1$s\" not found in installed apps + Unsupported plugin connection \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 31efcac7..f02f6331 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1338,4 +1338,19 @@ Prueba a pulsar el boton \"Activar\" para activar el plugin una vez.\nSi la activacion se realiza correctamente, el boton se activara automaticamente en un periodo de tiempo. Activar Activado correctamente + Autorizar plugin + Este es un plugin de terceros. Presta atencion al origen del plugin y a la seguridad de uso.\n\nPulsa el boton \"Autorizar\" para habilitar el plugin. + Autorizar + Oficial + Autorizado + Requiere autorizacion + Autorizacion denegada + Activacion recomendada + Activado + Confiable + Mecanismo + El plugin \"%1$s\" no esta habilitado en el Centro de plugins + El plugin \"%1$s\" no esta autorizado en el Centro de plugins + No se encontro el plugin \"%1$s\" en las apps instaladas + Conexion de plugin no compatible \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1c00bcd4..0a23277b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1338,4 +1338,19 @@ Essayez d\'appuyer sur le bouton \"Activer\" pour activer le plugin une fois.\nSi l\'activation reussit, le bouton s\'activera automatiquement dans un certain delai. Activer Activation reussie + Autoriser le plugin + Il s\'agit d\'un plugin tiers. Faites attention a la source du plugin et a la securite d\'utilisation.\n\nAppuyez sur le bouton \"Autoriser\" pour activer le plugin. + Autoriser + Officiel + Autorise + Autorisation requise + Autorisation refusee + Activation recommandee + Active + De confiance + Mecanisme + Le plugin \"%1$s\" n\'est pas active dans le Centre des plugins + Le plugin \"%1$s\" n\'est pas autorise dans le Centre des plugins + Plugin \"%1$s\" introuvable parmi les applis installees + Connexion de plugin non prise en charge \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a6674186..a580ae99 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1339,4 +1339,19 @@ \"有効化\" ボタンをタップして, プラグインを一度有効化してみてください.\n有効化に成功すると, 一定時間内にボタンが自動的にオンになります. 有効化 有効化しました + プラグインを許可 + これはサードパーティ製プラグインです. 提供元と利用上の安全性に注意してください.\n\n\"許可\" ボタンをタップしてプラグインを有効にします. + 許可 + 公式 + 許可済み + 許可が必要 + 許可が拒否されました + 有効化を推奨 + 有効 + 信頼済み + 方式 + プラグイン \"%1$s\" はプラグインセンターで有効になっていません + プラグイン \"%1$s\" はプラグインセンターで許可されていません + インストール済みアプリにプラグイン \"%1$s\" が見つかりません + サポートされていないプラグイン接続方式です \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2688fb73..655d6481 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1340,4 +1340,19 @@ \"활성화\" 버튼을 눌러 플러그인을 한 번 활성화해 보세요.\n활성화에 성공하면 일정 시간 내에 버튼이 자동으로 켜집니다. 활성화 활성화되었습니다 + 플러그인 권한 부여 + 이 플러그인은 타사 플러그인입니다. 플러그인 출처와 사용 안전에 유의하세요.\n\n\"권한 부여\" 버튼을 눌러 플러그인을 활성화하세요. + 권한 부여 + 공식 + 권한 부여됨 + 권한 필요 + 권한 거부됨 + 활성화 권장 + 활성화됨 + 신뢰됨 + 방식 + 플러그인 \"%1$s\" 이(가) 플러그인 센터에서 활성화되어 있지 않습니다 + 플러그인 \"%1$s\" 이 (가) 플러그인 센터에서 권한이 부여되지 않았습니다 + 설치된 앱에서 플러그인 \"%1$s\" 을 (를) 찾을 수 없습니다 + 지원되지 않는 플러그인 연결 방식입니다 \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0f4a7834..b266a7cf 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1338,4 +1338,19 @@ Попробуйте нажать кнопку \"Активировать\", чтобы активировать плагин один раз.\nЕсли активация пройдет успешно, кнопка автоматически включится в течение некоторого времени. Активировать Активация выполнена + Авторизовать плагин + Это сторонний плагин. Обратите внимание на источник плагина и безопасность использования.\n\nНажмите кнопку \"Авторизовать\", чтобы включить плагин. + Авторизовать + Официальный + Авторизован + Требуется авторизация + Авторизация отклонена + Рекомендуется активация + Активирован + Доверенный + Механизм + Плагин \"%1$s\" не включен в Центре плагинов + Плагин \"%1$s\" не авторизован в Центре плагинов + Плагин \"%1$s\" не найден среди установленных приложений + Неподдерживаемое подключение плагина \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index d6b575ee..c7c4393e 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1334,4 +1334,19 @@ 可嘗試點擊 \"激活\" 按鈕激活一次插件.\n如激活成功, 按鈕將在一定時間內自動開啓. 激活 激活成功 + 授權插件 + 這是一個第三方插件, 請注意插件來源及使用安全.\n\n點擊 \"授權\" 按鈕以啓用插件. + 授權 + 官方 + 已授權 + 需要授權 + 授權拒絕 + 建議激活 + 已激活 + 受信任 + 機制 + 插件 \"%1$s\" 未在插件中心啓用 + 插件 \"%1$s\" 未在插件中心獲得授權 + 已安裝應用中未找到插件 \"%1$s\" + 不支持的插件連接方式 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index bbdc0b82..a9c0655d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1334,4 +1334,19 @@ 可嘗試點選 \"啟用\" 按鈕啟用一次外掛.\n如啟用成功, 按鈕將在一定時間內自動開啟. 啟用 啟用成功 + 授權外掛 + 這是一個第三方外掛, 請注意外掛來源及使用安全.\n\n點選 \"授權\" 按鈕以啟用外掛. + 授權 + 官方 + 已授權 + 需要授權 + 授權拒絕 + 建議啟用 + 已啟用 + 受信任 + 機制 + 外掛 \"%1$s\" 未在外掛中心啟用 + 外掛 \"%1$s\" 未在外掛中心獲得授權 + 已安裝應用中未找到外掛 \"%1$s\" + 不支援的外掛連線方式 \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 214740e6..e40fc76b 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1335,4 +1335,19 @@ 可尝试点击 \"激活\" 按钮激活一次插件.\n如激活成功, 按钮将在一定时间内自动开启. 激活 激活成功 + 授权插件 + 这是一个第三方插件, 请注意插件来源及使用安全.\n\n点击 \"授权\" 按钮以启用插件. + 授权 + 官方 + 已授权 + 需要授权 + 授权拒绝 + 建议激活 + 已激活 + 受信任 + 机制 + 插件 \"%1$s\" 未在插件中心启用 + 插件 \"%1$s\" 未在插件中心获得授权 + 已安装应用中未找到插件 \"%1$s\" + 不支持的插件连接方式 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0aa0df5..47c23391 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,6 +151,7 @@ key_$_pref_bundle_default_item key_$_pref_bundle_disabled_items key_$_record_toast + key_$_release_history key_$_restart_strategy key_$_restart_strategy_quick key_$_restart_strategy_scheduled @@ -184,7 +185,6 @@ key_$_updates_checked_states_cleared key_$_use_volume_control_record key_$_use_volume_control_running - key_$_release_history key_$_version_history_restore_does_not_auto_save_to_disk key_$_working_directory key_$_working_directory_histories @@ -251,6 +251,8 @@ %1$s (%2$d) ... JKS + AIDL / IPC + SDK / In-Process Powered by AutoJs6 Root Hello @@ -1609,4 +1611,19 @@ Try clicking the \"Activate\" button to activate the plugin once.\nIf activation is successful, the button will automatically turn on within a certain period of time. Activate Activated successfully - \ No newline at end of file + Authorize Plugin + This is a third-party plugin. Please pay attention to the plugin source and usage safety.\n\nClick the \"Authorize\" button to enable the plugin. + Authorize + Official + Authorized + Authorization required + Authorization denied + Activation recommended + Activated + Trusted + Mechanism + Plugin \"%1$s\" is not enabled in Plugin Center + Plugin \"%1$s\" is not authorized in Plugin Center + Plugin \"%1$s\" not found in installed apps + Unsupported plugin connection + diff --git a/version.properties b/version.properties index b87b4a10..b772f28b 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Wed Feb 25 16:44:22 CST 2026 -BUILD_TIME=1772009062191 +#Sun Mar 01 17:58:13 CST 2026 +BUILD_TIME=1772359093087 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=3753 -VERSION_NAME=6.7.0 Alpha21 +VERSION_BUILD=3761 +VERSION_NAME=6.7.0 Alpha22 VSCODE_EXT_REQUIRED_VERSION=1.0.13