Commit 14e19656 authored by Mygod's avatar Mygod

Add preliminary support for plugin aliases

parent 14bdafd0
...@@ -24,9 +24,17 @@ import android.graphics.drawable.Drawable ...@@ -24,9 +24,17 @@ import android.graphics.drawable.Drawable
abstract class Plugin { abstract class Plugin {
abstract val id: String abstract val id: String
open val idAliases get() = emptyArray<String>()
abstract val label: CharSequence abstract val label: CharSequence
open val icon: Drawable? get() = null open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null open val defaultConfig: String? get() = null
open val packageName: String get() = "" open val packageName: String get() = ""
open val trusted: Boolean get() = true open val trusted: Boolean get() = true
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return id == (other as Plugin).id
}
override fun hashCode() = id.hashCode()
} }
...@@ -51,7 +51,7 @@ class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val se ...@@ -51,7 +51,7 @@ class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val se
}) })
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig) pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins().lookup[id]?.defaultConfig)
val selectedOptions: PluginOptions get() = getOptions(selected) val selectedOptions: PluginOptions get() = getOptions(selected)
override fun toString(): String { override fun toString(): String {
......
/*******************************************************************************
* *
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.content.Intent
import android.content.pm.PackageManager
import com.github.shadowsocks.Core.app
class PluginList : ArrayList<Plugin>() {
init {
add(NoPlugin)
addAll(app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA).map { NativePlugin(it) })
}
val lookup = flatMap { plugin ->
sequence {
yield(plugin.id to plugin)
for (alias in plugin.idAliases) yield(alias to plugin)
}.asIterable()
}.associate { it }
}
...@@ -102,19 +102,15 @@ object PluginManager { ...@@ -102,19 +102,15 @@ object PluginManager {
} }
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null private var cachedPlugins: PluginList? = null
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) { fun fetchPlugins() = synchronized(this) {
if (receiver == null) receiver = app.listenForPackageChanges { if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(this) { synchronized(this) {
receiver = null receiver = null
cachedPlugins = null cachedPlugins = null
} }
} }
if (cachedPlugins == null) { if (cachedPlugins == null) cachedPlugins = PluginList()
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associateBy { it.id }
}
cachedPlugins!! cachedPlugins!!
} }
......
...@@ -33,6 +33,17 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() ...@@ -33,6 +33,17 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
private val resources by lazy { app.packageManager.getResourcesForApplication(componentInfo.applicationInfo) } private val resources by lazy { app.packageManager.getResourcesForApplication(componentInfo.applicationInfo) }
override val id by lazy { componentInfo.metaData.loadString(PluginContract.METADATA_KEY_ID) { resources }!! } override val id by lazy { componentInfo.metaData.loadString(PluginContract.METADATA_KEY_ID) { resources }!! }
override val idAliases: Array<String> by lazy {
when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) {
is String -> arrayOf(value)
is Int -> when (resources.getResourceTypeName(value)) {
"string" -> arrayOf(resources.getString(value))
else -> resources.getStringArray(value)
}
null -> emptyArray()
else -> error("unknown type for plugin meta-data idAliases")
}
}
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) } override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) }
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) } override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) }
override val defaultConfig by lazy { override val defaultConfig by lazy {
......
...@@ -52,6 +52,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -52,6 +52,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2 const val REQUEST_UNSAVED_CHANGES = 2
private const val REQUEST_PICK_PLUGIN = 3
} }
@Parcelize @Parcelize
...@@ -69,7 +70,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -69,7 +70,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
private var profileId = -1L private var profileId = -1L
private lateinit var isProxyApps: SwitchPreference private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: IconListPreference private lateinit var plugin: PluginPreference
private lateinit var pluginConfigure: EditTextPreference private lateinit var pluginConfigure: EditTextPreference
private lateinit var pluginConfiguration: PluginConfiguration private lateinit var pluginConfiguration: PluginConfiguration
private lateinit var receiver: BroadcastReceiver private lateinit var receiver: BroadcastReceiver
...@@ -102,18 +103,6 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -102,18 +103,6 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
findPreference<Preference>(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy findPreference<Preference>(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin)!! plugin = findPreference(Key.plugin)!!
pluginConfigure = findPreference(Key.pluginConfigure)!! pluginConfigure = findPreference(Key.pluginConfigure)!!
plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener { _, newValue ->
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
pluginConfigure.isEnabled = newValue.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
if (PluginManager.fetchPlugins()[newValue]?.trusted == false) {
Snackbar.make(view!!, R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()
}
true
}
pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace) pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace)
pluginConfigure.onPreferenceChangeListener = this pluginConfigure.onPreferenceChangeListener = this
initPlugins() initPlugins()
...@@ -139,11 +128,6 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -139,11 +128,6 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
} }
private fun initPlugins() { private fun initPlugins() {
val plugins = PluginManager.fetchPlugins()
plugin.entries = plugins.map { it.value.label }.toTypedArray()
plugin.entryValues = plugins.map { it.value.id }.toTypedArray()
plugin.entryIcons = plugins.map { it.value.icon }.toTypedArray()
plugin.entryPackageNames = plugins.map { it.value.packageName }.toTypedArray()
pluginConfiguration = PluginConfiguration(DataStore.plugin) pluginConfiguration = PluginConfiguration(DataStore.plugin)
plugin.value = pluginConfiguration.selected plugin.value = pluginConfiguration.selected
plugin.init() plugin.init()
...@@ -203,12 +187,13 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -203,12 +187,13 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {
when (preference.key) { when (preference.key) {
Key.plugin -> BottomSheetPreferenceDialogFragment().apply { Key.plugin -> PluginPreferenceDialogFragment().apply {
setArg(Key.plugin) setArg(Key.plugin)
setTargetFragment(this@ProfileConfigFragment, 0) setTargetFragment(this@ProfileConfigFragment, REQUEST_PICK_PLUGIN)
}.showAllowingStateLoss(parentFragmentManager, Key.plugin) }.showAllowingStateLoss(parentFragmentManager, Key.plugin)
Key.pluginConfigure -> { Key.pluginConfigure -> {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE) val intent = PluginManager.buildIntent(plugin.plugins.lookup.getValue(pluginConfiguration.selected).id,
PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else { if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else {
startActivityForResult(intent startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()), .putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()),
...@@ -221,6 +206,19 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -221,6 +206,19 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_PICK_PLUGIN -> if (resultCode == Activity.RESULT_OK) {
val selected = plugin.plugins.lookup.getValue(
data?.getStringExtra(PluginPreferenceDialogFragment.KEY_SELECTED_ID)!!)
val override = pluginConfiguration.pluginsOptions.keys.firstOrNull {
plugin.plugins.lookup[it] == selected
}
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, override ?: selected.id)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
pluginConfigure.isEnabled = selected !is NoPlugin
pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
if (!selected.trusted) Snackbar.make(view!!, R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()
}
REQUEST_CODE_PLUGIN_CONFIGURE -> when (resultCode) { REQUEST_CODE_PLUGIN_CONFIGURE -> when (resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS) val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
......
...@@ -24,27 +24,21 @@ import android.content.Context ...@@ -24,27 +24,21 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import androidx.preference.ListPreference import androidx.preference.ListPreference
import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.PluginList
import com.github.shadowsocks.plugin.PluginManager
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { class PluginPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
companion object FallbackProvider : SummaryProvider<IconListPreference> { companion object FallbackProvider : SummaryProvider<PluginPreference> {
override fun provideSummary(preference: IconListPreference?): CharSequence? { override fun provideSummary(preference: PluginPreference): CharSequence? {
val i = preference?.selectedEntry return preference.selectedEntry?.label ?: preference.unknownValueSummary.format(preference.value)
return if (i != null && i < 0) preference.unknownValueSummary?.format(preference.value) else
preference?.entry
} }
} }
var entryIcons: Array<Drawable?>? = null lateinit var plugins: PluginList
val selectedEntry: Int get() = entryValues.indexOf(value) val selectedEntry get() = plugins.lookup[value]
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry) private val entryIcon: Drawable? get() = selectedEntry?.icon
// fun setEntryIcons(@ArrayRes entryIconsResId: Int) { private val unknownValueSummary = context.getString(R.string.plugin_unknown)
// val array = getContext().getResources().obtainTypedArray(entryIconsResId)
// entryIcons = Array(array.length(), { i -> array.getDrawable(i) })
// array.recycle()
// }
var unknownValueSummary: String? = null
var entryPackageNames: Array<String>? = null
private var listener: OnPreferenceChangeListener? = null private var listener: OnPreferenceChangeListener? = null
override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener
...@@ -57,17 +51,15 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -57,17 +51,15 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
val listener = listener val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) { if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString() value = newValue.toString()
if (entryIcons != null) icon = entryIcon icon = entryIcon
true true
} else false } else false
} }
// val a = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference)
// val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1)
// if (entryIconsResId != -1) entryIcons = entryIconsResId
// a.recycle()
} }
fun init() { fun init() {
plugins = PluginManager.fetchPlugins()
entryValues = plugins.lookup.map { it.key }.toTypedArray()
icon = entryIcon icon = entryIcon
summaryProvider = FallbackProvider summaryProvider = FallbackProvider
} }
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
package com.github.shadowsocks.preference package com.github.shadowsocks.preference
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
...@@ -39,12 +40,17 @@ import androidx.recyclerview.widget.DefaultItemAnimator ...@@ -39,12 +40,17 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.Plugin
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { class PluginPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
companion object {
const val KEY_SELECTED_ID = "id"
}
private inner class IconListViewHolder(val dialog: BottomSheetDialog, view: View) : RecyclerView.ViewHolder(view), private inner class IconListViewHolder(val dialog: BottomSheetDialog, view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener { View.OnClickListener, View.OnLongClickListener {
private var index = 0 private lateinit var plugin: Plugin
private val text1 = view.findViewById<TextView>(android.R.id.text1) private val text1 = view.findViewById<TextView>(android.R.id.text1)
private val text2 = view.findViewById<TextView>(android.R.id.text2) private val text2 = view.findViewById<TextView>(android.R.id.text2)
private val icon = view.findViewById<ImageView>(android.R.id.icon) private val icon = view.findViewById<ImageView>(android.R.id.icon)
...@@ -54,46 +60,42 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -54,46 +60,42 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
view.setOnLongClickListener(this) view.setOnLongClickListener(this)
} }
fun bind(i: Int, selected: Boolean = false) { fun bind(plugin: Plugin, selected: Boolean = false) {
text1.text = preference.entries[i] this.plugin = plugin
text2.text = preference.entryValues[i] text1.text = plugin.label
text2.text = plugin.packageName
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface) text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface) text2.setTypeface(null, typeface)
text2.isVisible = preference.entryValues[i].isNotEmpty() && text2.isVisible = plugin.packageName.isNotEmpty() && plugin.label != plugin.packageName
preference.entries[i] != preference.entryValues[i] icon.setImageDrawable(plugin.icon)
icon.setImageDrawable(preference.entryIcons?.get(i))
index = i
} }
override fun onClick(p0: View?) { override fun onClick(v: View?) {
clickedIndex = index clicked = plugin
dialog.dismiss() dialog.dismiss()
} }
override fun onLongClick(p0: View?): Boolean { override fun onLongClick(v: View?) = try {
val pn = preference.entryPackageNames?.get(index) ?: return false startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder()
return try { .scheme("package")
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder() .opaquePart(plugin.packageName)
.scheme("package") .build()))
.opaquePart(pn) true
.build())) } catch (_: ActivityNotFoundException) {
true false
} catch (_: ActivityNotFoundException) {
false
}
} }
} }
private inner class IconListAdapter(private val dialog: BottomSheetDialog) : private inner class IconListAdapter(private val dialog: BottomSheetDialog) :
RecyclerView.Adapter<IconListViewHolder>() { RecyclerView.Adapter<IconListViewHolder>() {
override fun getItemCount(): Int = preference.entries.size override fun getItemCount(): Int = preference.plugins.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = IconListViewHolder(dialog, override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = IconListViewHolder(dialog,
LayoutInflater.from(parent.context).inflate(R.layout.icon_list_item_2, parent, false)) LayoutInflater.from(parent.context).inflate(R.layout.icon_list_item_2, parent, false))
override fun onBindViewHolder(holder: IconListViewHolder, position: Int) { override fun onBindViewHolder(holder: IconListViewHolder, position: Int) {
if (preference.selectedEntry < 0) holder.bind(position) else when (position) { if (selected < 0) holder.bind(preference.plugins[position]) else when (position) {
0 -> holder.bind(preference.selectedEntry, true) 0 -> holder.bind(preference.selectedEntry!!, true)
in preference.selectedEntry + 1..Int.MAX_VALUE -> holder.bind(position) in selected + 1..Int.MAX_VALUE -> holder.bind(preference.plugins[position])
else -> holder.bind(position - 1) else -> holder.bind(preference.plugins[position - 1])
} }
} }
} }
...@@ -102,8 +104,9 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -102,8 +104,9 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
arguments = bundleOf(ARG_KEY to key) arguments = bundleOf(ARG_KEY to key)
} }
private val preference by lazy { getPreference() as IconListPreference } private val preference by lazy { getPreference() as PluginPreference }
private var clickedIndex = -1 private val selected by lazy { preference.plugins.indexOf(preference.selectedEntry) }
private var clicked: Plugin? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity() val activity = requireActivity()
...@@ -123,9 +126,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -123,9 +126,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (clickedIndex >= 0 && clickedIndex != preference.selectedEntry) { val clicked = clicked
val value = preference.entryValues[clickedIndex].toString() if (clicked != null && clicked != preference.selectedEntry) {
if (preference.callChangeListener(value)) preference.value = value targetFragment!!.onActivityResult(targetRequestCode, Activity.RESULT_OK,
} Intent().putExtra(KEY_SELECTED_ID, clicked.id))
} else targetFragment!!.onActivityResult(targetRequestCode, Activity.RESULT_CANCELED, null)
} }
} }
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
<PreferenceCategory <PreferenceCategory
app:title="@string/plugin"> app:title="@string/plugin">
<com.github.shadowsocks.preference.IconListPreference <com.github.shadowsocks.preference.PluginPreference
app:key="plugin" app:key="plugin"
app:persistent="false" app:persistent="false"
app:title="@string/plugin" app:title="@string/plugin"
......
* 1.3.4: * 1.3.4:
* Optional new metadata `com.github.shadowsocks.plugin.id.aliases` for plugin ID aliases;
(see doc for `PluginContract.METADATA_KEY_ID_ALIASES` and main documentation "Plugin ID Aliasing" for more information)
* Please use `android:path` instead of `android:pathPrefix`, sample code in documentations have been updated to reflect this recommendation. * Please use `android:path` instead of `android:pathPrefix`, sample code in documentations have been updated to reflect this recommendation.
* Fix occasional crash in `AlertDialogFragment`. * Fix occasional crash in `AlertDialogFragment`.
* Translation updates. * Translation updates.
......
...@@ -242,6 +242,49 @@ Plugin app must include this in their application tag: (which should be automati ...@@ -242,6 +242,49 @@ Plugin app must include this in their application tag: (which should be automati
android:value="1.0.0"/> android:value="1.0.0"/>
``` ```
# Plugin ID Aliasing
To implement plugin ID aliasing, you:
* MUST define meta-data `com.github.shadowsocks.plugin.id.aliases` in your plugin content provider with `android:value="alias"`,
or use `android:resources` to specify a string resource or string array resource for multiple aliases.
* MUST be able to be matched by `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` when invoked on alias.
To do this, you SHOULD use multiple `intent-filter` and use a different `android:path` for each alias.
Alternatively, you MAY also use a single `intent-filter` and use `android:pathPattern` to match all your aliases at once.
You MUST NOT use `android:pathPrefix` or allow `android:pathPattern` to match undeclared plugin ID/alias as it might create a conflict with other plugins.
* SHOULD NOT add or change `intent-filter` for activities to include your aliases -- your plugin ID will always be used.
For example:
```xml
<manifest>
...
<application>
...
<provider>
...
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ALIAS"/>
</intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/>
<meta-data android:name="com.github.shadowsocks.plugin.aliases"
android:value="$PLUGIN_ALIAS"/>
...
</provider>
...
</application>
</manifest>
```
# Android TV # Android TV
Android TV client does not invoke configuration activities. Therefore your plugins should automatically work with them. Android TV client does not invoke configuration activities. Therefore your plugins should automatically work with them.
...@@ -78,6 +78,13 @@ object PluginContract { ...@@ -78,6 +78,13 @@ object PluginContract {
* Constant Value: "com.github.shadowsocks.plugin.id" * Constant Value: "com.github.shadowsocks.plugin.id"
*/ */
const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id" const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id"
/**
* The metadata key to retrieve plugin id aliases.
* Can be a string (representing one alias) or a resource to a string or string array.
*
* Constant Value: "com.github.shadowsocks.plugin.id.aliases"
*/
const val METADATA_KEY_ID_ALIASES = "com.github.shadowsocks.plugin.id.aliases"
/** /**
* The metadata key to retrieve default configuration. Default value is empty. * The metadata key to retrieve default configuration. Default value is empty.
* *
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment