Simplify plugin package search. Fix plugin detection. (#926)

Currently plugin searching still works in two stages, first it searches
for all plugin broadcast receivers and then waits for all the answers
to complete the plugin map. If any of plugins doesn't answer the call
this means all of them won't be shown and that's not very user-friendly.

The change simplifies plugin search to just package scan for specific
broadcast receivers. Android provides package name and app info for it.
We don't need any other info from plugins, so we don't need to wait for
broadcast roadtrip.

Further improvements:
* introduce meta-data and uses-feature tags in
plugin manifests and get rid of broadcast receiver completely.
* Implement view in settings activity with list of installed plugins
This commit is contained in:
Kanedias 2019-02-28 23:30:26 +03:00 committed by Adrian Ulrich
parent 0b3c1ef5d8
commit 50d8d43b54
4 changed files with 56 additions and 107 deletions

View File

@ -410,7 +410,7 @@ public class FullPlaybackActivity extends SlidingPlaybackActivity
if (song != null) {
Intent songIntent = new Intent();
songIntent.putExtra("id", song.id);
queryPluginsForIntent(songIntent);
showPluginMenu(songIntent);
}
break;
default:

View File

@ -771,7 +771,7 @@ public class LibraryActivity
TrackDetailsDialog.show(getFragmentManager(), songId);
break;
case CTX_MENU_PLUGINS: {
queryPluginsForIntent(intent);
showPluginMenu(intent);
break;
}
case CTX_MENU_MORE_FROM_ARTIST: {

View File

@ -25,22 +25,14 @@ package ch.blinkenlights.android.vanilla;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.annotation.SuppressLint;
import android.app.Activity;
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.media.AudioManager;
import android.net.Uri;
@ -72,11 +64,6 @@ public abstract class PlaybackActivity extends Activity
View.OnClickListener,
CoverView.Callback
{
/**
* This constant is unavailable on Android < N.
* For newer versions the constant is {@link Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND}
*/
private static final int FLAG_RECEIVER_INCLUDE_BACKGROUND = 0x01000000;
private Action mUpAction;
private Action mDownAction;
@ -94,19 +81,6 @@ public abstract class PlaybackActivity extends Activity
* The looper for the worker thread.
*/
protected Looper mLooper;
/**
* Broadcast receiver for plugin collecting
*/
private BroadcastReceiver mPluginInfoReceiver;
/**
* 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<String, ApplicationInfo> mPlugins = new HashMap<>();
protected CoverView mCoverView;
protected ImageButton mPlayPauseButton;
@ -124,8 +98,6 @@ public abstract class PlaybackActivity extends Activity
PlaybackService.addTimelineCallback(this);
mPluginInfoReceiver = new PluginBroadcastReceiver();
setVolumeControlStream(AudioManager.STREAM_MUSIC);
HandlerThread thread = new HandlerThread(getClass().getName(), Process.THREAD_PRIORITY_LOWEST);
@ -180,7 +152,6 @@ public abstract class PlaybackActivity extends Activity
public void onResume()
{
super.onResume();
registerReceiver(mPluginInfoReceiver, new IntentFilter(PluginUtils.ACTION_HANDLE_PLUGIN_PARAMS));
if (PlaybackService.hasInstance()) {
PlaybackService service = PlaybackService.get(this);
service.userActionTriggered();
@ -188,12 +159,6 @@ public abstract class PlaybackActivity extends Activity
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mPluginInfoReceiver);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
@ -737,89 +702,36 @@ public abstract class PlaybackActivity extends Activity
}
/**
* Sends broadcast query that wakes plugins are queries them for info.
* Answers are later processed in {@link PluginBroadcastReceiver}
*
* @param songIntent intent containing song id as {@link Long} in its "id" extra.
* Queries all plugin packages and shows plugin selection dialog.
* @param intent intent with a song to send to plugins
*/
@SuppressLint("WrongConstant") // flag is ignored on Android < 7.0
protected void queryPluginsForIntent(Intent songIntent) {
// obtain list of plugins anew - some plugins may be installed/deleted
mPlugins.clear();
mLastRequestedCtx = songIntent;
Intent requestPlugins = new Intent(PluginUtils.ACTION_REQUEST_PLUGIN_PARAMS);
requestPlugins.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
requestPlugins.addFlags(FLAG_RECEIVER_INCLUDE_BACKGROUND);
sendBroadcast(requestPlugins);
}
protected void showPluginMenu(final Intent intent) {
final Map<String, ApplicationInfo> plugins = PluginUtils.getPluginMap(this);
final String[] pNamesArr = plugins.keySet().toArray(new String[0]);
/**
* Broadcast receiver that handles return intents from queried plugins.
* Answered ones are stored in {@link #mPlugins}.
* Shows plugin menu once all queried plugins have answered.
*
* @see #showPluginMenu()
*/
private class PluginBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
List<ResolveInfo> 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();
}
}
}
}
/**
* 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<String> 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) {
if (mLastRequestedCtx == null) {
return;
}
long id = mLastRequestedCtx.getLongExtra("id", LibraryAdapter.INVALID_ID);
Song resolved = MediaUtils.getSongByTypeId(PlaybackActivity.this, MediaUtils.TYPE_SONG, id);
if (resolved != null) {
ApplicationInfo selected = mPlugins.get(pNamesArr[which]);
long id = intent.getLongExtra("id", LibraryAdapter.INVALID_ID);
Song song = MediaUtils.getSongByTypeId(PlaybackActivity.this, MediaUtils.TYPE_SONG, id);
if (song != null) {
ApplicationInfo selected = plugins.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);
request.putExtra(PluginUtils.EXTRA_PARAM_URI, Uri.fromFile(new File(song.path)));
request.putExtra(PluginUtils.EXTRA_PARAM_SONG_TITLE, song.title);
request.putExtra(PluginUtils.EXTRA_PARAM_SONG_ARTIST, song.artist);
request.putExtra(PluginUtils.EXTRA_PARAM_SONG_ALBUM, song.album);
if (request.resolveActivity(getPackageManager()) != null) {
startActivity(request);
} else {
Log.e("PluginSystem", "Couldn't start plugin activity for " + request);
}
}
mLastRequestedCtx = null;
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mLastRequestedCtx = null;
}
}).create().show();
.create().show();
}
}

View File

@ -18,11 +18,15 @@ package ch.blinkenlights.android.vanilla;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Common plugin utilities and constants reside here.
@ -52,9 +56,42 @@ public class PluginUtils {
static final String EXTRA_PLUGIN_MAP = "ch.blinkenlights.android.vanilla.internal.extra.PLUGIN_MAP";
/**
* Find all vanilla plugins by their very specific broadcast receiver intent.
* @param ctx context to use when resolving packages.
* @return list of all resolved plugins, may be empty but never null
*/
@NonNull
private static List<ResolveInfo> resolvePlugins(Context ctx) {
PackageManager pm = ctx.getPackageManager();
Intent filter = new Intent(ACTION_REQUEST_PLUGIN_PARAMS);
return pm.queryBroadcastReceivers(filter, PackageManager.GET_DISABLED_COMPONENTS);
}
/**
* Checks whether does any of vanilla plugins exist on this Android system
* @param ctx context to use when resolving packages
* @return true if at least one plugin is installed, false otherwise
*/
public static boolean checkPlugins(Context ctx) {
PackageManager pm = ctx.getPackageManager();
List<ResolveInfo> resolved = pm.queryBroadcastReceivers(new Intent(ACTION_REQUEST_PLUGIN_PARAMS), 0);
return !resolved.isEmpty();
return !resolvePlugins(ctx).isEmpty();
}
/**
* Same as {@link #resolvePlugins(Context)} but in a map convenient for showing in dialogs.
* @param ctx ctx context to use when resolving packages
* @return all vanilla plugins in a map App Name <-> App Info
*/
@NonNull
public static Map<String, ApplicationInfo> getPluginMap(Context ctx) {
PackageManager pm = ctx.getPackageManager();
List<ResolveInfo> resolved = resolvePlugins(ctx);
Map<String, ApplicationInfo> pluginMap = new HashMap<>(resolved.size());
for (ResolveInfo ri : resolved) {
ApplicationInfo appInfo = ri.activityInfo.applicationInfo;
pluginMap.put(appInfo.loadLabel(pm).toString(), appInfo);
}
return pluginMap;
}
}