6.7.0 - Alpha22 - 插件中心 M3 - 插件中心支持传统 SDK 插件发现并增加信任机制和授权机制

This commit is contained in:
SuperMonster003
2026-03-01 19:41:12 +08:00
parent cc347aaa12
commit 238784faa0
33 changed files with 1290 additions and 171 deletions

View File

@@ -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,

View File

@@ -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<LegacyInstalledPlugin> = 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"
}
}

View File

@@ -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"
}

View File

@@ -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<String>): 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"
}

View File

@@ -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()
}

View File

@@ -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),
}
}

View File

@@ -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)

View File

@@ -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 ->

View File

@@ -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,
)

View File

@@ -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<List<PluginCenterItem>>(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,
)
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<String> {
if (!item.isInstalled) {
return listOf(context.getString(R.string.text_not_installed))
}
val states = mutableListOf<String>()
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<String> get() = item.collaborators

View File

@@ -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<String> {
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()
}
}

View File

@@ -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)
}
}

View File

@@ -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<String>,
)
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<String>,
val developer: String? = null,
val state: PluginAuthorizedState = PluginAuthorizedState.REQUIRED,
)
}

View File

@@ -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<String> {
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<OcrResult> {
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 }

View File

@@ -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<Any?, Any?>, callback: IRemoteCallback): Map<Any?, Any?> {
throw UnsupportedOperationException("Unsupported plugin connection")
throw UnsupportedOperationException(context.getString(R.string.error_unsupported_plugin_connection))
}
}

View File

