6.7.0 - Alpha22 - 插件中心 M3 - 插件中心支持传统 SDK 插件发现并增加信任机制和授权机制
This commit is contained in:
@@ -68,7 +68,7 @@ class InstalledPluginRepository {
|
|||||||
InstalledPlugin(
|
InstalledPlugin(
|
||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
title = d.pluginInfo?.name ?: appLabel ?: packageName,
|
title = d.pluginInfo?.name ?: appLabel ?: packageName,
|
||||||
description = d.pluginInfo?.description,
|
description = PluginDescriptionResolver.resolve(context, packageName, d.pluginInfo?.description),
|
||||||
author = d.pluginInfo?.author,
|
author = d.pluginInfo?.author,
|
||||||
versionName = versionName,
|
versionName = versionName,
|
||||||
versionCode = versionCode,
|
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 kotlinx.coroutines.launch
|
||||||
import org.autojs.autojs.ui.BaseActivity
|
import org.autojs.autojs.ui.BaseActivity
|
||||||
import org.autojs.autojs.ui.widget.SearchViewItem
|
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.DialogUtils.choiceWidgetThemeColor
|
||||||
|
import org.autojs.autojs.util.IntentUtils.startSafely
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
import org.autojs.autojs.util.ViewUtils.onceGlobalLayout
|
import org.autojs.autojs.util.ViewUtils.onceGlobalLayout
|
||||||
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
|
import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance
|
||||||
@@ -118,60 +118,46 @@ class PluginCenterActivity : BaseActivity() {
|
|||||||
private fun showSortDialog(center: PluginCenterFragment?) {
|
private fun showSortDialog(center: PluginCenterFragment?) {
|
||||||
if (center == null) return
|
if (center == null) return
|
||||||
|
|
||||||
|
val entries = PluginCenterFragment.Sort.values()
|
||||||
|
|
||||||
MaterialDialog.Builder(this)
|
MaterialDialog.Builder(this)
|
||||||
.title(R.string.text_sort)
|
.title(R.string.text_sort)
|
||||||
.items(
|
.items(entries.map { getString(it.titleRes) })
|
||||||
listOf(
|
.itemsCallbackSingleChoice(PluginSortStore.getSortOrdinal(this)) { d, _, which, _ ->
|
||||||
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, _ ->
|
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
when (which) {
|
val selectedSort = entries[which]
|
||||||
0 -> center.setSort(PluginCenterFragment.Sort.TITLE_ASC)
|
// center.setSort(selectedSort)
|
||||||
1 -> center.setSort(PluginCenterFragment.Sort.LAST_UPDATE_DESC)
|
PluginSortStore.setSort(this, selectedSort)
|
||||||
2 -> center.setSort(PluginCenterFragment.Sort.PACKAGE_SIZE_DESC)
|
true
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.choiceWidgetThemeColor()
|
.choiceWidgetThemeColor()
|
||||||
.negativeText(R.string.text_cancel)
|
.negativeText(R.string.dialog_button_cancel)
|
||||||
.negativeColorRes(R.color.dialog_button_default)
|
.negativeColorRes(R.color.dialog_button_default)
|
||||||
|
.positiveText(R.string.dialog_button_confirm)
|
||||||
|
.positiveColorRes(R.color.dialog_button_attraction)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFilterDialog(center: PluginCenterFragment?) {
|
private fun showFilterDialog(center: PluginCenterFragment?) {
|
||||||
if (center == null) return
|
if (center == null) return
|
||||||
|
|
||||||
|
val entries = PluginCenterFragment.Filter.values()
|
||||||
|
|
||||||
MaterialDialog.Builder(this)
|
MaterialDialog.Builder(this)
|
||||||
.title(R.string.text_filter)
|
.title(R.string.text_filter)
|
||||||
.items(
|
.items(entries.map { getString(it.titleRes) })
|
||||||
listOf(
|
.itemsCallbackSingleChoice(PluginFilterStore.getFilterOrdinal(this)) { d, _, which, _ ->
|
||||||
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, _ ->
|
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
when (which) {
|
val selectedFilter = entries[which]
|
||||||
0 -> center.setFilter(PluginCenterFragment.Filter.ALL)
|
// center.setFilter(selectedFilter)
|
||||||
1 -> center.setFilter(PluginCenterFragment.Filter.INSTALLED)
|
PluginFilterStore.setFilter(this, selectedFilter)
|
||||||
2 -> center.setFilter(PluginCenterFragment.Filter.NOT_INSTALLED)
|
true
|
||||||
3 -> center.setFilter(PluginCenterFragment.Filter.ENABLED)
|
|
||||||
4 -> center.setFilter(PluginCenterFragment.Filter.DISABLED)
|
|
||||||
5 -> center.setFilter(PluginCenterFragment.Filter.UPDATABLE)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.choiceWidgetThemeColor()
|
.choiceWidgetThemeColor()
|
||||||
.negativeText(R.string.text_cancel)
|
.negativeText(R.string.dialog_button_cancel)
|
||||||
.negativeColorRes(R.color.dialog_button_default)
|
.negativeColorRes(R.color.dialog_button_default)
|
||||||
|
.positiveText(R.string.dialog_button_confirm)
|
||||||
|
.positiveColorRes(R.color.dialog_button_attraction)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -56,8 +57,24 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
// zh-CN: 当前用于 UI 过滤的查询串, null 表示 "不做过滤".
|
// zh-CN: 当前用于 UI 过滤的查询串, null 表示 "不做过滤".
|
||||||
private var currentQuery: String? = null
|
private var currentQuery: String? = null
|
||||||
|
|
||||||
private var currentSort: Sort = Sort.TITLE_ASC
|
private lateinit var currentSort: Sort
|
||||||
private var currentFilter: Filter = Filter.ALL
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -65,75 +82,34 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
|
|
||||||
val context = requireContext().also { contextRef = it }
|
val context = requireContext().also { contextRef = it }
|
||||||
|
|
||||||
|
currentSort = PluginSortStore.getSort(context)
|
||||||
|
currentFilter = PluginFilterStore.getFilter(context)
|
||||||
|
|
||||||
adapter = PluginCenterItemAdapter(object : PluginCenterItemAdapter.Listener {
|
adapter = PluginCenterItemAdapter(object : PluginCenterItemAdapter.Listener {
|
||||||
override fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) {
|
override fun onToggleEnable(item: PluginCenterItem, enabled: Boolean) {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
vm.setEnabled(contextRef, item.packageName, false)
|
vm.setEnabled(contextRef, item.packageName, false)
|
||||||
item.isEnabled = false
|
item.isEnabled = false
|
||||||
|
item.enabledState = PluginEnabledState.DISABLED
|
||||||
|
item.lastError = null
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.setEnabled(contextRef, item.packageName, true)
|
if (!item.isInstalled) {
|
||||||
item.isEnabled = true
|
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 {
|
ensureAuthorized(item) { authorizedItem ->
|
||||||
val error = runCatching {
|
when (authorizedItem.mechanism) {
|
||||||
PaddleOcrPluginHost.probe(contextRef, item.packageName)
|
PluginMechanism.SDK -> enableLegacyPlugin(authorizedItem)
|
||||||
}.exceptionOrNull()
|
PluginMechanism.AIDL -> enableAidlPlugin(authorizedItem)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,6 +245,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
Filter.ENABLED -> item.isEnabled
|
Filter.ENABLED -> item.isEnabled
|
||||||
Filter.DISABLED -> !item.isEnabled
|
Filter.DISABLED -> !item.isEnabled
|
||||||
Filter.UPDATABLE -> item.updatableVersionCode != null
|
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) {
|
private fun tryEnableAfterWake(item: PluginCenterItem) {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val delays = longArrayOf(300L, 800L, 1500L)
|
val delays = longArrayOf(300L, 800L, 1500L)
|
||||||
@@ -376,6 +509,8 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
if (isAdded && _binding != null) {
|
if (isAdded && _binding != null) {
|
||||||
vm.setEnabled(contextRef, item.packageName, true)
|
vm.setEnabled(contextRef, item.packageName, true)
|
||||||
item.isEnabled = true
|
item.isEnabled = true
|
||||||
|
item.enabledState = PluginEnabledState.READY
|
||||||
|
item.lastError = null
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
@@ -389,6 +524,16 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.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 ?: run registerPackageReceiver@{
|
||||||
pkgReceiver = object : BroadcastReceiver() {
|
pkgReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@@ -449,6 +594,10 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
if (::contextRef.isInitialized) {
|
||||||
|
PluginSortStore.unregisterOnSharedPreferenceChangeListener(contextRef, sortPrefListener)
|
||||||
|
PluginFilterStore.unregisterOnSharedPreferenceChangeListener(contextRef, filterPrefListener)
|
||||||
|
}
|
||||||
pkgChangeRefreshJob?.cancel()
|
pkgChangeRefreshJob?.cancel()
|
||||||
pkgChangeRefreshJob = null
|
pkgChangeRefreshJob = null
|
||||||
pkgReceiver?.let { runCatching { requireContext().unregisterReceiver(it) } }
|
pkgReceiver?.let { runCatching { requireContext().unregisterReceiver(it) } }
|
||||||
@@ -475,21 +624,23 @@ class PluginCenterFragment : Fragment(R.layout.fragment_plugin_center) {
|
|||||||
|
|
||||||
// Sort strategy for rendering list.
|
// Sort strategy for rendering list.
|
||||||
// zh-CN: 用于渲染列表的排序策略.
|
// zh-CN: 用于渲染列表的排序策略.
|
||||||
enum class Sort {
|
enum class Sort(val titleRes: Int) {
|
||||||
TITLE_ASC,
|
TITLE_ASC(R.string.text_sort_by_name),
|
||||||
LAST_UPDATE_DESC,
|
LAST_UPDATE_DESC(R.string.text_sort_by_last_update_time),
|
||||||
PACKAGE_SIZE_DESC,
|
PACKAGE_SIZE_DESC(R.string.text_sort_by_package_size),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter strategy for rendering list.
|
// Filter strategy for rendering list.
|
||||||
// zh-CN: 用于渲染列表的筛选策略.
|
// zh-CN: 用于渲染列表的筛选策略.
|
||||||
enum class Filter {
|
enum class Filter(val titleRes: Int) {
|
||||||
ALL,
|
ALL(R.string.text_all),
|
||||||
INSTALLED,
|
INSTALLED(R.string.text_installed),
|
||||||
NOT_INSTALLED,
|
NOT_INSTALLED(R.string.text_not_installed),
|
||||||
ENABLED,
|
ENABLED(R.string.text_enabled),
|
||||||
DISABLED,
|
DISABLED(R.string.text_disabled),
|
||||||
UPDATABLE,
|
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 firstInstallTime: Long? = null,
|
||||||
val lastUpdateTime: Long? = null,
|
val lastUpdateTime: Long? = null,
|
||||||
val settings: PluginCenterItemSettings? = 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
|
val versionSummary: String
|
||||||
get() = formatVersionInfo(versionName, versionCode, versionDate)
|
get() = formatVersionInfo(versionName, versionCode, versionDate)
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import de.hdodenhof.circleimageview.CircleImageView
|
import de.hdodenhof.circleimageview.CircleImageView
|
||||||
import org.autojs.autojs.theme.ThemeColorManager
|
import org.autojs.autojs.theme.ThemeColorManager
|
||||||
import org.autojs.autojs.util.ColorUtils
|
import org.autojs.autojs.util.ColorUtils
|
||||||
|
import org.autojs.autojs.util.IntentUtils
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
|
import org.autojs.autojs.util.ViewUtils.colorFilterWithDesaturateOrNull
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding
|
import org.autojs.autojs6.databinding.PluginCenterRecyclerViewItemBinding
|
||||||
|
|
||||||
class PluginCenterItemViewHolder(
|
class PluginCenterItemViewHolder(
|
||||||
itemViewBinding: PluginCenterRecyclerViewItemBinding,
|
private val itemViewBinding: PluginCenterRecyclerViewItemBinding,
|
||||||
private val listener: PluginCenterItemAdapter.Listener,
|
private val listener: PluginCenterItemAdapter.Listener,
|
||||||
) : RecyclerView.ViewHolder(itemViewBinding.root) {
|
) : RecyclerView.ViewHolder(itemViewBinding.root) {
|
||||||
|
|
||||||
@@ -65,6 +66,12 @@ class PluginCenterItemViewHolder(
|
|||||||
iconView.setImageDrawable(d)
|
iconView.setImageDrawable(d)
|
||||||
} ?: iconView.setImageResource(R.mipmap.ic_app_shortcut_plugin_center_adaptive_round)
|
} ?: 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.setOnCheckedChangeListener(null)
|
||||||
switchView.isChecked = item.isEnabled
|
switchView.isChecked = item.isEnabled
|
||||||
|
|
||||||
@@ -109,6 +116,14 @@ class PluginCenterItemViewHolder(
|
|||||||
listener.onDetails(currentItem)
|
listener.onDetails(currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
itemViewBinding.title,
|
||||||
|
itemViewBinding.itemMiddleArea,
|
||||||
|
itemViewBinding.description,
|
||||||
|
).forEach {
|
||||||
|
it.setOnClickListener { listener.onDetails(currentItem) }
|
||||||
|
}
|
||||||
|
|
||||||
applyUiBySwitch(switchView.isChecked, item)
|
applyUiBySwitch(switchView.isChecked, item)
|
||||||
|
|
||||||
switchView.setOnCheckedChangeListener { _, isChecked ->
|
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 indexRepo = PluginIndexRepository()
|
||||||
|
|
||||||
private val installedRepo = InstalledPluginRepository()
|
private val installedRepo = InstalledPluginRepository()
|
||||||
|
private val legacyRepo = LegacyInstalledPluginRepository()
|
||||||
private val enableStore = PluginEnableStore
|
private val enableStore = PluginEnableStore
|
||||||
|
|
||||||
private val _items = MutableStateFlow<List<PluginCenterItem>>(emptyList())
|
private val _items = MutableStateFlow<List<PluginCenterItem>>(emptyList())
|
||||||
@@ -93,6 +94,12 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val legacyInstalled = runCatching {
|
||||||
|
legacyRepo.discoverInstalled(context)
|
||||||
|
}.onFailure { e ->
|
||||||
|
Log.w(TAG, "legacy discovery failed: ${e.message}")
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
|
||||||
installed.forEach { local ->
|
installed.forEach { local ->
|
||||||
if (local.bindError != null) {
|
if (local.bindError != null) {
|
||||||
enableStore.setEnabled(context, local.packageName, false)
|
enableStore.setEnabled(context, local.packageName, false)
|
||||||
@@ -104,9 +111,13 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
|
|
||||||
// Render list using "local only".
|
// Render list using "local only".
|
||||||
// zh-CN: 使用 "仅本地" 渲染列表.
|
// zh-CN: 使用 "仅本地" 渲染列表.
|
||||||
val onlyLocalItems = installed.mapNotNull { local ->
|
val localAidlItems = installed.mapNotNull { local ->
|
||||||
toPluginCenterItem(context, index = null, local = local)
|
toPluginCenterItem(context, index = null, local = local)
|
||||||
}
|
}
|
||||||
|
val localLegacyItems = legacyInstalled.mapNotNull { local ->
|
||||||
|
toLegacyPluginCenterItem(context, local)
|
||||||
|
}
|
||||||
|
val onlyLocalItems = localAidlItems + localLegacyItems
|
||||||
_items.value = onlyLocalItems
|
_items.value = onlyLocalItems
|
||||||
|
|
||||||
// Asynchronously load index and merge.
|
// 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).
|
// Supplement plugins that "exist locally but not in index" (third-party/not yet in index).
|
||||||
// zh-CN: 补充 "本地有但索引没有" 的插件 (第三方/暂未入索引).
|
// zh-CN: 补充 "本地有但索引没有" 的插件 (第三方/暂未入索引).
|
||||||
val extraLocals = installed
|
val extraAidlLocals = installed
|
||||||
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
|
.filter { ins -> indexEntries.none { it.packageName == ins.packageName } }
|
||||||
.mapNotNull { local -> toPluginCenterItem(context, index = null, local = local) }
|
.mapNotNull { local -> toPluginCenterItem(context, index = null, local = local) }
|
||||||
|
val extraLocals = extraAidlLocals + localLegacyItems
|
||||||
|
|
||||||
_items.value = fromIndex + extraLocals
|
_items.value = fromIndex + extraLocals
|
||||||
_indexLoaded.value = true
|
_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)
|
enableStore.setEnabled(context, packageName, enabled)
|
||||||
_items.value = _items.value.map {
|
_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)
|
PluginInfoDialogManager.refreshIfShowing(context, _items.value)
|
||||||
}
|
}
|
||||||
@@ -184,7 +207,41 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
candidates.firstOrNull { !UpdateIgnoreStore.isIgnored(packageName, it.versionCode) }
|
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(
|
return PluginCenterItem(
|
||||||
title = title,
|
title = title,
|
||||||
@@ -202,7 +259,7 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
updatableChangelogUrl = targetUpdate?.changelogUrl,
|
updatableChangelogUrl = targetUpdate?.changelogUrl,
|
||||||
updatableChangelogText = targetUpdate?.changelogText,
|
updatableChangelogText = targetUpdate?.changelogText,
|
||||||
|
|
||||||
author = author,
|
author = author ?: trustInfo.developer,
|
||||||
collaborators = collaborators,
|
collaborators = collaborators,
|
||||||
description = description,
|
description = description,
|
||||||
|
|
||||||
@@ -214,12 +271,86 @@ class PluginCenterViewModel : ViewModel() {
|
|||||||
|
|
||||||
// TODO 已安装优先用应用图标; 未安装走默认占位图.
|
// TODO 已安装优先用应用图标; 未安装走默认占位图.
|
||||||
icon = local?.icon,
|
icon = local?.icon,
|
||||||
isEnabled = enabled,
|
isEnabled = readyEnabled,
|
||||||
isInstalled = isInstalled,
|
isInstalled = isInstalled,
|
||||||
firstInstallTime = local?.firstInstallTime,
|
firstInstallTime = local?.firstInstallTime,
|
||||||
lastUpdateTime = local?.lastUpdateTime,
|
lastUpdateTime = local?.lastUpdateTime,
|
||||||
// TODO M1 暂不接入单插件设置入口.
|
// TODO M1 暂不接入单插件设置入口.
|
||||||
settings = null,
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.runtime.api.augment.converter.core.Bytes
|
||||||
import org.autojs.autojs.theme.ThemeColorManager
|
import org.autojs.autojs.theme.ThemeColorManager
|
||||||
import org.autojs.autojs.util.ColorUtils
|
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.DisplayUtils
|
||||||
import org.autojs.autojs.util.TimeUtils
|
import org.autojs.autojs.util.TimeUtils
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
@@ -68,11 +68,11 @@ object PluginInfoDialogManager {
|
|||||||
lastUninstallTime = item.lastUninstallTime,
|
lastUninstallTime = item.lastUninstallTime,
|
||||||
)
|
)
|
||||||
showPluginInfoDialogInternal(context, info) {
|
showPluginInfoDialogInternal(context, info) {
|
||||||
positiveText(R.string.text_install)
|
neutralText(R.string.text_install)
|
||||||
positiveColorRes(R.color.dialog_button_attraction)
|
neutralColorRes(R.color.dialog_button_attraction)
|
||||||
onPositive { d, _ ->
|
onNeutral { d, _ ->
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
val url = info.validateApkUrlAndPrompt(context, d) ?: return@onPositive
|
val url = info.validateApkUrlAndPrompt(context, d) ?: return@onNeutral
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
|
PluginInstaller.installFromUrlWithPrompt(context, url, info.sha256)
|
||||||
}
|
}
|
||||||
@@ -81,21 +81,14 @@ object PluginInfoDialogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showInstalledPluginInfoDialog(context: Context, item: PluginCenterItem) {
|
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(
|
val info = PluginInfoInstalled(
|
||||||
item = item,
|
item = item,
|
||||||
states = states,
|
states = parseStates(context, item),
|
||||||
updatableVersion = item.updatableVersionSummary,
|
updatableVersion = item.updatableVersionSummary,
|
||||||
firstInstallTime = item.firstInstallTime,
|
firstInstallTime = item.firstInstallTime,
|
||||||
lastUpdateTime = item.lastUpdateTime,
|
lastUpdateTime = item.lastUpdateTime,
|
||||||
)
|
)
|
||||||
showPluginInfoDialogInternal(context, info) {
|
showPluginInfoDialogInternal(context, info) {
|
||||||
positiveText(R.string.text_uninstall)
|
|
||||||
positiveColorRes(R.color.dialog_button_warn)
|
|
||||||
onPositive { d, _ -> item.uninstallWithPrompt(context, d) }
|
|
||||||
if (item.isUpdatable) {
|
if (item.isUpdatable) {
|
||||||
neutralText(R.string.dialog_button_view_update)
|
neutralText(R.string.dialog_button_view_update)
|
||||||
neutralColorRes(R.color.dialog_button_attraction)
|
neutralColorRes(R.color.dialog_button_attraction)
|
||||||
@@ -137,15 +130,32 @@ object PluginInfoDialogManager {
|
|||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
binding.stateValueFirst.text = info.states[0]
|
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.stateValueSecond.text = info.states[1]
|
||||||
|
|
||||||
|
binding.stateValueFirst.isVisible = true
|
||||||
binding.stateSpliterFirstSecond.isVisible = true
|
binding.stateSpliterFirstSecond.isVisible = true
|
||||||
binding.stateValueSecond.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.packageNameValue, info.packageName)
|
||||||
|
dialog.setCopyableTextIfAbsent(binding.mechanismValue, info.mechanism)
|
||||||
dialog.setCopyableTextIfAbsent(binding.versionValue, info.version)
|
dialog.setCopyableTextIfAbsent(binding.versionValue, info.version)
|
||||||
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoAuthorValue, info.author)
|
dialog.setCopyableTextIfAbsent(binding.pluginItemInfoAuthorValue, info.author)
|
||||||
dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description)
|
dialog.setCopyableTextIfAbsent(binding.descriptionValue, info.description)
|
||||||
@@ -237,6 +247,42 @@ object PluginInfoDialogManager {
|
|||||||
.cancelable(false)
|
.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? {
|
private fun PluginInfoBase.validateApkUrlAndPrompt(context: Context, parentDialog: MaterialDialog?): String? {
|
||||||
val url = this.apkUrl
|
val url = this.apkUrl
|
||||||
return when {
|
return when {
|
||||||
@@ -295,6 +341,7 @@ object PluginInfoDialogManager {
|
|||||||
private fun updateGuidelines(binding: PluginInfoDialogItemsBinding) {
|
private fun updateGuidelines(binding: PluginInfoDialogItemsBinding) {
|
||||||
val filteredBindings = listOf(
|
val filteredBindings = listOf(
|
||||||
binding.stateLabel to binding.stateGuideline,
|
binding.stateLabel to binding.stateGuideline,
|
||||||
|
binding.mechanismLabel to binding.mechanismGuideline,
|
||||||
binding.packageNameLabel to binding.packageNameGuideline,
|
binding.packageNameLabel to binding.packageNameGuideline,
|
||||||
binding.versionLabel to binding.versionGuideline,
|
binding.versionLabel to binding.versionGuideline,
|
||||||
binding.updatableVersionLabel to binding.updatableVersionGuideline,
|
binding.updatableVersionLabel to binding.updatableVersionGuideline,
|
||||||
@@ -345,6 +392,7 @@ object PluginInfoDialogManager {
|
|||||||
val title: String get() = item.title
|
val title: String get() = item.title
|
||||||
val icon: Drawable? get() = item.icon
|
val icon: Drawable? get() = item.icon
|
||||||
val packageName: String get() = item.packageName
|
val packageName: String get() = item.packageName
|
||||||
|
val mechanism: String get() = item.mechanism.displayName
|
||||||
val version: String? get() = item.versionSummary
|
val version: String? get() = item.versionSummary
|
||||||
val author: String? get() = item.author
|
val author: String? get() = item.author
|
||||||
val collaborators: List<String> get() = item.collaborators
|
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.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import org.autojs.autojs.core.plugin.center.PluginEnableStore
|
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.IOcrPlugin
|
||||||
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
import org.autojs.plugin.paddle.ocr.api.OcrOptions
|
||||||
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
import org.autojs.plugin.paddle.ocr.api.OcrResult
|
||||||
@@ -85,6 +86,9 @@ object PaddleOcrPluginHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun probe(context: Context, packageName: String): PluginInfo {
|
suspend fun probe(context: Context, packageName: String): PluginInfo {
|
||||||
|
if (!PluginTrustManager.isAuthorized(context, packageName)) {
|
||||||
|
error("Plugin not authorized: $packageName")
|
||||||
|
}
|
||||||
val serviceInfo = queryOcrServices(context, packageName).firstOrNull()
|
val serviceInfo = queryOcrServices(context, packageName).firstOrNull()
|
||||||
?: error("No OCR service found for package: $packageName")
|
?: error("No OCR service found for package: $packageName")
|
||||||
return withService(context, serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { it.getInfo() }
|
return withService(context, serviceInfo, DEFAULT_BIND_TIMEOUT_MS) { it.getInfo() }
|
||||||
@@ -97,6 +101,9 @@ object PaddleOcrPluginHost {
|
|||||||
options: OcrOptions = OcrOptions(),
|
options: OcrOptions = OcrOptions(),
|
||||||
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
||||||
): List<String> {
|
): List<String> {
|
||||||
|
if (!PluginTrustManager.isAuthorized(context, target.serviceInfo.packageName)) {
|
||||||
|
error("Plugin not authorized: ${target.serviceInfo.packageName}")
|
||||||
|
}
|
||||||
ensureRawSupport(target, options)
|
ensureRawSupport(target, options)
|
||||||
val start = uptimeMillis()
|
val start = uptimeMillis()
|
||||||
return createTempPfd(bitmap, options).use { pfd ->
|
return createTempPfd(bitmap, options).use { pfd ->
|
||||||
@@ -117,6 +124,9 @@ object PaddleOcrPluginHost {
|
|||||||
options: OcrOptions = OcrOptions(),
|
options: OcrOptions = OcrOptions(),
|
||||||
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
callTimeoutMs: Long = DEFAULT_CALL_TIMEOUT_MS,
|
||||||
): List<OcrResult> {
|
): List<OcrResult> {
|
||||||
|
if (!PluginTrustManager.isAuthorized(context, target.serviceInfo.packageName)) {
|
||||||
|
error("Plugin not authorized: ${target.serviceInfo.packageName}")
|
||||||
|
}
|
||||||
ensureRawSupport(target, options)
|
ensureRawSupport(target, options)
|
||||||
val start = uptimeMillis()
|
val start = uptimeMillis()
|
||||||
return createTempPfd(bitmap, options).use { pfd ->
|
return createTempPfd(bitmap, options).use { pfd ->
|
||||||
@@ -142,6 +152,7 @@ object PaddleOcrPluginHost {
|
|||||||
val list = discover(context)
|
val list = discover(context)
|
||||||
.filter { it.pluginInfo != null }
|
.filter { it.pluginInfo != null }
|
||||||
.filter { PluginEnableStore.isEnabled(context, it.serviceInfo.packageName, true) }
|
.filter { PluginEnableStore.isEnabled(context, it.serviceInfo.packageName, true) }
|
||||||
|
.filter { PluginTrustManager.isAuthorized(context, it.serviceInfo.packageName) }
|
||||||
if (list.isEmpty()) return null
|
if (list.isEmpty()) return null
|
||||||
if (engineId != null) {
|
if (engineId != null) {
|
||||||
list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it }
|
list.firstOrNull { d -> d.pluginInfo?.id == engineId }?.let { return it }
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import android.os.Parcel
|
|||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import org.autojs.autojs.core.plugin.Plugin
|
import org.autojs.autojs.core.plugin.Plugin
|
||||||
import org.autojs.autojs.core.plugin.Plugin.PluginLoadException
|
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.execution.ExecutionConfig
|
||||||
import org.autojs.autojs.pio.PFiles.copyAssetDir
|
import org.autojs.autojs.pio.PFiles.copyAssetDir
|
||||||
import org.autojs.autojs.pio.PFiles.deleteRecursively
|
import org.autojs.autojs.pio.PFiles.deleteRecursively
|
||||||
import org.autojs.autojs.rhino.TopLevelScope
|
import org.autojs.autojs.rhino.TopLevelScope
|
||||||
|
import org.autojs.autojs6.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@@ -32,9 +35,16 @@ class Plugins(private val context: Context, private val runtime: PluginRuntime)
|
|||||||
fun load(packageName: String): Plugin {
|
fun load(packageName: String): Plugin {
|
||||||
mPlugins[packageName]?.let { return it }
|
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(
|
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 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 }
|
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
|
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?> {
|
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:textColor="#03A5EF"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:text="@string/text_updatable" />
|
android:text="@string/text_updatable" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -85,6 +83,7 @@
|
|||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/item_middle_area"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -121,7 +120,6 @@
|
|||||||
android:textColor="@color/text_color_primary"
|
android:textColor="@color/text_color_primary"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
tools:text="SuperMonster003" />
|
tools:text="SuperMonster003" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
@@ -132,26 +130,34 @@
|
|||||||
<org.autojs.autojs.theme.widget.ThemeColorSwitch
|
<org.autojs.autojs.theme.widget.ThemeColorSwitch
|
||||||
android:id="@+id/sw"
|
android:id="@+id/sw"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/description"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:orientation="horizontal">
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:textColor="@color/text_color_primary_alpha_70"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:text="百度飞桨光学字符识别插件" />
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -196,7 +202,6 @@
|
|||||||
android:textColor="@color/text_color_primary"
|
android:textColor="@color/text_color_primary"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:translationY="-0.5dp" />
|
android:translationY="-0.5dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -236,7 +241,6 @@
|
|||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:translationY="-0.5dp"
|
android:translationY="-0.5dp"
|
||||||
tools:textColor="#03A5EF" />
|
tools:textColor="#03A5EF" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -273,7 +277,6 @@
|
|||||||
android:textColor="@color/text_color_primary_alpha_30"
|
android:textColor="@color/text_color_primary_alpha_30"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:translationY="-0.5dp" />
|
android:translationY="-0.5dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -309,9 +312,6 @@
|
|||||||
android:textColor="@color/text_color_primary"
|
android:textColor="@color/text_color_primary"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:translationY="-0.5dp" />
|
android:translationY="-0.5dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
|
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
@@ -68,7 +68,9 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/state_value_first"
|
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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
@@ -107,6 +109,45 @@
|
|||||||
android:id="@+id/state_value_second"
|
android:id="@+id/state_value_second"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible"
|
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"
|
tools:text="@string/text_updatable"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -120,7 +161,7 @@
|
|||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
android:lineSpacingExtra="2dp"
|
android:lineSpacingExtra="2dp"
|
||||||
android:textSize="@dimen/ref_md_listitem_textsize"
|
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_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
@@ -129,6 +170,73 @@
|
|||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</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
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/package_name_parent"
|
android:id="@+id/package_name_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -137,7 +245,7 @@
|
|||||||
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
|
android:layout_marginStart="@dimen/ref_md_listitem_margin_left"
|
||||||
android:layout_marginTop="4sp"
|
android:layout_marginTop="4sp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
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_constraintBottom_toTopOf="@id/version_parent"
|
||||||
app:layout_constraintEnd_toEndOf="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="hint_try_clicking_the_activate_button_to_activate_the_plugin">جرّب الضغط على زر \"تفعيل\" لتفعيل الاضافة مرة واحدة.\nاذا نجح التفعيل، فسيتم تشغيل الزر تلقائيا خلال فترة زمنية معينة.</string>
|
||||||
<string name="dialog_button_activate">تفعيل</string>
|
<string name="dialog_button_activate">تفعيل</string>
|
||||||
<string name="text_activated_successfully">تم التفعيل بنجاح</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>
|
</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="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="dialog_button_activate">Activate</string>
|
||||||
<string name="text_activated_successfully">Activated successfully</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>
|
</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="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="dialog_button_activate">Activar</string>
|
||||||
<string name="text_activated_successfully">Activado correctamente</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>
|
</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="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="dialog_button_activate">Activer</string>
|
||||||
<string name="text_activated_successfully">Activation reussie</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>
|
</resources>
|
||||||
@@ -1339,4 +1339,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"有効化\" ボタンをタップして, プラグインを一度有効化してみてください.\n有効化に成功すると, 一定時間内にボタンが自動的にオンになります.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"有効化\" ボタンをタップして, プラグインを一度有効化してみてください.\n有効化に成功すると, 一定時間内にボタンが自動的にオンになります.</string>
|
||||||
<string name="dialog_button_activate">有効化</string>
|
<string name="dialog_button_activate">有効化</string>
|
||||||
<string name="text_activated_successfully">有効化しました</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>
|
</resources>
|
||||||
@@ -1340,4 +1340,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"활성화\" 버튼을 눌러 플러그인을 한 번 활성화해 보세요.\n활성화에 성공하면 일정 시간 내에 버튼이 자동으로 켜집니다.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">\"활성화\" 버튼을 눌러 플러그인을 한 번 활성화해 보세요.\n활성화에 성공하면 일정 시간 내에 버튼이 자동으로 켜집니다.</string>
|
||||||
<string name="dialog_button_activate">활성화</string>
|
<string name="dialog_button_activate">활성화</string>
|
||||||
<string name="text_activated_successfully">활성화되었습니다</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>
|
</resources>
|
||||||
@@ -1338,4 +1338,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">Попробуйте нажать кнопку \"Активировать\", чтобы активировать плагин один раз.\nЕсли активация пройдет успешно, кнопка автоматически включится в течение некоторого времени.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">Попробуйте нажать кнопку \"Активировать\", чтобы активировать плагин один раз.\nЕсли активация пройдет успешно, кнопка автоматически включится в течение некоторого времени.</string>
|
||||||
<string name="dialog_button_activate">Активировать</string>
|
<string name="dialog_button_activate">Активировать</string>
|
||||||
<string name="text_activated_successfully">Активация выполнена</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>
|
</resources>
|
||||||
@@ -1334,4 +1334,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點擊 \"激活\" 按鈕激活一次插件.\n如激活成功, 按鈕將在一定時間內自動開啓.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點擊 \"激活\" 按鈕激活一次插件.\n如激活成功, 按鈕將在一定時間內自動開啓.</string>
|
||||||
<string name="dialog_button_activate">激活</string>
|
<string name="dialog_button_activate">激活</string>
|
||||||
<string name="text_activated_successfully">激活成功</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>
|
</resources>
|
||||||
@@ -1334,4 +1334,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點選 \"啟用\" 按鈕啟用一次外掛.\n如啟用成功, 按鈕將在一定時間內自動開啟.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可嘗試點選 \"啟用\" 按鈕啟用一次外掛.\n如啟用成功, 按鈕將在一定時間內自動開啟.</string>
|
||||||
<string name="dialog_button_activate">啟用</string>
|
<string name="dialog_button_activate">啟用</string>
|
||||||
<string name="text_activated_successfully">啟用成功</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>
|
</resources>
|
||||||
@@ -1335,4 +1335,19 @@
|
|||||||
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可尝试点击 \"激活\" 按钮激活一次插件.\n如激活成功, 按钮将在一定时间内自动开启.</string>
|
<string name="hint_try_clicking_the_activate_button_to_activate_the_plugin">可尝试点击 \"激活\" 按钮激活一次插件.\n如激活成功, 按钮将在一定时间内自动开启.</string>
|
||||||
<string name="dialog_button_activate">激活</string>
|
<string name="dialog_button_activate">激活</string>
|
||||||
<string name="text_activated_successfully">激活成功</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>
|
</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_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_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_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" translatable="false">key_$_restart_strategy</string>
|
||||||
<string name="key_restart_strategy_quick" translatable="false">key_$_restart_strategy_quick</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>
|
<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_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_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_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_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" translatable="false">key_$_working_directory</string>
|
||||||
<string name="key_working_directory_history" translatable="false">key_$_working_directory_histories</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_full_version_info" translatable="false">%1$s (%2$d)</string>
|
||||||
<string name="text_half_ellipsis" translatable="false" formatted="true">...</string>
|
<string name="text_half_ellipsis" translatable="false" formatted="true">...</string>
|
||||||
<string name="text_jks" translatable="false">JKS</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_powered_by_autojs" translatable="false">Powered by AutoJs6</string>
|
||||||
<string name="text_root" translatable="false">Root</string>
|
<string name="text_root" translatable="false">Root</string>
|
||||||
<string name="text_sample_button" translatable="false">Hello</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="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="dialog_button_activate">Activate</string>
|
||||||
<string name="text_activated_successfully">Activated successfully</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>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Wed Feb 25 16:44:22 CST 2026
|
#Sun Mar 01 17:58:13 CST 2026
|
||||||
BUILD_TIME=1772009062191
|
BUILD_TIME=1772359093087
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
|||||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3753
|
VERSION_BUILD=3761
|
||||||
VERSION_NAME=6.7.0 Alpha21
|
VERSION_NAME=6.7.0 Alpha22
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||||
|
|||||||
Reference in New Issue
Block a user