From 58b40d862c20953125ead164cf7ae1d8b9f17844 Mon Sep 17 00:00:00 2001 From: Antic1tizen One Date: Sun, 8 Jan 2017 22:52:07 +0400 Subject: [PATCH] Plugin subsystem. Shown as a last option in song context menu. Part of #407 (#442) --- res/values/translatable.xml | 3 + .../android/vanilla/LibraryActivity.java | 105 +++++++++++++++++- .../android/vanilla/PluginUtils.java | 69 ++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/ch/blinkenlights/android/vanilla/PluginUtils.java 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; + } +}