@@ -73,9 +73,7 @@
android:textColor="#03A5EF"
android:textSize="12sp"
android:text="@string/text_updatable" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
@@ -85,6 +83,7 @@
android:orientation="horizontal">
<LinearLayout
android:id="@+id/item_middle_area"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
@@ -121,7 +120,6 @@
android:textColor="@color/text_color_primary"
android:textSize="13sp"
tools:text="SuperMonster003" />
</LinearLayout>
<FrameLayout
@@ -132,26 +130,34 @@
<org.autojs.autojs.theme.widget.ThemeColorSwitch
android:id="@+id/sw"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:layout_gravity="center" />
</FrameLayout>
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/text_color_primary_alpha_70"
android:textSize="12sp"
tools:text="百度飞桨光学字符识别插件" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/text_color_primary_alpha_70"
android:textSize="12sp"
tools:text="百度飞桨光学字符识别插件" />
<FrameLayout
android:id="@+id/reserved"
android:layout_width="@dimen/plugin_center_item_side_length"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
@@ -196,7 +202,6 @@
android:textColor="@color/text_color_primary"
android:textSize="13sp"
android:translationY="-0.5dp" />
</LinearLayout>
<View
@@ -236,7 +241,6 @@
android:textSize="13sp"
android:translationY="-0.5dp"
tools:textColor="#03A5EF" />
</LinearLayout>
<View
@@ -273,7 +277,6 @@
android:textColor="@color/text_color_primary_alpha_30"
android:textSize="13sp"
android:translationY="-0.5dp" />
</LinearLayout>
<View
@@ -309,9 +312,6 @@
android:textColor="@color/text_color_primary"
android:textSize="13sp"
android:translationY="-0.5dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -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">
<androidx.constraintlayout.widget.Guideline
@@ -68,7 +68,9 @@
<TextView
android:id="@+id/state_value_first"
android:text="@string/text_enabled"
android:visibility="gone"
tools:visibility="visible"
tools:text="@string/text_plugin_authorization_required"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
@@ -107,6 +109,45 @@
android:id="@+id/state_value_second"
android:visibility="gone"
tools:visibility="visible"
tools:text="@string/text_disabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical|start"
android:textAlignment="viewStart"
android:maxLines="1"
android:ellipsize="end"
android:paddingBottom="0dp"
android:paddingTop="0dp"
android:lineSpacingExtra="2dp"
android:textSize="@dimen/ref_md_listitem_textsize"
app:layout_constraintStart_toEndOf="@id/state_spliter_first_second"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/state_spliter_second_third" />
<TextView
android:id="@+id/state_spliter_second_third"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical|start"
android:textAlignment="viewStart"
android:layout_marginHorizontal="8sp"
android:text="@string/symbol_pipe"
android:textSize="@dimen/ref_md_listitem_textsize"
android:textColor="@color/text_color_secondary"
app:layout_constraintStart_toEndOf="@id/state_value_second"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/state_value_third" />
<TextView
android:id="@+id/state_value_third"
android:visibility="gone"
tools:visibility="visible"
tools:text="@string/text_updatable"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -120,7 +161,7 @@
android:paddingTop="0dp"
android:lineSpacingExtra="2dp"
android:textSize="@dimen/ref_md_listitem_textsize"
app:layout_constraintStart_toEndOf="@id/state_spliter_first_second"
app:layout_constraintStart_toEndOf="@id/state_spliter_second_third"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@@ -129,6 +170,73 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mechanism_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/ref_md_dialog_frame_margin"
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
android:layout_marginTop="4sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/state_parent"
app:layout_constraintBottom_toTopOf="@id/package_name_parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/mechanism_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="100dp" />
<TextView
android:id="@+id/mechanism_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAlignment="viewStart"
android:text="@string/plugin_item_info_mechanism"
android:lineSpacingExtra="2dp"
android:textSize="@dimen/ref_md_listitem_textsize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/mechanism_guideline" />
<TextView
android:id="@+id/mechanism_colon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAlignment="viewStart"
android:layout_marginHorizontal="4sp"
android:text="@string/symbol_colon"
android:textSize="@dimen/ref_md_listitem_textsize"
app:layout_constraintStart_toEndOf="@id/mechanism_label"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/mechanism_value" />
<TextView
android:id="@+id/mechanism_value"
android:text="@string/ellipsis_six"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAlignment="viewStart"
android:maxLines="2"
android:ellipsize="end"
android:paddingBottom="0dp"
android:paddingTop="0dp"
android:lineSpacingExtra="2dp"
android:textSize="@dimen/ref_md_listitem_textsize"
app:layout_constraintStart_toEndOf="@id/mechanism_colon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/package_name_parent"
android:layout_width="match_parent"
@@ -137,7 +245,7 @@
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
android:layout_marginTop="4sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/state_parent"
app:layout_constraintTop_toBottomOf="@id/mechanism_parent"
app:layout_constraintBottom_toTopOf="@id/version_parent"
app:layout_constraintEnd_toEndOf="parent">

View File

@@ -1340,4 +1340,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">جرّب الضغط على زر \"تفعيل\" لتفعيل الاضافة مرة واحدة.\nاذا نجح التفعيل، فسيتم تشغيل الزر تلقائيا خلال فترة زمنية معينة.</string>
<string name="dialog_button_activate">تفعيل</string>
<string name="text_activated_successfully">تم التفعيل بنجاح</string>
<string name="text_authorize_plugin">تفويض الاضافة</string>
<string name="text_authorize_plugin_content">هذه اضافة من طرف ثالث. يرجى الانتباه الى مصدر الاضافة وامان الاستخدام.\n\nاضغط زر \"تفويض\" لتمكين الاضافة.</string>
<string name="dialog_button_authorize">تفويض</string>
<string name="text_plugin_official">رسمي</string>
<string name="text_plugin_authorized">مفوّض</string>
<string name="text_plugin_authorization_required">يتطلب تفويضا</string>
<string name="text_plugin_authorization_denied">تم رفض التفويض</string>
<string name="text_plugin_activation_recommended">يوصى بالتفعيل</string>
<string name="text_plugin_activated">مفعّل</string>
<string name="text_plugin_trusted">موثوق</string>
<string name="plugin_item_info_mechanism">الالية</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">الاضافة \"%1$s\" غير مُمكّنة في مركز الاضافات</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">الاضافة \"%1$s\" غير مُفوّضة في مركز الاضافات</string>
<string name="error_plugin_not_found_in_installed_apps">لم يتم العثور على الاضافة \"%1$s\" ضمن التطبيقات المثبتة</string>
<string name="error_unsupported_plugin_connection">طريقة اتصال الاضافة غير مدعومة</string>
</resources>

