Initial commit for plugin library

See spec at #1073.
This commit is contained in:
Mygod
2017-01-13 20:20:33 +08:00
parent 5ead55d67a
commit 1887c6ffb0
11 changed files with 522 additions and 45 deletions
@@ -0,0 +1,82 @@
package com.github.shadowsocks.plugin;
/**
* This class provides String constants that will be used in plugin interfaces.
*
* This class is written in Java to keep Java interoperability.
*
* @author Mygod
*/
public final class PluginInterface {
private PluginInterface() { }
/**
* Should be a NativePluginProvider.
*
* Constant Value: "com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN"
*/
public static final String CATEGORY_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN";
/**
* The lookup key for a string that provides the whole command as a string.
*
* Example: "obfs=http;obfs-host=www.baidu.com"
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
*/
public static final String EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS";
/**
* The lookup key for a CharSequence that provides user relevant help message.
*
* Example: "obfs=<http|tls> Enable obfuscating: HTTP or TLS (Experimental).
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
*/
public static final String EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE";
/**
* The metadata key to retrieve default configuration. Default value is empty.
*
* Constant Value: "com.github.shadowsocks.plugin.default_config"
*/
public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config";
/**
* Relative to the file to be copied. This column is required.
*
* Example: "kcptun", "doc/help.txt"
*
* Type: String
*/
public static final String COLUMN_PATH = "path";
/**
* Authority to use for native plugin ContentProvider.
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID
*/
public static String getAuthority(String pluginId) {
return "com.github.shadowsocks.plugin." + pluginId;
}
/**
* Activity Action: Used for ConfigurationActivity.
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE
*/
public static String ACTION_CONFIGURE(String pluginId) {
return getAuthority(pluginId) + ".ACTION_CONFIGURE";
}
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP
*/
public static String ACTION_HELP(String pluginId) {
return getAuthority(pluginId) + ".ACTION_HELP";
}
}
@@ -0,0 +1,78 @@
package com.github.shadowsocks.plugin;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.StringTokenizer;
/**
* Helper class for processing plugin options.
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*
* @author Mygod
*/
public final class PluginOptions extends HashMap<String, String> {
public PluginOptions() {
super();
}
public PluginOptions(int initialCapacity) {
super(initialCapacity);
}
public PluginOptions(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public PluginOptions(String options) throws IllegalArgumentException {
if (TextUtils.isEmpty(options)) return;
final StringTokenizer tokenizer = new StringTokenizer(options, "\\=;", true);
final StringBuilder current = new StringBuilder();
String key = null;
boolean firstEntry = true;
while (tokenizer.hasMoreTokens()) {
String nextToken = tokenizer.nextToken();
if ("\\".equals(nextToken)) current.append(tokenizer.nextToken());
else if ("=".equals(nextToken)) {
if (key != null) throw new IllegalArgumentException("Duplicate keys in " + options);
key = current.toString();
current.setLength(0);
} else if (";".equals(nextToken)) {
if (current.length() > 0) put(key, current.toString());
else if (firstEntry) id = key;
else throw new IllegalArgumentException("Value missing in " + options);
firstEntry = false;
}
}
}
public PluginOptions(String id, String options) throws IllegalArgumentException {
this(options);
this.id = id;
}
public String id;
private static void append(StringBuilder result, String str) {
for (int i = 0; i < str.length(); ++i) {
char ch = str.charAt(i);
switch (ch) {
case '\\': case '=': case ';': result.append('\\'); // intentionally no break
default: result.append(ch);
}
}
}
public String toString(boolean trimId) {
final StringBuilder result = new StringBuilder();
if (!trimId && !TextUtils.isEmpty(id)) append(result, id);
for (Entry<String, String> entry : entrySet()) if (entry.getValue() != null) {
if (result.length() > 0) result.append(';');
append(result, entry.getKey());
result.append('=');
append(result, entry.getValue());
}
return result.toString();
}
@Override
public String toString() {
return toString(false);
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:id="@+id/toolbar" />
@@ -0,0 +1,39 @@
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* Base class for configuration activity. A configuration activity is started when user wishes to configure the
* selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
* `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name="com.github.shadowsocks.$PLUGIN_ID.ConfigureActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE" /&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
trait ConfigurationActivity extends OptionsCapableActivity {
/**
* Equivalent to setResult(RESULT_CANCELED).
*/
final def discardChanges(): Unit = setResult(Activity.RESULT_CANCELED)
/**
* Equivalent to setResult(RESULT_OK, args_with_correct_format).
*
* @param options PluginOptions to save.
*/
final def saveChanges(options: PluginOptions): Unit =
setResult(Activity.RESULT_OK, new Intent().putExtra(PluginInterface.EXTRA_OPTIONS, options.toString))
}
@@ -0,0 +1,25 @@
package com.github.shadowsocks.plugin
/**
* Base class for a help activity. A help activity is started when user taps help when configuring options for your
* plugin. To create a help activity, just extend this class, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;activity android:name="com.github.shadowsocks.$PLUGIN_ID.HelpActivity"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP" /&gt;
* &lt;/intent-filter&gt;
* &lt;/activity&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
trait HelpActivity extends OptionsCapableActivity {
// HelpActivity can choose to ignore options
override protected def onInitializePluginOptions(options: PluginOptions): Unit = ()
}
@@ -0,0 +1,18 @@
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
/**
* HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
* provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
* manifest following the same procedure as adding a HelpActivity.
*
* @author Mygod
*/
trait HelpCallback extends HelpActivity {
def produceHelpMessage(options: PluginOptions): CharSequence
override protected def onInitializePluginOptions(options: PluginOptions): Unit = setResult(Activity.RESULT_OK,
new Intent().putExtra(PluginInterface.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
}
@@ -0,0 +1,65 @@
package com.github.shadowsocks.plugin
import android.content.{ContentProvider, ContentValues}
import android.database.{Cursor, MatrixCursor}
import android.net.Uri
import android.os.ParcelFileDescriptor
/**
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
* class, implement the abstract methods, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.PluginProvider"
* android:authorities="com.github.shadowsocks.$PLUGIN_ID"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*
* @author Mygod
*/
abstract class NativePluginProvider extends ContentProvider {
/**
* @inheritdoc
*
* NativePluginProvider returns application/x-elf by default. It's implementer's responsibility to change this to
* correct type.
*/
override def getType(uri: Uri): String = "application/x-elf"
override def onCreate(): Boolean = true
/**
* Provide all files needed for native plugin.
*
* @param provider A helper object to use to add files.
*/
protected def populateFiles(provider: PathProvider)
override def query(uri: Uri, projection: Array[String], selection: String, selectionArgs: Array[String],
sortOrder: String): Cursor = {
if (selection != null || selectionArgs != null || sortOrder != null) ???
val result = new MatrixCursor(projection.filter(_ == PluginInterface.COLUMN_PATH))
populateFiles(new PathProvider(uri, result))
result
}
def openFile(uri: Uri): ParcelFileDescriptor
override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = {
if (mode != "r") ???
openFile(uri)
}
// Methods that should not be used
override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ???
override def insert(uri: Uri, values: ContentValues): Uri = ???
override def delete(uri: Uri, selection: String, selectionArgs: Array[String]): Int = ???
}
@@ -0,0 +1,32 @@
package com.github.shadowsocks.plugin
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
/**
* Activity that's capable of getting EXTRA_OPTIONS input.
*
* @author Mygod
*/
trait OptionsCapableActivity extends Activity {
protected def pluginOptions(intent: Intent = getIntent): PluginOptions =
try new PluginOptions(intent.getStringExtra(PluginInterface.EXTRA_OPTIONS)) catch {
case exc: IllegalArgumentException =>
Toast.makeText(this, exc.getMessage, Toast.LENGTH_SHORT).show()
null
}
/**
* Populate args to your user interface.
*
* @param options PluginOptions parsed.
*/
protected def onInitializePluginOptions(options: PluginOptions = pluginOptions()): Unit
override protected def onPostCreate(savedInstanceState: Bundle) {
super.onPostCreate(savedInstanceState)
if (savedInstanceState == null) onInitializePluginOptions()
}
}
@@ -0,0 +1,36 @@
package com.github.shadowsocks.plugin
import java.io.File
import android.database.MatrixCursor
import android.net.Uri
/**
* Helper class to provide relative paths of files to copy.
*
* @author Mygod
*/
final class PathProvider private[plugin](baseUri: Uri, cursor: MatrixCursor) {
private val basePath = baseUri.getPath match {
case null => ""
case p => p.stripPrefix("/").stripSuffix("/")
}
def addPath(path: String): PathProvider = {
val stripped = path.stripPrefix("/").stripSuffix("/")
if (stripped.startsWith(basePath)) cursor.newRow().add(PluginInterface.COLUMN_PATH, stripped)
this
}
def addTo(file: File, to: String = ""): PathProvider = {
var sub = to + file.getName
if (basePath.startsWith(sub)) if (file.isDirectory) {
sub += '/'
file.listFiles().foreach(addTo(_, sub))
} else addPath(sub)
this
}
def addAt(file: File, at: String = ""): PathProvider = {
if (basePath.startsWith(at)) if (file.isDirectory) file.listFiles().foreach(addTo(_, at)) else addPath(at)
this
}
}