Unverified Commit 1eb08420 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #2431 from shadowsocks/plugin-1.3.4

Plugin 1.3.4
parents d14a0732 4c3574f7
...@@ -52,10 +52,10 @@ androidExtensions { ...@@ -52,10 +52,10 @@ androidExtensions {
def coroutinesVersion = '1.3.3' def coroutinesVersion = '1.3.3'
def roomVersion = '2.2.3' def roomVersion = '2.2.3'
def workVersion = '2.3.0' def workVersion = '2.3.1'
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api 'androidx.fragment:fragment-ktx:1.2.0' api 'androidx.fragment:fragment-ktx:1.2.1'
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycleVersion" api "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.1.0' api 'androidx.preference:preference:1.1.0'
......
...@@ -242,7 +242,7 @@ object BaseService { ...@@ -242,7 +242,7 @@ object BaseService {
File(Core.deviceStorage.noBackupFilesDir, "stat_main"), File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
File(configRoot, CONFIG_FILE), File(configRoot, CONFIG_FILE),
if (udpFallback == null) "-u" else null) if (udpFallback == null) "-u" else null)
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" } check(udpFallback?.plugin == null) { "UDP fallback cannot have plugins" }
udpFallback?.start(this, udpFallback?.start(this,
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"), File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
File(configRoot, CONFIG_FILE_UDP), File(configRoot, CONFIG_FILE_UDP),
......
...@@ -47,8 +47,7 @@ import java.security.MessageDigest ...@@ -47,8 +47,7 @@ import java.security.MessageDigest
class ProxyInstance(val profile: Profile, private val route: String = profile.route) { class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
private var configFile: File? = null private var configFile: File? = null
var trafficMonitor: TrafficMonitor? = null var trafficMonitor: TrafficMonitor? = null
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions val plugin by lazy { PluginManager.init(PluginConfiguration(profile.plugin ?: "")) }
val pluginPath by lazy { PluginManager.init(plugin) }
private var scheduleConfigUpdate = false private var scheduleConfigUpdate = false
suspend fun init(service: BaseService.Interface, hosts: HostsFile) { suspend fun init(service: BaseService.Interface, hosts: HostsFile) {
...@@ -115,7 +114,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -115,7 +114,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
this.configFile = configFile this.configFile = configFile
val config = profile.toJson() val config = profile.toJson()
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString()) plugin?.let { (path, opts) -> config.put("plugin", path).put("plugin_opts", opts.toString()) }
configFile.writeText(config.toString()) configFile.writeText(config.toString())
val cmd = service.buildAdditionalArguments(arrayListOf( val cmd = service.buildAdditionalArguments(arrayListOf(
......
...@@ -301,7 +301,7 @@ data class Profile( ...@@ -301,7 +301,7 @@ data class Profile(
.encodedAuthority("$auth@$wrappedHost:$remotePort") .encodedAuthority("$auth@$wrappedHost:$remotePort")
val configuration = PluginConfiguration(plugin ?: "") val configuration = PluginConfiguration(plugin ?: "")
if (configuration.selected.isNotEmpty()) { if (configuration.selected.isNotEmpty()) {
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false)) builder.appendQueryParameter(Key.plugin, configuration.getOptions().toString(false))
} }
if (!name.isNullOrEmpty()) builder.fragment(name) if (!name.isNullOrEmpty()) builder.fragment(name)
return builder.build() return builder.build()
...@@ -315,7 +315,7 @@ data class Profile( ...@@ -315,7 +315,7 @@ data class Profile(
put("password", password) put("password", password)
put("method", method) put("method", method)
if (profiles == null) return@apply if (profiles == null) return@apply
PluginConfiguration(plugin ?: "").selectedOptions.also { PluginConfiguration(plugin ?: "").getOptions().also {
if (it.id.isNotEmpty()) { if (it.id.isNotEmpty()) {
put("plugin", it.id) put("plugin", it.id)
put("plugin_opts", it.toString()) put("plugin_opts", it.toString())
......
...@@ -21,13 +21,11 @@ ...@@ -21,13 +21,11 @@
package com.github.shadowsocks.plugin package com.github.shadowsocks.plugin
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init { init {
check(resolveInfo.providerInfo != null) check(resolveInfo.providerInfo != null)
} }
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData override val componentInfo get() = resolveInfo.providerInfo!!
override val packageName: String get() = resolveInfo.providerInfo.packageName
} }
...@@ -24,9 +24,18 @@ import android.graphics.drawable.Drawable ...@@ -24,9 +24,18 @@ 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
open val directBootAware: 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()
} }
...@@ -50,14 +50,15 @@ class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val se ...@@ -50,14 +50,15 @@ class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val se
} else PluginOptions(line) } else PluginOptions(line)
}) })
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else fun getOptions(
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig) id: String = selected,
val selectedOptions: PluginOptions get() = getOptions(selected) defaultConfig: () -> String? = { PluginManager.fetchPlugins().lookup[id]?.defaultConfig }
) = if (id.isEmpty()) PluginOptions() else pluginsOptions[id] ?: PluginOptions(id, defaultConfig())
override fun toString(): String { override fun toString(): String {
val result = LinkedList<PluginOptions>() val result = LinkedList<PluginOptions>()
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt) for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
if (!pluginsOptions.contains(selected)) result.addFirst(selectedOptions) if (!pluginsOptions.contains(selected)) result.addFirst(getOptions())
return result.joinToString("\n") { it.toString(false) } return result.joinToString("\n") { it.toString(false) }
} }
} }
/*******************************************************************************
* *
* 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 = mutableMapOf<String, Plugin>().apply {
for (plugin in this@PluginList) {
fun check(old: Plugin?) = check(old == null || old === plugin)
check(put(plugin.id, plugin))
for (alias in plugin.idAliases) check(put(alias, plugin))
}
}
}
...@@ -24,6 +24,7 @@ import android.annotation.SuppressLint ...@@ -24,6 +24,7 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ProviderInfo import android.content.pm.ProviderInfo
import android.content.pm.Signature import android.content.pm.Signature
...@@ -100,19 +101,15 @@ object PluginManager { ...@@ -100,19 +101,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!!
} }
...@@ -125,30 +122,34 @@ object PluginManager { ...@@ -125,30 +122,34 @@ object PluginManager {
// the following parts are meant to be used by :bg // the following parts are meant to be used by :bg
@Throws(Throwable::class) @Throws(Throwable::class)
fun init(options: PluginOptions): String? { fun init(configuration: PluginConfiguration): Pair<String, PluginOptions>? {
if (options.id.isEmpty()) return null if (configuration.selected.isEmpty()) return null
var throwable: Throwable? = null var throwable: Throwable? = null
try { try {
val path = initNative(options) val result = initNative(configuration)
if (path != null) return path if (result != null) return result
} catch (t: Throwable) { } catch (t: Throwable) {
if (throwable == null) throwable = t else printLog(t) if (throwable == null) throwable = t else printLog(t)
} }
// add other plugin types here // add other plugin types here
throw throwable ?: PluginNotFoundException(options.id) throw throwable ?: PluginNotFoundException(configuration.selected)
} }
private fun initNative(options: PluginOptions): String? { private fun initNative(configuration: PluginConfiguration): Pair<String, PluginOptions>? {
val providers = app.packageManager.queryIntentContentProviders( val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), PackageManager.GET_META_DATA) Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(configuration.selected)),
PackageManager.GET_META_DATA or
PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
)
if (providers.isEmpty()) return null if (providers.isEmpty()) return null
val provider = providers.single().providerInfo val provider = providers.single().providerInfo
val options = configuration.getOptions { provider.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
var failure: Throwable? = null var failure: Throwable? = null
try { try {
initNativeFaster(provider)?.also { return it } initNativeFaster(provider)?.also { return it to options }
} catch (t: Throwable) { } catch (t: Throwable) {
Crashlytics.log(Log.WARN, TAG, "Initializing native plugin faster mode failed") Crashlytics.log(Log.WARN, TAG, "Initializing native plugin faster mode failed")
failure = t failure = t
...@@ -158,9 +159,8 @@ object PluginManager { ...@@ -158,9 +159,8 @@ object PluginManager {
scheme(ContentResolver.SCHEME_CONTENT) scheme(ContentResolver.SCHEME_CONTENT)
authority(provider.authority) authority(provider.authority)
}.build() }.build()
val cr = app.contentResolver
try { try {
return initNativeFast(cr, options, uri) return initNativeFast(app.contentResolver, options, uri)?.let { it to options }
} catch (t: Throwable) { } catch (t: Throwable) {
Crashlytics.log(Log.WARN, TAG, "Initializing native plugin fast mode failed") Crashlytics.log(Log.WARN, TAG, "Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) } failure?.also { t.addSuppressed(it) }
...@@ -168,7 +168,7 @@ object PluginManager { ...@@ -168,7 +168,7 @@ object PluginManager {
} }
try { try {
return initNativeSlow(cr, options, uri) return initNativeSlow(app.contentResolver, options, uri)?.let { it to options }
} catch (t: Throwable) { } catch (t: Throwable) {
failure?.also { t.addSuppressed(it) } failure?.also { t.addSuppressed(it) }
throw t throw t
...@@ -176,7 +176,7 @@ object PluginManager { ...@@ -176,7 +176,7 @@ object PluginManager {
} }
private fun initNativeFaster(provider: ProviderInfo): String? { private fun initNativeFaster(provider: ProviderInfo): String? {
return provider.metaData.getString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)?.let { relativePath -> return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)?.let { relativePath ->
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
check(canExecute()) check(canExecute())
}.absolutePath }.absolutePath
...@@ -219,4 +219,11 @@ object PluginManager { ...@@ -219,4 +219,11 @@ object PluginManager {
if (!initialized) entryNotFound() if (!initialized) entryNotFound()
return File(pluginDir, options.id).absolutePath return File(pluginDir, options.id).absolutePath
} }
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
is String -> value
is Int -> app.packageManager.getResourcesForApplication(applicationInfo).getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")
}
} }
...@@ -20,22 +20,38 @@ ...@@ -20,22 +20,38 @@
package com.github.shadowsocks.plugin package com.github.shadowsocks.plugin
import android.content.pm.ComponentInfo
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Build
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.plugin.PluginManager.loadString
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val metaData: Bundle protected abstract val componentInfo: ComponentInfo
override val id: String by lazy { metaData.getString(PluginContract.METADATA_KEY_ID)!! } override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) } override val idAliases: Array<String> by lazy {
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) } when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) {
override val defaultConfig: String? by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) } is String -> arrayOf(value)
override val packageName: String get() = resolveInfo.resolvePackageName is Int -> app.packageManager.getResourcesForApplication(componentInfo.applicationInfo).run {
when (getResourceTypeName(value)) {
"string" -> arrayOf(getString(value))
else -> getStringArray(value)
}
}
null -> emptyArray()
else -> error("unknown type for plugin meta-data idAliases")
}
}
override val label: CharSequence get() = resolveInfo.loadLabel(app.packageManager)
override val icon: Drawable get() = resolveInfo.loadIcon(app.packageManager)
override val defaultConfig by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = componentInfo.packageName
override val trusted by lazy { override val trusted by lazy {
Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains) Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
} }
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
} }
...@@ -160,4 +160,5 @@ ...@@ -160,4 +160,5 @@
<string name="plugin_unknown">Unknown plugin %s</string> <string name="plugin_unknown">Unknown plugin %s</string>
<string name="plugin_untrusted">Warning: This plugin does not seem to come from a known trusted source.</string> <string name="plugin_untrusted">Warning: This plugin does not seem to come from a known trusted source.</string>
<string name="profile_plugin">Plugin: %s</string> <string name="profile_plugin">Plugin: %s</string>
<string name="plugin_auto_connect_unlock_only">This plugin might not work with Auto Connect</string>
</resources> </resources>
...@@ -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,20 +103,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -102,20 +103,9 @@ 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
pluginConfiguration = PluginConfiguration(DataStore.plugin)
initPlugins() initPlugins()
udpFallback = findPreference(Key.udpFallback)!! udpFallback = findPreference(Key.udpFallback)!!
DataStore.privateStore.registerChangeListener(this) DataStore.privateStore.registerChangeListener(this)
...@@ -139,16 +129,10 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -139,16 +129,10 @@ 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)
plugin.value = pluginConfiguration.selected plugin.value = pluginConfiguration.selected
plugin.init() plugin.init()
pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty() pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString() pluginConfigure.text = pluginConfiguration.getOptions().toString()
} }
private fun showPluginEditor() { private fun showPluginEditor() {
...@@ -203,15 +187,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -203,15 +187,16 @@ 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.getOptions().toString()),
REQUEST_CODE_PLUGIN_CONFIGURE) REQUEST_CODE_PLUGIN_CONFIGURE)
} }
} }
...@@ -221,6 +206,20 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -221,6 +206,20 @@ 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
plugin.value = pluginConfiguration.selected
pluginConfigure.isEnabled = selected !is NoPlugin
pluginConfigure.text = pluginConfiguration.getOptions().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,20 @@ import android.content.Context ...@@ -24,27 +24,20 @@ 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) =
val i = preference?.selectedEntry 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 +50,15 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -57,17 +50,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
...@@ -32,68 +33,76 @@ import android.view.View ...@@ -32,68 +33,76 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator 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)
private val unlock = view.findViewById<View>(R.id.unlock).apply {
TooltipCompat.setTooltipText(this, getText(R.string.plugin_auto_connect_unlock_only))
}
init { init {
view.setOnClickListener(this) view.setOnClickListener(this)
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] val label = plugin.label
text1.text = label
text2.text = plugin.id
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.id.isNotEmpty() && label != plugin.id
preference.entries[i] != preference.entryValues[i] icon.setImageDrawable(plugin.icon)
icon.setImageDrawable(preference.entryIcons?.get(i)) unlock.isGone = plugin.directBootAware || !DataStore.persistAcrossReboot
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 +111,9 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -102,8 +111,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 +133,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -123,9 +133,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)
} }
} }
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
...@@ -30,4 +31,14 @@ ...@@ -30,4 +31,14 @@
android:maxLines="2" android:maxLines="2"
android:ellipsize="end"/> android:ellipsize="end"/>
</LinearLayout> </LinearLayout>
<ImageView
android:id="@+id/unlock"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:paddingStart="12dp"
android:src="@drawable/ic_action_lock_open"
android:tint="@color/material_amber_a700"
android:importantForAccessibility="no"
tools:ignore="RtlSymmetry" />
</LinearLayout> </LinearLayout>
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
<resources> <resources>
<color name="background_selected">@color/material_primary_100</color> <color name="background_selected">@color/material_primary_100</color>
<color name="background_stat">@color/material_primary_300</color> <color name="background_stat">@color/material_primary_300</color>
<color name="material_amber_a700">#ffab00</color>
</resources> </resources>
...@@ -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:
* 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.
* Added missing documentation regarding direct boot support.
Please add `android:directBootAware="true"` with proper support for your `provider` if possible.
* You can now use `android:resources` on `meta-data` tags. (main/host app update required, however, you should never use dynamic resources)
* Fix occasional crash in `AlertDialogFragment`.
* Translation updates.
* Dependency updates:
- `androidx.core:core-ktx:1.2.0`;
- `com.google.android.material:material:1.1.0`.
* 1.3.3: * 1.3.3:
* Fix a build script issue. * Fix a build script issue.
* 1.3.2: * 1.3.2:
......
...@@ -10,13 +10,12 @@ Support library for easier development on [shadowsocks ...@@ -10,13 +10,12 @@ Support library for easier development on [shadowsocks
These are some plugins ready to use on shadowsocks-android. These are some plugins ready to use on shadowsocks-android.
* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases) * [v2ray](https://github.com/shadowsocks/v2ray-plugin-android)
* [kcptun](https://github.com/shadowsocks/kcptun-android/releases) * [kcptun](https://github.com/shadowsocks/kcptun-android/releases)
* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases)
## Developer's guide ## Developer's guide
WARNING: This library is still in beta (0.x) and its content is subject to massive changes.
This library is designed with Java interoperability in mind so theoretically you can use this This library is designed with Java interoperability in mind so theoretically you can use this
library with other languages and/or build tools but there isn't documentation for that yet. This library with other languages and/or build tools but there isn't documentation for that yet. This
guide is written for Scala + SBT. Contributions are welcome. guide is written for Scala + SBT. Contributions are welcome.
...@@ -32,11 +31,11 @@ There are no arbitrary restrictions/requirements on package name, component name ...@@ -32,11 +31,11 @@ There are no arbitrary restrictions/requirements on package name, component name
### Add dependency ### Add dependency
First you need to add this library to your dependencies. This library is written mostly in Scala First you need to add this library to your dependencies.
and it's most convenient to use it with SBT: This library is written mostly in Kotlin but can also work with Java-only projects:
```scala ```gradle
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4" implementation 'com.github.shadowsocks:plugin:$LATEST_VERSION'
``` ```
### Native binary configuration ### Native binary configuration
...@@ -44,7 +43,7 @@ libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4" ...@@ -44,7 +43,7 @@ libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4"
First you need to get your native binary compiling on Android platform. First you need to get your native binary compiling on Android platform.
* [Sample project for C](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615); * [Sample project for C](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615);
* [Sample project for Go](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c). * [Sample project for Go](https://github.com/shadowsocks/v2ray-plugin-android/tree/172bd4cec0276112828614482fb646b79dbf1540).
In addition to functionalities of a normal plugin, it has to support these additional flags that In addition to functionalities of a normal plugin, it has to support these additional flags that
may get passed through arguments: may get passed through arguments:
...@@ -56,25 +55,23 @@ In addition to functionalities of a normal plugin, it has to support these addit ...@@ -56,25 +55,23 @@ In addition to functionalities of a normal plugin, it has to support these addit
### Implement a binary provider ### Implement a binary provider
It's super easy. You just need to implement two or three methods. For example for `obfs-local`: You just need to implement two or three methods. For example for `v2ray`:
```scala ```kotlin
final class BinaryProvider extends NativePluginProvider { class BinaryProvider : NativePluginProvider() {
override protected def populateFiles(provider: PathProvider) { override fun populateFiles(provider: PathProvider) {
provider.addPath("obfs-local", "755") provider.addPath("v2ray", 0b111101101)
// add additional files here // add additional files here
} }
// remove this method to disable fast mode, read more in the documentation // remove this method to disable fast mode, read more in the documentation
override def getExecutable: String = override fun getExecutable() = context!!.applicationInfo.nativeLibraryDir + "/libv2ray.so"
getContext.getApplicationInfo.nativeLibraryDir + "/libobfs-local.so"
override fun openFile(uri: Uri): ParcelFileDescriptor = when (uri.path) {
override def openFile(uri: Uri): ParcelFileDescriptor = uri.getPath match { "/v2ray" -> ParcelFileDescriptor.open(File(getExecutable()), ParcelFileDescriptor.MODE_READ_ONLY)
case "/obfs-local" => // handle additional files here
ParcelFileDescriptor.open(new File(getExecutable), ParcelFileDescriptor.MODE_READ_ONLY) else -> throw FileNotFoundException()
// handle additional files here }
case _ => throw new FileNotFoundException()
}
} }
``` ```
...@@ -87,6 +84,7 @@ Then add it to your manifest: ...@@ -87,6 +84,7 @@ Then add it to your manifest:
... ...
<provider android:name=".BinaryProvider" <provider android:name=".BinaryProvider"
android:exported="true" android:exported="true"
android:directBootAware="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"> android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
<intent-filter> <intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/> <action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
...@@ -95,12 +93,16 @@ Then add it to your manifest: ...@@ -95,12 +93,16 @@ Then add it to your manifest:
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/> <action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin" <data android:scheme="plugin"
android:host="com.github.shadowsocks" android:host="com.github.shadowsocks"
android:pathPrefix="/$PLUGIN_ID"/> android:path="/$PLUGIN_ID"/>
</intent-filter> </intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id" <meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/> android:value="$PLUGIN_ID"/>
<!-- Optional: default is empty -->
<meta-data android:name="com.github.shadowsocks.plugin.default_config" <meta-data android:name="com.github.shadowsocks.plugin.default_config"
android:value="dummy=default;plugin=options"/> android:value="dummy=default;plugin=options"/>
<!-- Optional: remove to disable faster mode, read more in the documentation -->
<meta-data android:name="com.github.shadowsocks.plugin.executable_path"
android:value="$PATH_TO_EXECUTABLE_RELATIVE_TO_NATIVE_LIB_DIR"/>
</provider> </provider>
... ...
</application> </application>
......
...@@ -46,9 +46,9 @@ mavenPublish { ...@@ -46,9 +46,9 @@ mavenPublish {
} }
dependencies { dependencies {
api 'androidx.core:core-ktx:1.1.0' api 'androidx.core:core-ktx:1.2.0'
api 'androidx.drawerlayout:drawerlayout:1.1.0-alpha03' // https://android-developers.googleblog.com/2019/07/android-q-beta-5-update.html api 'androidx.drawerlayout:drawerlayout:1.1.0-alpha03' // https://android-developers.googleblog.com/2019/07/android-q-beta-5-update.html
api 'com.google.android.material:material:1.1.0-rc02' api 'com.google.android.material:material:1.1.0'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
......
...@@ -125,14 +125,14 @@ In native mode, plugins are provided as native executables and `shadowsocks-libe ...@@ -125,14 +125,14 @@ In native mode, plugins are provided as native executables and `shadowsocks-libe
Every native mode plugin MUST have a content provider to provide the native executables (since they Every native mode plugin MUST have a content provider to provide the native executables (since they
can exceed 1M which is the limit of Intent size) that: can exceed 1M which is the limit of Intent size) that:
* MUST have `android:label` and `android:icon`; (may be configured by its parent `application`) * MUST have `android:label` and `android:icon`; (may be inherited from parent `application`)
* SHOULD have `android:directBootAware="true"` with proper support if possible;
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`; * MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`;
(used for discovering plugins) (used for discovering plugins)
* MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID`; * MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID` or a string resource;
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` and * MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` and
data `plugin://com.github.shadowsocks/$PLUGIN_ID`; (used for configuring plugin) data `plugin://com.github.shadowsocks/$PLUGIN_ID`; (used for configuring plugin)
* CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value, default is * CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value or a string resource, default is empty;
empty;
* MUST implement `query` that returns the file list which MUST include `$PLUGIN_ID` when having * MUST implement `query` that returns the file list which MUST include `$PLUGIN_ID` when having
these as arguments: these as arguments:
- `uri = "content://$authority_of_your_provider`; - `uri = "content://$authority_of_your_provider`;
...@@ -155,6 +155,7 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the ...@@ -155,6 +155,7 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the
... ...
<provider android:name=".BinaryProvider" <provider android:name=".BinaryProvider"
android:exported="true" android:exported="true"
android:directBootAware="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER" android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"
tools:ignore="ExportedContentProvider"> tools:ignore="ExportedContentProvider">
<intent-filter> <intent-filter>
...@@ -164,7 +165,7 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the ...@@ -164,7 +165,7 @@ This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/> <action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
<data android:scheme="plugin" <data android:scheme="plugin"
android:host="com.github.shadowsocks" android:host="com.github.shadowsocks"
android:pathPrefix="/$PLUGIN_ID"/> android:path="/$PLUGIN_ID"/>
</intent-filter> </intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id" <meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/> android:value="$PLUGIN_ID"/>
...@@ -202,6 +203,7 @@ This allows the host app to launch your plugin without ever launching your app. ...@@ -202,6 +203,7 @@ This allows the host app to launch your plugin without ever launching your app.
## JVM mode ## JVM mode
This feature hasn't been implemented yet. This feature hasn't been implemented yet.
Please open an issue if you need this.
# Plugin security # Plugin security
...@@ -241,6 +243,49 @@ Plugin app must include this in their application tag: (which should be automati ...@@ -241,6 +243,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.
GROUP=com.github.shadowsocks GROUP=com.github.shadowsocks
VERSION_NAME=1.3.3 VERSION_NAME=1.3.4
VERSION_CODE=13 VERSION_CODE=14
POM_ARTIFACT_ID=plugin POM_ARTIFACT_ID=plugin
POM_NAME=Shadowsocks Plugin POM_NAME=Shadowsocks Plugin
......
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
<application <application
android:theme="@style/Theme.Shadowsocks"> android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version" <meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="1.3.2"/> android:value="1.3.4"/>
</application> </application>
</manifest> </manifest>
...@@ -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