View File

@@ -1335,4 +1335,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">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.</string>
<string name="dialog_button_activate">Activate</string>
<string name="text_activated_successfully">Activated successfully</string>
<string name="text_authorize_plugin">Authorize Plugin</string>
<string name="text_authorize_plugin_content">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.</string>
<string name="dialog_button_authorize">Authorize</string>
<string name="text_plugin_official">Official</string>
<string name="text_plugin_authorized">Authorized</string>
<string name="text_plugin_authorization_required">Authorization required</string>
<string name="text_plugin_authorization_denied">Authorization denied</string>
<string name="text_plugin_activation_recommended">Activation recommended</string>
<string name="text_plugin_activated">Activated</string>
<string name="text_plugin_trusted">Trusted</string>
<string name="plugin_item_info_mechanism">Mechanism</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">Plugin \"%1$s\" is not enabled in Plugin Center</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">Plugin \"%1$s\" is not authorized in Plugin Center</string>
<string name="error_plugin_not_found_in_installed_apps">Plugin \"%1$s\" not found in installed apps</string>
<string name="error_unsupported_plugin_connection">Unsupported plugin connection</string>
</resources>

View File

@@ -1338,4 +1338,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">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.</string>
<string name="dialog_button_activate">Activar</string>
<string name="text_activated_successfully">Activado correctamente</string>
<string name="text_authorize_plugin">Autorizar plugin</string>
<string name="text_authorize_plugin_content">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.</string>
<string name="dialog_button_authorize">Autorizar</string>
<string name="text_plugin_official">Oficial</string>
<string name="text_plugin_authorized">Autorizado</string>
<string name="text_plugin_authorization_required">Requiere autorizacion</string>
<string name="text_plugin_authorization_denied">Autorizacion denegada</string>
<string name="text_plugin_activation_recommended">Activacion recomendada</string>
<string name="text_plugin_activated">Activado</string>
<string name="text_plugin_trusted">Confiable</string>
<string name="plugin_item_info_mechanism">Mecanismo</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">El plugin \"%1$s\" no esta habilitado en el Centro de plugins</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">El plugin \"%1$s\" no esta autorizado en el Centro de plugins</string>
<string name="error_plugin_not_found_in_installed_apps">No se encontro el plugin \"%1$s\" en las apps instaladas</string>
<string name="error_unsupported_plugin_connection">Conexion de plugin no compatible</string>
</resources>

View File

@@ -1338,4 +1338,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">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.</string>
<string name="dialog_button_activate">Activer</string>
<string name="text_activated_successfully">Activation reussie</string>
<string name="text_authorize_plugin">Autoriser le plugin</string>
<string name="text_authorize_plugin_content">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.</string>
<string name="dialog_button_authorize">Autoriser</string>
<string name="text_plugin_official">Officiel</string>
<string name="text_plugin_authorized">Autorise</string>
<string name="text_plugin_authorization_required">Autorisation requise</string>
<string name="text_plugin_authorization_denied">Autorisation refusee</string>
<string name="text_plugin_activation_recommended">Activation recommandee</string>
<string name="text_plugin_activated">Active</string>
<string name="text_plugin_trusted">De confiance</string>
<string name="plugin_item_info_mechanism">Mecanisme</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">Le plugin \"%1$s\" n\'est pas active dans le Centre des plugins</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">Le plugin \"%1$s\" n\'est pas autorise dans le Centre des plugins</string>
<string name="error_plugin_not_found_in_installed_apps">Plugin \"%1$s\" introuvable parmi les applis installees</string>
<string name="error_unsupported_plugin_connection">Connexion de plugin non prise en charge</string>
</resources>

View File

