diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java new file mode 100644 index 00000000..112df2d5 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Commandline.java @@ -0,0 +1,146 @@ +package com.github.shadowsocks.utils; + +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * Commandline objects help handling command lines specifying processes to + * execute. + * + * The class can be used to define a command line as nested elements or as a + * helper to define a command line by an application. + *
+ *
+ * <someelement>
+ *
+ * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
+ *
+ * Adds support for escape character '\'.
+ *
+ * @author Mygod
+ */
+public final class Commandline {
+ private Commandline() { }
+
+ /**
+ * Quote the parts of the given array in way that makes them
+ * usable as command line arguments.
+ * @param line the list of arguments to quote.
+ * @return empty string for null or no command, else every argument split
+ * by spaces and quoted by quoting rules.
+ */
+ public static String toString(String[] line) {
+ // empty path return empty string
+ if (line == null || line.length == 0) {
+ return "";
+ }
+ // path containing one or more elements
+ final StringBuilder result = new StringBuilder();
+ for (int i = 0; i < line.length; i++) {
+ if (i > 0) {
+ result.append(' ');
+ }
+ for (int j = 0; j < line[i].length(); ++j) {
+ char ch = line[i].charAt(j);
+ switch (ch) {
+ case ' ': case '\\': case '"': case '\'': result.append('\\'); // intentionally no break
+ default: result.append(ch);
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Crack a command line.
+ * @param toProcess the command line to process.
+ * @return the command line broken into strings.
+ * An empty or null toProcess parameter results in a zero sized array.
+ */
+ public static String[] translateCommandline(String toProcess) {
+ if (toProcess == null || toProcess.length() == 0) {
+ //no command? no string
+ return new String[0];
+ }
+ // parse with a simple finite state machine
+
+ final int normal = 0;
+ final int inQuote = 1;
+ final int inDoubleQuote = 2;
+ int state = normal;
+ final StringTokenizer tok = new StringTokenizer(toProcess, "\\\"\' ", true);
+ final ArrayList
+ * <acommandline executable="/executable/to/run">
+ * <argument value="argument 1" />
+ * <argument line="argument_1 argument_2 argument_3" />
+ * <argument value="argument 4" />
+ * </acommandline>
+ * </someelement>
+ * <manifest>
+ * ...
+ * <application>
+ * ...
+ * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.ConfigureActivity">
+ * <intent-filter>
+ * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE" />
+ * </intent-filter>
+ * </activity>
+ * ...
+ * </application>
+ *</manifest>
+ *
+ * @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))
+}
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala
new file mode 100644
index 00000000..40288b8c
--- /dev/null
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala
@@ -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:
+ *
+ * <manifest>
+ * ...
+ * <application>
+ * ...
+ * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.HelpActivity">
+ * <intent-filter>
+ * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP" />
+ * </intent-filter>
+ * </activity>
+ * ...
+ * </application>
+ *</manifest>
+ *
+ * @author Mygod
+ */
+trait HelpActivity extends OptionsCapableActivity {
+ // HelpActivity can choose to ignore options
+ override protected def onInitializePluginOptions(options: PluginOptions): Unit = ()
+}
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala
new file mode 100644
index 00000000..1babb814
--- /dev/null
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpCallback.scala
@@ -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)))
+}
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala
new file mode 100644
index 00000000..1fdc7845
--- /dev/null
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/NativePluginProvider.scala
@@ -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:
+ *
+ * <manifest>
+ * ...
+ * <application>
+ * ...
+ * <provider android:name="com.github.shadowsocks.$PLUGIN_ID.PluginProvider"
+ * android:authorities="com.github.shadowsocks.$PLUGIN_ID">
+ * <intent-filter>
+ * <category android:name="com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN" />
+ * </intent-filter>
+ * </provider>
+ * ...
+ * </application>
+ *</manifest>
+ *
+ * @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 = ???
+}
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala
new file mode 100644
index 00000000..40dbd96a
--- /dev/null
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/OptionsCapableActivity.scala
@@ -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()
+ }
+}
diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala
new file mode 100644
index 00000000..4eddf6d6
--- /dev/null
+++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/PathProvider.scala
@@ -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
+ }
+}