6.7.0 - Alpha22 - 插件中心 M3 - 插件中心支持传统 SDK 插件发现并增加信任机制和授权机制
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user