@@ -1339,4 +1339,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"有効化\" ボタンをタップして, プラグインを一度有効化してみてください.\n有効化に成功すると, 一定時間内にボタンが自動的にオンになります.</string>
<string name="dialog_button_activate">有効化</string>
<string name="text_activated_successfully">有効化しました</string>
<string name="text_authorize_plugin">プラグインを許可</string>
<string name="text_authorize_plugin_content">これはサードパーティ製プラグインです. 提供元と利用上の安全性に注意してください.\n\n\"許可\" ボタンをタップしてプラグインを有効にします.</string>
<string name="dialog_button_authorize">許可</string>
<string name="text_plugin_official">公式</string>
<string name="text_plugin_authorized">許可済み</string>
<string name="text_plugin_authorization_required">許可が必要</string>
<string name="text_plugin_authorization_denied">許可が拒否されました</string>
<string name="text_plugin_activation_recommended">有効化を推奨</string>
<string name="text_plugin_activated">有効</string>
<string name="text_plugin_trusted">信頼済み</string>
<string name="plugin_item_info_mechanism">方式</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">プラグイン \"%1$s\" はプラグインセンターで有効になっていません</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">プラグイン \"%1$s\" はプラグインセンターで許可されていません</string>
<string name="error_plugin_not_found_in_installed_apps">インストール済みアプリにプラグイン \"%1$s\" が見つかりません</string>
<string name="error_unsupported_plugin_connection">サポートされていないプラグイン接続方式です</string>
</resources>

View File

@@ -1340,4 +1340,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"활성화\" 버튼을 눌러 플러그인을 한 번 활성화해 보세요.\n활성화에 성공하면 일정 시간 내에 버튼이 자동으로 켜집니다.</string>
<string name="dialog_button_activate">활성화</string>
<string name="text_activated_successfully">활성화되었습니다</string>
<string name="text_authorize_plugin">플러그인 권한 부여</string>
<string name="text_authorize_plugin_content">이 플러그인은 타사 플러그인입니다. 플러그인 출처와 사용 안전에 유의하세요.\n\n\"권한 부여\" 버튼을 눌러 플러그인을 활성화하세요.</string>
<string name="dialog_button_authorize">권한 부여</string>
<string name="text_plugin_official">공식</string>
<string name="text_plugin_authorized">권한 부여됨</string>
<string name="text_plugin_authorization_required">권한 필요</string>
<string name="text_plugin_authorization_denied">권한 거부됨</string>
<string name="text_plugin_activation_recommended">활성화 권장</string>
<string name="text_plugin_activated">활성화됨</string>
<string name="text_plugin_trusted">신뢰됨</string>
<string name="plugin_item_info_mechanism">방식</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">플러그인 \"%1$s\" 이(가) 플러그인 센터에서 활성화되어 있지 않습니다</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">플러그인 \"%1$s\" 이 (가) 플러그인 센터에서 권한이 부여되지 않았습니다</string>
<string name="error_plugin_not_found_in_installed_apps">설치된 앱에서 플러그인 \"%1$s\" 을 (를) 찾을 수 없습니다</string>
<string name="error_unsupported_plugin_connection">지원되지 않는 플러그인 연결 방식입니다</string>
</resources>

View File

@@ -1338,4 +1338,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">Попробуйте нажать кнопку \"Активировать\", чтобы активировать плагин один раз.\nЕсли активация пройдет успешно, кнопка автоматически включится в течение некоторого времени.</string>
<string name="dialog_button_activate">Активировать</string>
<string name="text_activated_successfully">Активация выполнена</string>
<string name="text_authorize_plugin">Авторизовать плагин</string>
<string name="text_authorize_plugin_content">Это сторонний плагин. Обратите внимание на источник плагина и безопасность использования.\n\nНажмите кнопку \"Авторизовать\", чтобы включить плагин.</string>
<string name="dialog_button_authorize">Авторизовать</string>
<string name="text_plugin_official">Официальный</string>
<string name="text_plugin_authorized">Авторизован</string>
<string name="text_plugin_authorization_required">Требуется авторизация</string>
<string name="text_plugin_authorization_denied">Авторизация отклонена</string>
<string name="text_plugin_activation_recommended">Рекомендуется активация</string>
<string name="text_plugin_activated">Активирован</string>
<string name="text_plugin_trusted">Доверенный</string>
<string name="plugin_item_info_mechanism">Механизм</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">Плагин \"%1$s\" не включен в Центре плагинов</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">Плагин \"%1$s\" не авторизован в Центре плагинов</string>
<string name="error_plugin_not_found_in_installed_apps">Плагин \"%1$s\" не найден среди установленных приложений</string>
<string name="error_unsupported_plugin_connection">Неподдерживаемое подключение плагина</string>
</resources>

