diff --git a/res/values/translatable.xml b/res/values/translatable.xml
index 9e767407..ce5c5512 100644
--- a/res/values/translatable.xml
+++ b/res/values/translatable.xml
@@ -315,4 +315,7 @@ THE SOFTWARE.
Scan progress
Scanning media library…
+
+
+ Plugins
diff --git a/src/ch/blinkenlights/android/vanilla/LibraryActivity.java b/src/ch/blinkenlights/android/vanilla/LibraryActivity.java
index b1136c27..f14c78d2 100644
--- a/src/ch/blinkenlights/android/vanilla/LibraryActivity.java
+++ b/src/ch/blinkenlights/android/vanilla/LibraryActivity.java
@@ -26,9 +26,14 @@ package ch.blinkenlights.android.vanilla;
import ch.blinkenlights.android.medialibrary.MediaLibrary;
import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -38,7 +43,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
-import android.provider.MediaStore;
import android.support.iosched.tabs.VanillaTabLayout;
import android.support.v4.view.ViewPager;
import android.text.TextUtils;
@@ -56,6 +60,11 @@ import android.widget.TextView;
import android.widget.SearchView;
import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
import junit.framework.Assert;
/**
@@ -139,6 +148,19 @@ public class LibraryActivity
* open the playback activity when an item is pressed twice.
*/
private long mLastActedId;
+ /**
+ * Holds last intent that was passed to the context menu
+ */
+ private Intent mLastRequestedCtx;
+ /**
+ * Plugin descriptions and packages.
+ * Keys are plugin names, values are their packages
+ */
+ private Map mPlugins = new HashMap<>();
+ /**
+ * Broadcast receiver for plugin collecting
+ */
+ private BroadcastReceiver mPluginInfoReceiver;
/**
* The pager adapter that manages each media ListView.
*/
@@ -172,6 +194,7 @@ public class LibraryActivity
mViewPager = pager;
SharedPreferences settings = PlaybackService.getSettings(this);
+ mPluginInfoReceiver = new PluginBroadcastReceiver();
mBottomBarControls = (BottomBarControls)findViewById(R.id.bottombar_controls);
mBottomBarControls.setOnClickListener(this);
@@ -195,6 +218,43 @@ public class LibraryActivity
bindControlButtons();
}
+ /**
+ * Shows plugin selection dialog. Be sure that {@link #mLastRequestedCtx} is initialized
+ * and {@link #mPlugins} are populated before calling this.
+ *
+ * @see PluginBroadcastReceiver
+ */
+ private void showPluginMenu() {
+ Set pluginNames = mPlugins.keySet();
+ final String[] pNamesArr = pluginNames.toArray(new String[pluginNames.size()]);
+ new AlertDialog.Builder(this)
+ .setItems(pNamesArr, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ long id = mLastRequestedCtx.getLongExtra("id", LibraryAdapter.INVALID_ID);
+ Song resolved = MediaUtils.getSongByTypeId(LibraryActivity.this, MediaUtils.TYPE_SONG, id);
+ if (resolved != null) {
+ ApplicationInfo selected = mPlugins.get(pNamesArr[which]);
+ Intent request = new Intent(PluginUtils.ACTION_LAUNCH_PLUGIN);
+ request.setPackage(selected.packageName);
+ request.putExtra(PluginUtils.EXTRA_PARAM_URI, Uri.fromFile(new File(resolved.path)));
+ request.putExtra(PluginUtils.EXTRA_PARAM_SONG_TITLE, resolved.title);
+ request.putExtra(PluginUtils.EXTRA_PARAM_SONG_ARTIST, resolved.artist);
+ request.putExtra(PluginUtils.EXTRA_PARAM_SONG_ALBUM, resolved.album);
+ startService(request);
+ }
+
+ mLastRequestedCtx = null;
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mLastRequestedCtx = null;
+ }
+ }).create().show();
+ }
+
@Override
public void onRestart()
{
@@ -213,6 +273,18 @@ public class LibraryActivity
updateHeaders();
}
+ @Override
+ public void onResume() {
+ super.onResume();
+ registerReceiver(mPluginInfoReceiver, new IntentFilter(PluginUtils.ACTION_HANDLE_PLUGIN_PARAMS));
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ unregisterReceiver(mPluginInfoReceiver);
+ }
+
/**
* Load the tab order and update the tab bars if needed.
*/
@@ -593,6 +665,7 @@ public class LibraryActivity
private static final int CTX_MENU_MORE_FROM_ALBUM = 8;
private static final int CTX_MENU_MORE_FROM_ARTIST = 9;
private static final int CTX_MENU_OPEN_EXTERNAL = 10;
+ private static final int CTX_MENU_PLUGINS = 11;
/**
* Creates a context menu for an adapter row.
@@ -627,8 +700,11 @@ public class LibraryActivity
}
if (type == MediaUtils.TYPE_ALBUM || type == MediaUtils.TYPE_SONG)
menu.add(0, CTX_MENU_MORE_FROM_ARTIST, 0, R.string.more_from_artist).setIntent(rowData);
- if (type == MediaUtils.TYPE_SONG)
+ if (type == MediaUtils.TYPE_SONG) {
menu.add(0, CTX_MENU_MORE_FROM_ALBUM, 0, R.string.more_from_album).setIntent(rowData);
+ if (PluginUtils.checkPlugins(this))
+ menu.add(0, CTX_MENU_PLUGINS, 1, R.string.plugins).setIntent(rowData); // last in order
+ }
menu.addSubMenu(0, CTX_MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist).getItem().setIntent(rowData);
menu.add(0, CTX_MENU_DELETE, 0, R.string.delete).setIntent(rowData);
}
@@ -709,6 +785,15 @@ public class LibraryActivity
FileUtils.dispatchIntent(this, intent);
break;
}
+ case CTX_MENU_PLUGINS: {
+ // obtain list of plugins anew - some plugins may be installed/deleted
+ mPlugins.clear();
+ mLastRequestedCtx = intent;
+ Intent requestPlugins = new Intent(PluginUtils.ACTION_REQUEST_PLUGIN_PARAMS);
+ intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+ sendBroadcast(requestPlugins);
+ break;
+ }
case CTX_MENU_MORE_FROM_ARTIST: {
String selection;
if (intent.getIntExtra(LibraryAdapter.DATA_TYPE, -1) == MediaUtils.TYPE_ALBUM) {
@@ -923,4 +1008,20 @@ public class LibraryActivity
}
}
+ private class PluginBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ List resolved = getPackageManager().queryBroadcastReceivers(new Intent(PluginUtils.ACTION_REQUEST_PLUGIN_PARAMS), 0);
+ if (PluginUtils.ACTION_HANDLE_PLUGIN_PARAMS.equals(intent.getAction())) {
+ // plugin answered, store it in the map
+ String pluginName = intent.getStringExtra(PluginUtils.EXTRA_PARAM_PLUGIN_NAME);
+ ApplicationInfo info = intent.getParcelableExtra(PluginUtils.EXTRA_PARAM_PLUGIN_APP);
+ mPlugins.put(pluginName, info);
+
+ if (mPlugins.size() == resolved.size()) { // got all plugins
+ showPluginMenu();
+ }
+ }
+ }
+ }
}
diff --git a/src/ch/blinkenlights/android/vanilla/PluginUtils.java b/src/ch/blinkenlights/android/vanilla/PluginUtils.java
new file mode 100644
index 00000000..2cd57044
--- /dev/null
+++ b/src/ch/blinkenlights/android/vanilla/PluginUtils.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 Oleg Chernovskiy
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package ch.blinkenlights.android.vanilla;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import java.util.List;
+
+/**
+ * Common plugin utilities and constants reside here.
+ * Note that public `action` and `extra` strings are common over all plugins, so please keep them in sync.
+ */
+
+public class PluginUtils {
+
+ private PluginUtils() {
+ }
+
+ // these actions are for passing between main player and plugins
+ public static final String ACTION_REQUEST_PLUGIN_PARAMS = "ch.blinkenlights.android.vanilla.action.REQUEST_PLUGIN_PARAMS"; // broadcast
+ public static final String ACTION_HANDLE_PLUGIN_PARAMS = "ch.blinkenlights.android.vanilla.action.HANDLE_PLUGIN_PARAMS"; // answer
+ public static final String ACTION_WAKE_PLUGIN = "ch.blinkenlights.android.vanilla.action.WAKE_PLUGIN"; // targeted at each found
+ public static final String ACTION_LAUNCH_PLUGIN = "ch.blinkenlights.android.vanilla.action.LAUNCH_PLUGIN"; // targeted at selected by user
+
+ // these are used by plugins to describe themselves
+ public static final String EXTRA_PARAM_PLUGIN_NAME = "ch.blinkenlights.android.vanilla.extra.PLUGIN_NAME";
+ public static final String EXTRA_PARAM_PLUGIN_APP = "ch.blinkenlights.android.vanilla.extra.PLUGIN_APP";
+ public static final String EXTRA_PARAM_PLUGIN_DESC = "ch.blinkenlights.android.vanilla.extra.PLUGIN_DESC";
+
+ // this is passed to plugin when it is selected by user
+ public static final String EXTRA_PARAM_URI = "ch.blinkenlights.android.vanilla.extra.URI";
+ public static final String EXTRA_PARAM_SONG_TITLE = "ch.blinkenlights.android.vanilla.extra.SONG_TITLE";
+ public static final String EXTRA_PARAM_SONG_ALBUM = "ch.blinkenlights.android.vanilla.extra.SONG_ALBUM";
+ public static final String EXTRA_PARAM_SONG_ARTIST = "ch.blinkenlights.android.vanilla.extra.SONG_ARTIST";
+
+ static final String EXTRA_PLUGIN_MAP = "ch.blinkenlights.android.vanilla.internal.extra.PLUGIN_MAP";
+
+ public static boolean checkPlugins(Context ctx) {
+ List resolved = ctx.getPackageManager().queryBroadcastReceivers(new Intent(ACTION_REQUEST_PLUGIN_PARAMS), 0);
+ if(!resolved.isEmpty()) {
+ // If plugin is just installed, Android will not deliver intents to its receiver
+ // until it's started at least one time
+ for (ResolveInfo ri : resolved) {
+ Intent awaker = new Intent(ACTION_WAKE_PLUGIN);
+ awaker.setPackage(ri.activityInfo.packageName);
+ ctx.startService(awaker);
+ }
+ return true;
+ }
+
+ return false;
+ }
+}