diff --git a/.github/issue_template.md b/.github/issue_template.md
index d5edcba1..bc91020f 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -30,7 +30,7 @@ _Put an `x` inside the [ ] that applies._
* [ ] Per-App Proxy
* [ ] Bypass mode
* [ ] UDP Forwarding
-* KCP Parameters: (leave blank if not enabled)
+* Plugin configuration (if applicable):
* [ ] Auto Connect
* [ ] TCP Fast Open
* [ ] NAT mode
diff --git a/plugin/README.md b/plugin/README.md
new file mode 100644
index 00000000..05cbe427
--- /dev/null
+++ b/plugin/README.md
@@ -0,0 +1,169 @@
+# shadowsocks-android plugin framework
+
+Support library for easier development on [shadowsocks
+ plugin](https://github.com/shadowsocks/shadowsocks-org/issues/28) for Android. Also includes some
+ useful resources to easily get consistent styling with the main app.
+
+## Official plugins
+
+These are some plugins ready to use on shadowsocks-android.
+
+* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases)
+* [kcptun](https://github.com/shadowsocks/kcptun-android/releases)
+
+## 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
+ 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.
+
+The following is just a simple guide to get you started, for more technical details, see
+ [doc.md](https://github.com/shadowsocks/shadowsocks-android/blob/develop/plugin/doc.md).
+
+### Package name
+
+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`.
+
+### Add dependency
+
+First you need to add this library to your dependencies. This library is written mostly in Scala
+ and it's most convenient to use it with SBT:
+
+```scala
+libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.1-SNAPSHOT"
+```
+
+### Native binary configuration
+
+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 Go](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c).
+
+In addition to functionalities of a normal plugin, it has to support these additional flags that
+ may get passed through arguments:
+
+* `-V`: VPN mode. In this case, the plugin should pass all file descriptors that needs protecting
+ from VPN connections (i.e. its traffic will not be forwarded through the VPN) through an
+ ancillary message to `./protect_path`;
+* `--fast-open`: TCP fast open enabled.
+
+### Implement a binary provider
+
+It's super easy. You just need to implement two or three methods. For example for `obfs-local`:
+
+```scala
+final class BinaryProvider extends NativePluginProvider {
+ override protected def populateFiles(provider: PathProvider) {
+ provider.addPath("obfs-local", "755")
+ // add additional files here
+ }
+
+ // remove this method to disable fast mode, read more in the documentation
+ override def getExecutable: String =
+ getContext.getApplicationInfo.nativeLibraryDir + "/libobfs-local.so"
+
+ override def openFile(uri: Uri): ParcelFileDescriptor = uri.getPath match {
+ case "/obfs-local" =>
+ ParcelFileDescriptor.open(new File(getExecutable), ParcelFileDescriptor.MODE_READ_ONLY)
+ // handle additional files here
+ case _ => throw new FileNotFoundException()
+ }
+}
+```
+
+Then add it to your manifest:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+### Add user interfaces
+
+You should add to your plugin app a configuration activity or a help activity or both if you're
+ going to use `ConfigurationActivity.fallbackToManualEditor`.
+
+#### Configuration activity
+
+This is used if found instead of a manual input dialog when user clicks "Configure..." in the main
+ app. This gives you maximum freedom of the user interface. To implement this, you need to extend
+ `ConfigurationActivity` and you will get current options via
+ `onInitializePluginOptions(PluginOptions)` and you can invoke `saveChanges(PluginOptions)` or
+ `discardChanges()` before `finish()` or `fallbackToManualEditor()`. Then add it to your manifest:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+#### Help activity/callback
+
+This is started when user taps "?" in manual editor. To implement this, you need to extend
+ `HelpCallback` if you want a simple dialog with help message as `CharSequence` or `HelpActivity`
+ if you want to provide custom user interface, implement the required methods, then add it to your
+ manifest:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+Great. Now your plugin is ready to use.
diff --git a/plugin/doc.md b/plugin/doc.md
new file mode 100644
index 00000000..4caebaac
--- /dev/null
+++ b/plugin/doc.md
@@ -0,0 +1,230 @@
+# Overview
+
+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.
+
+# Plugin configuration
+
+Plugins get their args configured via one of the following two options:
+
+* A configuration activity;
+ ([example](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615))
+* If no configuration activity is found or the activity requests the fallback mode, the fallback
+ mode will be used: user manual input and optional help message.
+ ([example](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c))
+
+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.
+
+## Configuration activity
+
+If the plugin provides a configuration activity, it will be started when user picks your plugin and
+ taps configure. It:
+
+* MUST have action: `com.github.shadowsocks.plugin.ACTION_CONFIGURE`;
+* MUST have category: `android.intent.category.DEFAULT`;
+* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
+* SHOULD parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` (all options as a single
+ string) and display the current options;
+* SHOULD distinguish between server settings and feature settings in some way, e.g. for
+ `obfs-local`, `obfs` is a server setting and `obfs_host` is a feature setting;
+* On finish, it SHOULD return one of the following results:
+ - `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`:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+## Help activity/callback
+
+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:
+
+* MUST have action: `com.github.shadowsocks.plugin.ACTION_HELP`;
+* MUST have category: `android.intent.category.DEFAULT`;
+* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
+* CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more
+ relevant information;
+* SHOULD either:
+ - Be invisible and return help message with CharSequence extra
+ `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)
+ - Be visible and return `RESULT_CANCELED`.
+* SHOULD distinguish between server settings and feature settings in some way, e.g. for
+ `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`:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+# Plugin implementation
+
+Every plugin can be either in native mode or JVM mode.
+
+## Native 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:
+
+* MUST have `android:label` and `android:icon`; (may be configured by its parent `application`)
+* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`;
+ (used for discovering plugins)
+* MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID`;
+* 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)
+* CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value, default is
+ empty;
+* MUST implement `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, for
+ example `755`)
+ - `selection = null`;
+ - `selectionArgs = null`;
+ - `sortOrder = null`;
+* MUST implement `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`:
+
+```xml
+
+ ...
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+
+```
+
+## Native mode without binary copying
+
+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:
+
+* MUST implement `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)
+
+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.
+
+## JVM mode
+
+This feature hasn't been implemented yet.
+
+# Plugin security
+
+Plugins are certified using package signatures and shadowsocks-android will consider these
+ signatures as trusted:
+
+* Signatures by [trusted sources](https://github.com/shadowsocks/shadowsocks-android/blob/develop/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala#L22)
+ which includes:
+ - @madeye, i.e. the signer of the main repo;
+ - The main repo doesn't contain any other trusted signatures. Third-party forks should add their
+ signatures to this trusted sources if they have plugins signed by them before publishing their
+ source code.
+* Current package signature, which means:
+ - If you get apk from shadowsocks-android releases or Google Play, this means only apk signed by
+ @madeye will be recognized as trusted.
+ - If you get apk from a third-party fork, all plugins from that developer will get recognized as
+ trusted automatically even if its source code isn't available anywhere online.
+
+A warning will be shown for untrusted plugins. No arbitrary restrictions will be applied.
+
+# Plugin platform versioning
+
+In order to be able to identify compatible and incompatible plugins, [Semantic
+ Versioning](http://semver.org/) will be used.
+
+>Given a version number MAJOR.MINOR.PATCH, increment the:
+>
+>1. MAJOR version when you make incompatible API changes,
+>2. MINOR version when you add functionality in a backwards-compatible manner, and
+>3. 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)
+
+```
+
+```
diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java
index ec73be4b..e716d2cb 100644
--- a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java
+++ b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java
@@ -26,7 +26,7 @@ public final class PluginContract {
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
- * Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
+ * Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP"
*/
public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP";
@@ -39,7 +39,7 @@ public final class PluginContract {
*/
public static final String EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY";
/**
- * The lookup key for a string that provides the whole command as a string.
+ * The lookup key for a string that provides the options as a string.
*
* Example: "obfs=http;obfs-host=www.baidu.com"
*
@@ -69,7 +69,7 @@ public final class PluginContract {
*/
public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config";
- static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable";
+ public static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable";
/** ConfigurationActivity result: fallback to manual edit mode. */
public static final int RESULT_FALLBACK = 1;
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala
index ff618ade..3759c868 100644
--- a/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala
@@ -12,9 +12,13 @@ import android.content.Intent
* ...
* <application>
* ...
- * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.ConfigureActivity">
+ * <activity android:name=".ConfigureActivity">
* <intent-filter>
- * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE" />
+ * <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>
* ...
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala
index 40288b8c..591833d9 100644
--- a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala
@@ -8,9 +8,13 @@ package com.github.shadowsocks.plugin
* ...
* <application>
* ...
- * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.HelpActivity">
+ * <activity android:name=".HelpActivity">
* <intent-filter>
- * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP" />
+ * <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>
* ...