View File

@@ -1334,4 +1334,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點擊 \"激活\" 按鈕激活一次插件.\n如激活成功, 按鈕將在一定時間內自動開啓.</string>
<string name="dialog_button_activate">激活</string>
<string name="text_activated_successfully">激活成功</string>
<string name="text_authorize_plugin">授權插件</string>
<string name="text_authorize_plugin_content">這是一個第三方插件, 請注意插件來源及使用安全.\n\n點擊 \"授權\" 按鈕以啓用插件.</string>
<string name="dialog_button_authorize">授權</string>
<string name="text_plugin_official">官方</string>
<string name="text_plugin_authorized">已授權</string>
<string name="text_plugin_authorization_required">需要授權</string>
<string name="text_plugin_authorization_denied">授權拒絕</string>
<string name="text_plugin_activation_recommended">建議激活</string>
<string name="text_plugin_activated">已激活</string>
<string name="text_plugin_trusted">受信任</string>
<string name="plugin_item_info_mechanism">機制</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">插件 \"%1$s\" 未在插件中心啓用</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">插件 \"%1$s\" 未在插件中心獲得授權</string>
<string name="error_plugin_not_found_in_installed_apps">已安裝應用中未找到插件 \"%1$s\"</string>
<string name="error_unsupported_plugin_connection">不支持的插件連接方式</string>
</resources>

View File

@@ -1334,4 +1334,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點選 \"啟用\" 按鈕啟用一次外掛.\n如啟用成功, 按鈕將在一定時間內自動開啟.</string>
<string name="dialog_button_activate">啟用</string>
<string name="text_activated_successfully">啟用成功</string>
<string name="text_authorize_plugin">授權外掛</string>
<string name="text_authorize_plugin_content">這是一個第三方外掛, 請注意外掛來源及使用安全.\n\n點選 \"授權\" 按鈕以啟用外掛.</string>
<string name="dialog_button_authorize">授權</string>
<string name="text_plugin_official">官方</string>
<string name="text_plugin_authorized">已授權</string>
<string name="text_plugin_authorization_required">需要授權</string>
<string name="text_plugin_authorization_denied">授權拒絕</string>
<string name="text_plugin_activation_recommended">建議啟用</string>
<string name="text_plugin_activated">已啟用</string>
<string name="text_plugin_trusted">受信任</string>
<string name="plugin_item_info_mechanism">機制</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">外掛 \"%1$s\" 未在外掛中心啟用</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">外掛 \"%1$s\" 未在外掛中心獲得授權</string>
<string name="error_plugin_not_found_in_installed_apps">已安裝應用中未找到外掛 \"%1$s\"</string>
<string name="error_unsupported_plugin_connection">不支援的外掛連線方式</string>
</resources>

View File

@@ -1335,4 +1335,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可尝试点击 \"激活\" 按钮激活一次插件.\n如激活成功, 按钮将在一定时间内自动开启.</string>
<string name="dialog_button_activate">激活</string>
<string name="text_activated_successfully">激活成功</string>
<string name="text_authorize_plugin">授权插件</string>
<string name="text_authorize_plugin_content">这是一个第三方插件, 请注意插件来源及使用安全.\n\n点击 \"授权\" 按钮以启用插件.</string>
<string name="dialog_button_authorize">授权</string>
<string name="text_plugin_official">官方</string>
<string name="text_plugin_authorized">已授权</string>
<string name="text_plugin_authorization_required">需要授权</string>
<string name="text_plugin_authorization_denied">授权拒绝</string>
<string name="text_plugin_activation_recommended">建议激活</string>
<string name="text_plugin_activated">已激活</string>
<string name="text_plugin_trusted">受信任</string>
<string name="plugin_item_info_mechanism">机制</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">插件 \"%1$s\" 未在插件中心启用</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">插件 \"%1$s\" 未在插件中心获得授权</string>
<string name="error_plugin_not_found_in_installed_apps">已安装应用中未找到插件 \"%1$s\"</string>
<string name="error_unsupported_plugin_connection">不支持的插件连接方式</string>
</resources>

