Commit 99d97d1d authored by Mygod's avatar Mygod

Fix launching plugin

parent b78c10e8
......@@ -6,12 +6,6 @@ version := "4.0.0"
versionCode := Some(174)
proguardOptions ++=
"-keep class android.support.v14.preference.SwitchPreference { <init>(...); }" ::
"-keep class android.support.v7.preference.DropDownPreference { <init>(...); }" ::
"-keep class android.support.v7.preference.PreferenceScreen { <init>(...); }" ::
"-keep class be.mygod.preference.EditTextPreference { <init>(...); }" ::
"-keep class be.mygod.preference.NumberPickerPreference { <init>(...); }" ::
"-keep class be.mygod.preference.PreferenceCategory { <init>(...); }" ::
"-keep class com.github.shadowsocks.System { *; }" ::
"-dontwarn com.google.android.gms.internal.**" ::
"-dontwarn com.j256.ormlite.**" ::
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="profile_item_max_width">480dp</dimen>
<dimen name="icon_list_item_padding">8dp</dimen>
<dimen name="icon_list_item_padding">16dp</dimen>
<!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 -->
<dimen name="fab_margin_top">-44dp</dimen>
</resources>
......@@ -20,8 +20,9 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.{DialogInterface, Intent, SharedPreferences}
import android.content._
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
......@@ -33,7 +34,7 @@ import android.view.MenuItem
import be.mygod.preference.{EditTextPreference, PreferenceFragment}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin.{PluginConfiguration, PluginInterface, PluginManager, PluginOptions}
import com.github.shadowsocks.plugin._
import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment}
import com.github.shadowsocks.utils.{Action, Key, Utils}
......@@ -79,29 +80,21 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
pluginConfigure.setEnabled(!TextUtils.isEmpty(selected))
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
true
})
pluginConfigure.setOnPreferenceChangeListener((_, value) => try {
val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
true
} catch {
case exc: IllegalArgumentException =>
Snackbar.make(getActivity.findViewById(R.id.snackbar), exc.getLocalizedMessage, Snackbar.LENGTH_LONG).show()
false
})
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
initPlugins()
app.listenForPackageChanges(if (getView != null) getView.post(initPlugins))
app.listenForPackageChanges(initPlugins())
app.settings.registerOnSharedPreferenceChangeListener(this)
}
def initPlugins() {
val plugins = PluginManager.fetchPlugins()
plugin.setEntries(plugins.map(_.label))
plugin.setEntryValues(plugins.map(_.id.asInstanceOf[CharSequence]))
plugin.setEntryIcons(plugins.map(_.icon))
plugin.setEntries(plugins.map(_._2.label).toArray)
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
plugin.entryPackageNames = plugins.map(_._2.packageName).toArray
pluginConfiguration = new PluginConfiguration(app.settings.getString(Key.plugin, null))
plugin.setValue(pluginConfiguration.selected)
plugin.checkSummary()
......@@ -109,6 +102,18 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
}
private def onPluginConfigureChanged(p: Preference, value: Any) = try {
val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
true
} catch {
case exc: IllegalArgumentException =>
Snackbar.make(getActivity.findViewById(R.id.snackbar), exc.getLocalizedMessage, Snackbar.LENGTH_LONG).show()
false
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
......@@ -124,9 +129,9 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
override def onDisplayPreferenceDialog(preference: Preference): Unit = if (preference eq pluginConfigure) {
val selected = pluginConfiguration.selected
val intent = new Intent(PluginInterface.ACTION_CONFIGURE(selected))
val intent = PluginManager.buildIntent(selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(getActivity.getPackageManager) != null)
startActivityForResult(intent.putExtra(PluginInterface.EXTRA_OPTIONS,
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS,
pluginConfiguration.selectedOptions.toString), REQUEST_CODE_CONFIGURE) else {
val bundle = new Bundle()
bundle.putString(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, selected)
......@@ -134,6 +139,15 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
}
} else super.onDisplayPreferenceDialog(preference)
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
case REQUEST_CODE_CONFIGURE => if (resultCode == Activity.RESULT_OK) {
val options = data.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.setText(options)
onPluginConfigureChanged(null, options)
}
case _ => super.onActivityResult(requestCode, resultCode, data)
}
override def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_delete =>
new AlertDialog.Builder(getActivity)
......
package com.github.shadowsocks.plugin
import android.content.pm.{PackageManager, ProviderInfo}
import android.content.ContentResolver
import android.content.pm.{PackageManager, ResolveInfo}
import android.net.Uri
import android.os.Bundle
/**
* @author Mygod
*/
class NativePlugin(providerInfo: ProviderInfo, packageManager: PackageManager)
extends ResolvedPlugin(providerInfo, packageManager) {
assert(providerInfo != null)
assert(providerInfo.authority.startsWith(PluginInterface.AUTHORITY_BASE))
override final lazy val id: String =
providerInfo.authority.substring(PluginInterface.AUTHORITY_BASE.length)
override final lazy val defaultConfig: String =
providerInfo.metaData.getString(PluginInterface.METADATA_KEY_DEFAULT_CONFIG)
final class NativePlugin(resolveInfo: ResolveInfo, packageManager: PackageManager)
extends ResolvedPlugin(resolveInfo, packageManager) {
assert(resolveInfo.providerInfo != null)
override protected def metaData: Bundle = resolveInfo.providerInfo.metaData
private lazy val uriBuilder: Uri.Builder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(resolveInfo.providerInfo.authority)
}
......@@ -10,4 +10,5 @@ abstract class Plugin {
def label: CharSequence
def icon: Drawable = null
def defaultConfig: String = null
def packageName: String = null
}
......@@ -19,7 +19,7 @@ class PluginConfiguration(val pluginsOptions: Map[String, PluginOptions], val se
val args = mutable.Queue(Commandline.translateCommandline(line): _*)
args.dequeue()
while (args.nonEmpty) args.dequeue() match {
case "--nocomp" => opt.put("nocomp", "1")
case "--nocomp" => opt.put("nocomp", null)
case option if option.startsWith("--") => opt.put(option.substring(2), args.dequeue())
case option => throw new IllegalArgumentException("Unknown kcptun parameter: " + option)
}
......@@ -30,7 +30,10 @@ class PluginConfiguration(val pluginsOptions: Map[String, PluginOptions], val se
case line => new PluginOptions(line)
})
def getOptions(id: String): PluginOptions = if (id.isEmpty) new PluginOptions() else pluginsOptions(id)
def getOptions(id: String): PluginOptions = if (id.isEmpty) new PluginOptions() else pluginsOptions.get(id) match {
case Some(options) => options
case None => new PluginOptions(id, PluginManager.fetchPlugins()(id).defaultConfig)
}
def selectedOptions: PluginOptions = getOptions(selected)
override def toString: String = {
......@@ -39,6 +42,7 @@ class PluginConfiguration(val pluginsOptions: Map[String, PluginOptions], val se
case this.selected => result.prepend(opt)
case _ => result.append(opt)
}
if (!pluginsOptions.contains(selected)) result.prepend(selectedOptions)
result.map(_.toString(false)).mkString("\n")
}
}
......@@ -5,36 +5,43 @@ import java.io.{File, FileNotFoundException, FileOutputStream, IOException}
import android.content.pm.PackageManager
import android.content.{BroadcastReceiver, ContentResolver, Intent}
import android.net.Uri
import android.text.TextUtils
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils.autoClose
import com.github.shadowsocks.utils.IOUtils
import com.github.shadowsocks.utils.{Commandline, IOUtils}
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConverters._
import scala.collection.JavaConversions._
/**
* @author Mygod
*/
object PluginManager {
private var receiver: BroadcastReceiver = _
private var cachedPlugins: Array[Plugin] = _
def fetchPlugins(): Array[Plugin] = {
private var cachedPlugins: Map[String, Plugin] = _
def fetchPlugins(): Map[String, Plugin] = {
if (receiver == null) receiver = app.listenForPackageChanges(synchronized(cachedPlugins = null))
synchronized(if (cachedPlugins == null) {
val pm = app.getPackageManager
cachedPlugins = (NoPlugin +:
pm.queryContentProviders(null, 0, PackageManager.GET_META_DATA).asScala
.filter(_ != null)
.filter(_.authority.startsWith(PluginInterface.AUTHORITY_BASE))
.map(new NativePlugin(_, pm))).toArray
cachedPlugins = (
pm.queryIntentContentProviders(new Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
.map(new NativePlugin(_, pm)) :+
NoPlugin
).map(plugin => plugin.id -> plugin).toMap
})
cachedPlugins
}
private def buildUri(id: String) = new Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path('/' + id)
.build()
def buildIntent(id: String, action: String): Intent = new Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@throws[Throwable]
def initPlugin(id: String): String = {
if (TextUtils.isEmpty(id)) return null
if (id.isEmpty) return null
var throwable: Throwable = null
try initNativePlugin(id) match {
......@@ -56,32 +63,35 @@ object PluginManager {
@throws[IndexOutOfBoundsException]
@throws[AssertionError]
private def initNativePlugin(id: String): String = {
val providers = app.getPackageManager.queryIntentContentProviders(
new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(id)), 0)
assert(providers.length == 1)
val builder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(PluginInterface.getAuthority(id))
.authority(providers(0).providerInfo.authority)
val cr = app.getContentResolver
val cursor = cr.query(builder.build(), Array(PluginInterface.COLUMN_PATH), null, null, null)
val cursor = cr.query(builder.build(), Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
null, null, null)
if (cursor != null) {
var initialized = false
val pluginDir = new File(app.getFilesDir, "plugin")
if (cursor.moveToFirst()) {
try {
IOUtils.deleteRecursively(pluginDir)
} catch {
case ex: IOException => // Ignore
}
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.getAbsolutePath + '/'
do {
val path = cursor.getString(0)
val file = new File(pluginDir, path)
assert(file.getAbsolutePath.startsWith(pluginDirPath))
autoClose(cr.openInputStream(builder.path(path).build()))(in =>
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
if (path == id) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) throw new IndexOutOfBoundsException("Plugin entry binary not found")
def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
if (!cursor.moveToFirst()) entryNotFound()
IOUtils.deleteRecursively(pluginDir)
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.getAbsolutePath + '/'
var list: List[String] = Nil
do {
val path = cursor.getString(0)
val file = new File(pluginDir, path)
assert(file.getAbsolutePath.startsWith(pluginDirPath))
autoClose(cr.openInputStream(builder.path(path).build()))(in =>
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
list +:= Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
if (path == id) initialized = true
} while (cursor.moveToNext())
if (!initialized) entryNotFound()
Shell.SH.run(list)
new File(pluginDir, id).getAbsolutePath
} else null
}
......
package com.github.shadowsocks.plugin
import android.content.pm.{PackageManager, ProviderInfo}
import android.graphics.drawable.Drawable
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* Base class for any kind of plugin that can be used.
*
* @author Mygod
*/
abstract class ResolvedPlugin(providerInfo: ProviderInfo, packageManager: PackageManager = app.getPackageManager)
extends Plugin {
override final lazy val label: CharSequence = packageManager.getApplicationLabel(providerInfo.applicationInfo)
override final lazy val icon: Drawable = packageManager.getApplicationIcon(providerInfo.applicationInfo)
}
package com.github.shadowsocks.plugin
import android.content.pm.{PackageManager, ResolveInfo}
import android.graphics.drawable.Drawable
import android.os.Bundle
import com.github.shadowsocks.ShadowsocksApplication.app
/**
* Base class for any kind of plugin that can be used.
*
* @author Mygod
*/
abstract class ResolvedPlugin(resolveInfo: ResolveInfo, packageManager: PackageManager = app.getPackageManager)
extends Plugin {
protected def metaData: Bundle
override final lazy val id: String = metaData.getString(PluginContract.METADATA_KEY_ID)
override final lazy val label: CharSequence = resolveInfo.loadLabel(packageManager)
override final lazy val icon: Drawable = resolveInfo.loadIcon(packageManager)
override final lazy val defaultConfig: String = metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG)
override final def packageName: String = resolveInfo.resolvePackageName
}
package com.github.shadowsocks.preference
import android.content.{ActivityNotFoundException, Intent}
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.support.design.widget.BottomSheetDialog
import android.support.v14.preference.PreferenceDialogFragment
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.{LinearLayoutManager, RecyclerView}
import android.view.ViewGroup.LayoutParams
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.TextView
import android.widget.{ImageView, TextView}
import com.github.shadowsocks.R
/**
......@@ -19,6 +22,7 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
private lazy val preference = getPreference.asInstanceOf[IconListPreference]
private lazy val index: Int = preference.selectedEntry
private lazy val entries: Array[CharSequence] = preference.getEntries
private lazy val entryValues: Array[CharSequence] = preference.getEntryValues
private lazy val entryIcons: Array[Drawable] = preference.getEntryIcons
private var clickedIndex = -1
......@@ -39,30 +43,41 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
if (preference.callChangeListener(value)) preference.setValue(value)
}
private final class IconListViewHolder(val dialog: BottomSheetDialog, val view: TextView) extends ViewHolder(view)
with View.OnClickListener {
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
private var index: Int = _
{
val padding = getResources.getDimension(R.dimen.icon_list_item_padding).toInt
view.setPadding(padding, padding, padding, padding)
view.setCompoundDrawablePadding(padding)
view.setOnClickListener(this)
val typedArray = dialog.getContext.obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle()
}
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
view.setOnClickListener(this)
view.setOnLongClickListener(this)
def bind(i: Int, selected: Boolean = false) {
view.setText(entries(i))
view.setCompoundDrawablesWithIntrinsicBounds(entryIcons(i), null, null, null)
view.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
text1.setText(entries(i))
text2.setText(entryValues(i))
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface)
text2.setVisibility(if (entryValues(i).length > 0) View.VISIBLE else View.GONE)
icon.setImageDrawable(entryIcons(i))
index = i
}
def onClick(v: View) {
clickedIndex = index
dialog.dismiss()
}
override def onLongClick(v: View): Boolean = preference.entryPackageNames(index) match {
case null => false
case pn => try {
startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, new Uri.Builder()
.authority(Settings.AUTHORITY)
.opaquePart(pn)
.build()))
true
} catch {
case _: ActivityNotFoundException => false
}
}
}
private final class IconListAdapter(dialog: BottomSheetDialog) extends RecyclerView.Adapter[IconListViewHolder] {
def getItemCount: Int = entries.length
......@@ -72,6 +87,6 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
case _ => vh.bind(i - 1)
}
def onCreateViewHolder(vg: ViewGroup, i: Int): IconListViewHolder = new IconListViewHolder(dialog,
LayoutInflater.from(vg.getContext).inflate(android.R.layout.simple_list_item_1, vg, false).asInstanceOf[TextView])
LayoutInflater.from(vg.getContext).inflate(R.layout.icon_list_item_2, vg, false))
}
}
......@@ -19,6 +19,7 @@ class IconListPreference(context: Context, attrs: AttributeSet = null) extends L
def selectedEntry: Int = getEntryValues.indexOf(getValue)
var unknownValueSummary: String = _
var entryPackageNames: Array[String] = _
private var listener: OnPreferenceChangeListener = _
override def getOnPreferenceChangeListener: OnPreferenceChangeListener = listener
......
......@@ -23,7 +23,7 @@ package com.github.shadowsocks.preference
import android.app.{Activity, AlertDialog}
import android.content.{DialogInterface, Intent}
import be.mygod.preference.EditTextPreferenceDialogFragment
import com.github.shadowsocks.plugin.PluginInterface
import com.github.shadowsocks.plugin.{PluginContract, PluginManager}
/**
* @author Mygod
......@@ -38,9 +38,9 @@ class PluginConfigurationDialogFragment extends EditTextPreferenceDialogFragment
override def onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
val intent = new Intent(PluginInterface.ACTION_HELP(getArguments.getString(PLUGIN_ID_FRAGMENT_TAG)))
val intent = PluginManager.buildIntent(getArguments.getString(PLUGIN_ID_FRAGMENT_TAG), PluginContract.ACTION_HELP)
if (intent.resolveActivity(getActivity.getPackageManager) != null) builder.setNeutralButton("?", ((_, _) =>
startActivityForResult(intent.putExtra(PluginInterface.EXTRA_OPTIONS, editText.getText.toString),
startActivityForResult(intent.putExtra(PluginContract.EXTRA_OPTIONS, editText.getText.toString),
REQUEST_CODE_HELP)): DialogInterface.OnClickListener)
}
......@@ -48,7 +48,7 @@ class PluginConfigurationDialogFragment extends EditTextPreferenceDialogFragment
case REQUEST_CODE_HELP => requestCode match {
case Activity.RESULT_OK => new AlertDialog.Builder(getContext)
.setTitle("?")
.setMessage(data.getCharSequenceExtra(PluginInterface.EXTRA_HELP_MESSAGE))
.setMessage(data.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))
.show()
case _ =>
}
......
......@@ -41,7 +41,7 @@ object IOUtils {
autoClose(new FileWriter(file))(writer => writer.write(content))
@throws[FileNotFoundException]
def deleteRecursively(file: File) {
def deleteRecursively(file: File): Unit = if (file.exists()) {
if (file.isDirectory) file.listFiles().foreach(deleteRecursively)
if (!file.delete()) throw new FileNotFoundException("Failed to delete: " + file.getAbsolutePath)
}
......
package com.github.shadowsocks.plugin;
/**
* This class provides String constants that will be used in plugin interfaces.
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
*
* This class is written in Java to keep Java interoperability.
*
* @author Mygod
*/
public final class PluginInterface {
private PluginInterface() { }
public final class PluginContract {
private PluginContract() { }
/**
* Should be a NativePluginProvider.
* ContentProvider Action: Used for NativePluginProvider.
*
* Constant Value: "com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN"
* Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
*/
public static final String CATEGORY_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN";
public static final String ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN";
/**
* Activity Action: Used for ConfigurationActivity.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
*/
public static final String ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE";
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
*/
public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP";
/**
* The lookup key for a string that provides the whole command as a string.
......@@ -35,6 +48,12 @@ public final class PluginInterface {
*/
public static final String EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE";
/**
* The metadata key to retrieve plugin id. Required for plugins.
*
* Constant Value: "com.github.shadowsocks.plugin.id"
*/
public static final String METADATA_KEY_ID = "com.github.shadowsocks.plugin.id";
/**
* The metadata key to retrieve default configuration. Default value is empty.
*
......@@ -50,35 +69,21 @@ public final class PluginInterface {
* Type: String
*/
public static final String COLUMN_PATH = "path";
static final String AUTHORITY_BASE = "com.github.shadowsocks.plugin.";
/**
* Authority to use for native plugin ContentProvider.
* File mode bits. Default value is "644".
*
* Example: "755"
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID
* Type: String
*/
public static String getAuthority(String pluginId) {
return AUTHORITY_BASE + pluginId;
}
public static final String COLUMN_MODE = "mode";
/**
* Activity Action: Used for ConfigurationActivity.
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE
* The scheme for general plugin actions.
*/
public static String ACTION_CONFIGURE(String pluginId) {
return getAuthority(pluginId) + ".ACTION_CONFIGURE";
}
public static final String SCHEME = "plugin";
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* @param pluginId Plugin ID.
* @return com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP
* The authority for general plugin actions.
*/
public static String ACTION_HELP(String pluginId) {
return getAuthority(pluginId) + ".ACTION_HELP";
}
public static final String AUTHORITY = "com.github.shadowsocks";
}
......@@ -25,8 +25,9 @@ public final class PluginOptions extends HashMap<String, String> {
}
private PluginOptions(String options, boolean parseId) {
this();
if (TextUtils.isEmpty(options)) return;
final StringTokenizer tokenizer = new StringTokenizer(options, "\\=;", true);
final StringTokenizer tokenizer = new StringTokenizer(options + ';', "\\=;", true);
final StringBuilder current = new StringBuilder();
String key = null;
while (tokenizer.hasMoreTokens()) {
......@@ -35,18 +36,15 @@ public final class PluginOptions extends HashMap<String, String> {
else if ("=".equals(nextToken) && key == null) {
key = current.toString();
current.setLength(0);
} else if (";".equals(nextToken))
} else if (";".equals(nextToken)) {
if (key != null) {
put(key, current.toString());
key = null;
} else if (parseId) {
id = current.toString();
parseId = false;
} else {
put(current.toString(), null);
current.setLength(0);
}
else current.append(nextToken);
} else if (current.length() > 0)
if (parseId) id = current.toString(); else put(current.toString(), null);
current.setLength(0);
parseId = false;
} else current.append(nextToken);
}
}
public PluginOptions(String options) {
......@@ -57,7 +55,7 @@ public final class PluginOptions extends HashMap<String, String> {
this.id = id;
}
public String id;
public String id = "";
private static void append(StringBuilder result, String str) {
for (int i = 0; i < str.length(); ++i) {
......
......@@ -35,5 +35,5 @@ trait ConfigurationActivity extends OptionsCapableActivity {
* @param options PluginOptions to save.
*/
final def saveChanges(options: PluginOptions): Unit =
setResult(Activity.RESULT_OK, new Intent().putExtra(PluginInterface.EXTRA_OPTIONS, options.toString))
setResult(Activity.RESULT_OK, new Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString))
}
......@@ -14,5 +14,5 @@ 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)))
new Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
}
......@@ -17,7 +17,7 @@ import android.os.ParcelFileDescriptor
* &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.CATEGORY_NATIVE_PLUGIN" /&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
......@@ -47,7 +47,7 @@ abstract class NativePluginProvider extends ContentProvider {
override def query(uri: Uri, projection: Array[String], selection: String, selectionArgs: Array[String],
sortOrder: String): Cursor = {
assert(selection == null && selectionArgs == null && sortOrder == null)
val result = new MatrixCursor(projection.filter(_ == PluginInterface.COLUMN_PATH))
val result = new MatrixCursor(projection)
populateFiles(new PathProvider(uri, result))
result
}
......
......@@ -12,7 +12,7 @@ import android.widget.Toast
*/
trait OptionsCapableActivity extends Activity {
protected def pluginOptions(intent: Intent = getIntent): PluginOptions =
try new PluginOptions(intent.getStringExtra(PluginInterface.EXTRA_OPTIONS)) catch {
try new PluginOptions(intent.getStringExtra(PluginContract.EXTRA_OPTIONS)) catch {
case exc: IllegalArgumentException =>
Toast.makeText(this, exc.getMessage, Toast.LENGTH_SHORT).show()
null
......
......@@ -16,21 +16,24 @@ final class PathProvider private[plugin](baseUri: Uri, cursor: MatrixCursor) {
case p => p.stripPrefix("/").stripSuffix("/")
}
def addPath(path: String): PathProvider = {
def addPath(path: String, mode: String = "644"): PathProvider = {
val stripped = path.stripPrefix("/").stripSuffix("/")
if (stripped.startsWith(basePath)) cursor.newRow().add(PluginInterface.COLUMN_PATH, stripped)
if (stripped.startsWith(basePath)) cursor.newRow()
.add(PluginContract.COLUMN_PATH, stripped)
.add(PluginContract.COLUMN_MODE, mode)
this
}
def addTo(file: File, to: String = ""): PathProvider = {
def addTo(file: File, to: String = "", mode: String = "644"): PathProvider = {
var sub = to + file.getName
if (basePath.startsWith(sub)) if (file.isDirectory) {
sub += '/'
file.listFiles().foreach(addTo(_, sub))
} else addPath(sub)
file.listFiles().foreach(addTo(_, sub, mode))
} else addPath(sub, mode)
this
}
def addAt(file: File, at: String = ""): PathProvider = {
if (basePath.startsWith(at)) if (file.isDirectory) file.listFiles().foreach(addTo(_, at)) else addPath(at)
def addAt(file: File, at: String = "", mode: String = "644"): PathProvider = {
if (basePath.startsWith(at))
if (file.isDirectory) file.listFiles().foreach(addTo(_, at, mode)) else addPath(at, mode)
this
}
}
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.2")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.3")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
......
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