fqnews/plugin/doc.md
Plugin should be bundled as an apk. $PLUGIN_ID in this documentation corresponds to the
executable name for the plugin in order to be cross-platform, e.g. obfs-local. An apk can have
more than one plugins bundled. We don't care as long as they have different $PLUGIN_ID. For
duplicated plugin ID, host should refuse to start.
There are no arbitrary restrictions/requirements on package name, component name and content
provider authority, but you're suggested to follow the format in this documentations. For package
name, use com.github.shadowsocks.plugin.$PLUGIN_ID if it only contains a single plugin to prevent
duplicated plugins. In some places hyphens are not accepted, for example package name. In that
case, hyphens - should be changed into underscores _. For example, the package name for
obfs-local would probably be com.github.shadowsocks.plugin.obfs_local.
It's advised to use this library for easier development, but you're free to start from scratch following this documentation.
Plugins get their args configured via one of the following two options:
Your user interface need not be consistent with shadowsocks-android styling - you don't need to use preferences UI at all if you don't feel like it - however it's recommended to use Material Design at minimum.
If the plugin provides a configuration activity, it will be started when user picks your plugin and taps configure. It:
com.github.shadowsocks.plugin.ACTION_CONFIGURE;android.intent.category.DEFAULT;plugin://com.github.shadowsocks/$PLUGIN_ID;com.github.shadowsocks.plugin.EXTRA_OPTIONS (all options as a single
string) and display the current options;obfs-local, obfs is a server setting and obfs_host is a feature setting;RESULT_OK = 0: In this case it MUST return the data Intent with the new
com.github.shadowsocks.plugin.EXTRA_OPTIONS;RESULT_CANCELED = -1: Nothing will be changed;RESULT_FALLBACK = 1: Fallback mode is requested and the host should display the fallback
editor.This corresponds to com.github.shadowsocks.plugin.ConfigurationActivity in the plugin library.
Here's what a proper configuration activity usually should look like in AndroidManifest.xml:
<manifest>
...
<application>
...
<activity android:name=".ConfigActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
If the plugin doesn't provide a configuration activity, it's highly recommended to provide a help message in the form of an Activity. It:
com.github.shadowsocks.plugin.ACTION_HELP;android.intent.category.DEFAULT;plugin://com.github.shadowsocks/$PLUGIN_ID;com.github.shadowsocks.plugin.EXTRA_OPTIONS and display some more
relevant information;@NightMode int extra com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE and act
accordingly;com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE in the data intent with RESULT_OK; (in this
case, a simple dialog will be shown containing the message)RESULT_CANCELED.simple_obfs, obfs is a server setting and obfs_host is a feature setting.This corresponds to com.github.shadowsocks.plugin.HelpActivity or
com.github.shadowsocks.plugin.HelpCallback in the plugin library. Here's what a proper help
activity/callback usually should look like in AndroidManifest.xml:
<manifest>
...
<application>
...
<activity android:name=".HelpActivity">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="plugin"
android:host="com.github.shadowsocks"
android:path="/$PLUGIN_ID"/>
</intent-filter>
</activity>
...
</application>
</manifest>
Every plugin can be either in native mode or JVM mode.
In native mode, plugins are provided as native executables and shadowsocks-libev's plugin mode
will be used.
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:
android:label and android:icon; (may be inherited from parent application)android:directBootAware="true" with proper support if possible;com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN;
(used for discovering plugins)com.github.shadowsocks.plugin.id with string value $PLUGIN_ID or a string resource;com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN and
data plugin://com.github.shadowsocks/$PLUGIN_ID; (used for configuring plugin)com.github.shadowsocks.plugin.default_config with string value or a string resource, default is empty;query that returns the file list which MUST include $PLUGIN_ID when having
these as arguments:
uri = "content://$authority_of_your_provider;projection = ["path", "mode"]; (relative path, for example obfs-local; file mode as integer, for
example 0b110100100)selection = null;selectionArgs = null;sortOrder = null;openFile that for files returned in query, openFile with mode = "r" returns
a valid ParcelFileDescriptor for reading. For example, uri can be
content://com.github.shadowsocks.plugin.kcptun/kcptun.This corresponds to com.github.shadowsocks.plugin.NativePluginProvider in the plugin library.
Here's what a proper native plugin provider usually should look like in AndroidManifest.xml:
<manifest>
...
<application>
...
<provider android:name=".BinaryProvider"
android:exported="true"
android:directBootAware="true"
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"
tools:ignore="ExportedContentProvider">
<intent-filter>
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
</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_ID"/>
</intent-filter>
<meta-data android:name="com.github.shadowsocks.plugin.id"
android:value="$PLUGIN_ID"/>
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
android:value="dummy=default;plugin=options"/>
</provider>
...
</application>
</manifest>
If your plugin binary executable can run in place, you can support native mode without binary
copying. To support this mode, your ContentProvider must first support native mode with binary
copying (this will be used if the fast routine fails) and:
call that returns absolute path to the entry executable as
com.github.shadowsocks.plugin.EXTRA_ENTRY when having method = "shadowsocks:getExecutable";
(com.github.shadowsocks.plugin.EXTRA_OPTIONS is provided in extras as well just in case you
need them)android:installLocation="internalOnly" for <manifest> in AndroidManifest.xml;android:extractNativeLibs="true" for <application> in AndroidManifest.xml;If you don't plan to support this mode, you can just throw UnsupportedOperationException when
being invoked. It will fallback to the slow routine automatically.
Additionally, if your plugin only needs to supply the path of your executable without doing any extra setup work,
you can use an additional meta-data with name com.github.shadowsocks.plugin.executable_path
to supply executable path to your native binary.
This allows the host app to launch your plugin without ever launching your app.
This feature hasn't been implemented yet. Please open an issue if you need this.
Plugins are certified using package signatures and shadowsocks-android will consider these signatures as trusted:
A warning will be shown for untrusted plugins. No arbitrary restrictions will be applied.
In order to be able to identify compatible and incompatible plugins, Semantic Versioning will be used.
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards-compatible manner, and
- PATCH version when you make backwards-compatible bug fixes.
Plugin app must include this in their application tag: (which should be automatically included if you are using our library)
<meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="1.0.0"/>
To implement plugin ID aliasing, you:
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.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.intent-filter for activities to include your aliases -- your plugin ID will always be used.For example:
<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 client does not invoke configuration activities. Therefore your plugins should automatically work with them.