View File

@@ -151,6 +151,7 @@
<string name="key_pref_bundle_default_item" translatable="false">key_$_pref_bundle_default_item</string>
<string name="key_pref_bundle_disabled_items" translatable="false">key_$_pref_bundle_disabled_items</string>
<string name="key_record_toast" translatable="false">key_$_record_toast</string>
<string name="key_release_history" translatable="false">key_$_release_history</string>
<string name="key_restart_strategy" translatable="false">key_$_restart_strategy</string>
<string name="key_restart_strategy_quick" translatable="false">key_$_restart_strategy_quick</string>
<string name="key_restart_strategy_scheduled" translatable="false">key_$_restart_strategy_scheduled</string>
@@ -184,7 +185,6 @@
<string name="key_updates_checked_states_cleared" translatable="false">key_$_updates_checked_states_cleared</string>
<string name="key_use_volume_control_record" translatable="false">key_$_use_volume_control_record</string>
<string name="key_use_volume_control_running" translatable="false">key_$_use_volume_control_running</string>
<string name="key_release_history" translatable="false">key_$_release_history</string>
<string name="key_version_history_restore_does_not_auto_save_to_disk" translatable="false">key_$_version_history_restore_does_not_auto_save_to_disk</string>
<string name="key_working_directory" translatable="false">key_$_working_directory</string>
<string name="key_working_directory_history" translatable="false">key_$_working_directory_histories</string>
@@ -251,6 +251,8 @@
<string name="text_full_version_info" translatable="false">%1$s (%2$d)</string>
<string name="text_half_ellipsis" translatable="false" formatted="true">...</string>
<string name="text_jks" translatable="false">JKS</string>
<string name="text_plugin_mechanism_aidl" translatable="false">AIDL / IPC</string>
<string name="text_plugin_mechanism_sdk" translatable="false">SDK / In-Process</string>
<string name="text_powered_by_autojs" translatable="false">Powered by AutoJs6</string>
<string name="text_root" translatable="false">Root</string>
<string name="text_sample_button" translatable="false">Hello</string>
@@ -1609,4 +1611,19 @@
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">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.</string>
<string name="dialog_button_activate">Activate</string>
<string name="text_activated_successfully">Activated successfully</string>
</resources>
<string name="text_authorize_plugin">Authorize Plugin</string>
<string name="text_authorize_plugin_content">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.</string>
<string name="dialog_button_authorize">Authorize</string>
<string name="text_plugin_official">Official</string>
<string name="text_plugin_authorized">Authorized</string>
<string name="text_plugin_authorization_required">Authorization required</string>
<string name="text_plugin_authorization_denied">Authorization denied</string>
<string name="text_plugin_activation_recommended">Activation recommended</string>
<string name="text_plugin_activated">Activated</string>
<string name="text_plugin_trusted">Trusted</string>
<string name="plugin_item_info_mechanism">Mechanism</string>
<string name="error_plugin_is_not_enabled_in_plugin_center">Plugin \"%1$s\" is not enabled in Plugin Center</string>
<string name="error_plugin_is_not_authorized_in_plugin_center">Plugin \"%1$s\" is not authorized in Plugin Center</string>
<string name="error_plugin_not_found_in_installed_apps">Plugin \"%1$s\" not found in installed apps</string>
<string name="error_unsupported_plugin_connection">Unsupported plugin connection</string>
</resources>