/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* 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 <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.framework;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.fastbootmobile.encore.app.R;
import com.fastbootmobile.encore.art.ImageCache;
import com.fastbootmobile.encore.art.RecyclingBitmapDrawable;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.providers.AbstractProviderConnection;
import com.fastbootmobile.encore.providers.Constants;
import com.fastbootmobile.encore.providers.DSPConnection;
import com.fastbootmobile.encore.providers.IMusicProvider;
import com.fastbootmobile.encore.providers.InjectedProviderConnection;
import com.fastbootmobile.encore.providers.MultiProviderPlaylistProvider;
import com.fastbootmobile.encore.providers.ProviderConnection;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import com.fastbootmobile.encore.service.IPlaybackService;
import com.fastbootmobile.encore.service.NativeHub;
import com.fastbootmobile.encore.service.PlaybackService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Plugins Manager class
*/
public class PluginsLookup {
private static final boolean DEBUG = false;
private static final String TAG = "PluginsLookup";
public static final int BUNDLED_PROVIDERS_COUNT = 2; // Local and Multi-provider
public static final String DATA_PACKAGE = "package";
public static final String DATA_SERVICE = "service";
public static final String DATA_NAME = "name";
public static final String DATA_AUTHOR = "author";
public static final String DATA_CONFIGCLASS = "configclass";
private static final String PREFS_PLUGINS = "plugins";
private static final String PREF_KNOWN_PLUGINS = "known_plugins";
public interface ConnectionListener {
void onServiceConnected(AbstractProviderConnection connection);
void onServiceDisconnected(AbstractProviderConnection connection);
}
private Context mContext;
private final List<ProviderConnection> mConnections;
private final List<DSPConnection> mDSPConnections;
private List<ConnectionListener> mConnectionListeners;
private IPlaybackService mPlaybackService;
private MultiProviderPlaylistProvider mMultiProviderPlaylistProvider;
private ProviderConnection mMultiProviderConnection;
private Handler mHandler;
private int mServiceUsage;
private Set<ProviderConnection> mNewServices;
private ServiceConnection mPlaybackConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mPlaybackService = IPlaybackService.Stub.asInterface(service);
PlaybackProxy.notifyPlaybackConnected();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mPlaybackService = null;
}
};
private ConnectionListener mProviderListener = new ConnectionListener() {
@Override
public void onServiceConnected(AbstractProviderConnection connection) {
for (ConnectionListener listener : mConnectionListeners) {
listener.onServiceConnected(connection);
}
}
@Override
public void onServiceDisconnected(AbstractProviderConnection connection) {
for (ConnectionListener listener : mConnectionListeners) {
listener.onServiceDisconnected(connection);
}
}
};
private Runnable mShutdownPlaybackRunnable = new Runnable() {
@Override
public void run() {
mContext.stopService(new Intent(mContext, PlaybackService.class));
mContext.unbindService(mPlaybackConnection);
mPlaybackService = null;
}
};
private static final PluginsLookup INSTANCE = new PluginsLookup();
public static PluginsLookup getDefault() {
return INSTANCE;
}
private PluginsLookup() {
mConnections = new ArrayList<>();
mDSPConnections = new ArrayList<>();
mConnectionListeners = new ArrayList<>();
mNewServices = new TreeSet<>();
mHandler = new Handler();
}
public void initialize(Context context) {
mContext = context;
requestUpdatePlugins();
injectMultiProviderPlaylistProvider();
connectPlayback();
}
private void injectMultiProviderPlaylistProvider() {
// Inject our Multi-Provider Playlist provider
mMultiProviderPlaylistProvider = new MultiProviderPlaylistProvider(mContext);
mMultiProviderConnection = injectProvider(mMultiProviderPlaylistProvider,
"com.fastbootmobile.encore.providers", "com.fastbootmobile.encore.providers.MultiProviderPlaylistProvider",
mContext.getString(R.string.multiprovider_name), "The OmniROM Project", null);
}
private InjectedProviderConnection injectProvider(IMusicProvider provider, String pkg,
String service, String name, String author,
String configClass) {
InjectedProviderConnection pc = new InjectedProviderConnection(provider, mContext, name,
author, pkg, service, configClass);
pc.setListener(mProviderListener);
synchronized (mConnections) {
mConnections.add(pc);
}
return pc;
}
public void incPlaybackUsage() {
mServiceUsage++;
connectPlayback();
}
public void decPlaybackUsage() {
mServiceUsage--;
if (mServiceUsage == 0) {
releasePlaybackServiceIfPossible();
}
}
public void requestUpdatePlugins() {
new Thread() {
public void run() {
updatePlugins();
}
}.start();
}
public void registerProviderListener(ConnectionListener listener) {
mConnectionListeners.add(listener);
}
public void removeProviderListener(ConnectionListener listener) {
mConnectionListeners.remove(listener);
}
public ProviderConnection getMultiProviderPlaylistProvider(){
return mMultiProviderConnection;
}
/**
* Connects the app to the playback service
*/
private void connectPlayback() {
mHandler.removeCallbacks(mShutdownPlaybackRunnable);
if (mPlaybackService == null) {
Intent i = new Intent(mContext, PlaybackService.class);
mContext.startService(i);
mContext.bindService(i, mPlaybackConnection, Context.BIND_ABOVE_CLIENT);
}
}
/**
* Update the list of plugins
*/
public void updatePlugins() {
fetchProviders();
fetchDSPs();
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_PLUGINS, 0);
Set<String> knownPlugins = new TreeSet<>(prefs.getStringSet(PREF_KNOWN_PLUGINS, new TreeSet<String>()));
if (knownPlugins.size() == 0) {
// First start, fill in all plugins
synchronized (mConnections) {
for (ProviderConnection connection : mConnections) {
knownPlugins.add(connection.getServiceName());
}
}
prefs.edit().putStringSet(PREF_KNOWN_PLUGINS, knownPlugins).apply();
} else {
// Check if there's any new plugin
synchronized (mConnections) {
for (ProviderConnection connection : mConnections) {
if (!knownPlugins.contains(connection.getServiceName())) {
knownPlugins.add(connection.getServiceName());
mNewServices.add(connection);
}
}
}
prefs.edit().putStringSet(PREF_KNOWN_PLUGINS, knownPlugins).apply();
}
}
public Set<ProviderConnection> getNewPlugins() {
return mNewServices;
}
public void resetNewPlugins() {
mNewServices.clear();
}
public void releasePlaybackServiceIfPossible() {
if (mPlaybackService != null) {
final int state = PlaybackProxy.getState();
if (state == PlaybackService.STATE_PAUSED || state == PlaybackService.STATE_STOPPED) {
releasePlaybackService();
}
}
}
private void releasePlaybackService() {
if (mPlaybackService != null) {
mHandler.removeCallbacks(mShutdownPlaybackRunnable);
mHandler.postDelayed(mShutdownPlaybackRunnable, 1000);
}
}
public void tearDown(NativeHub hub) {
Log.i(TAG, "tearDown()");
releasePlaybackService();
synchronized (mConnections) {
for (ProviderConnection connection : mConnections) {
connection.unbindService(hub);
}
for (DSPConnection connection : mDSPConnections) {
connection.unbindService(hub);
}
}
}
IPlaybackService getPlaybackService() {
return getPlaybackService(true);
}
IPlaybackService getPlaybackService(boolean connectIfUnavailable) {
if (mPlaybackService == null && connectIfUnavailable) {
connectPlayback();
}
return mPlaybackService;
}
public ProviderConnection getProvider(ProviderIdentifier id) {
if (id == null) {
Log.e(TAG, "getProvider called with null identifier");
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stack) {
Log.e(TAG, element.toString());
}
return null;
}
synchronized (mConnections) {
for (ProviderConnection connection : mConnections) {
if (connection.getIdentifier().equals(id)) {
return connection;
}
}
}
return null;
}
public ProviderConnection getProviderByName(final String name) {
synchronized (mConnections) {
for (ProviderConnection connection : mConnections) {
if (connection.getProviderName().equalsIgnoreCase(name)) {
return connection;
}
}
}
return null;
}
public DSPConnection getDSP(ProviderIdentifier id) {
synchronized (mDSPConnections) {
for (DSPConnection connection : mDSPConnections) {
if (connection.getIdentifier().equals(id)) {
return connection;
}
}
}
return null;
}
/**
* Returns the list of available music content providers. See DATA_** for the list of keys
* available
* @return A list of providers available. Each ProviderConnection is mutable, which means
* you can bind and unbind the instances as you wish.
*/
public List<ProviderConnection> getAvailableProviders() {
// That list may be modified, so we return a copy
synchronized (mConnections) {
return new ArrayList<>(mConnections);
}
}
public List<DSPConnection> getAvailableDSPs() {
synchronized (mDSPConnections) {
return new ArrayList<>(mDSPConnections);
}
}
/**
* Read all the services providers from the package manager for the PICK_PROVIDER action
*/
private List<HashMap<String, String>> fetchProviders() {
Intent baseIntent = new Intent(Constants.ACTION_PICK_PROVIDER);
// baseIntent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
return fetchServicesForIntent(baseIntent, false);
}
/**
* Read all the services providers from the package manager for the PICK_DSP_PROVIDER action
*/
private List<HashMap<String, String>> fetchDSPs() {
Intent baseIntent = new Intent(Constants.ACTION_PICK_DSP_PROVIDER);
return fetchServicesForIntent(baseIntent, true);
}
private synchronized List<HashMap<String, String>> fetchServicesForIntent(Intent intent, boolean isDSP) {
PackageManager pm = mContext.getPackageManager();
assert(pm != null);
List<HashMap<String, String>> services = new ArrayList<>();
// We query the PackageManager for all services implementing this intent
List<ResolveInfo> list = pm.queryIntentServices(intent,
PackageManager.GET_META_DATA | PackageManager.GET_RESOLVED_FILTER);
for (ResolveInfo info : list) {
ServiceInfo sinfo = info.serviceInfo;
if (sinfo != null) {
HashMap<String, String> item = new HashMap<>();
item.put(DATA_PACKAGE, sinfo.packageName);
item.put(DATA_SERVICE, sinfo.name);
if (sinfo.metaData != null) {
item.put(DATA_NAME, sinfo.metaData.getString(Constants.METADATA_PROVIDER_NAME));
item.put(DATA_CONFIGCLASS, sinfo.metaData.getString(Constants.METADATA_CONFIG_CLASS));
item.put(DATA_AUTHOR, sinfo.metaData.getString(Constants.METADATA_PROVIDER_AUTHOR));
}
String providerName = item.get(DATA_NAME);
if (DEBUG) Log.d(TAG, "Found providers plugin: " + sinfo.packageName + ", "
+ sinfo.name + ", name:" + providerName);
if (providerName != null) {
boolean found = false;
synchronized (mDSPConnections) {
for (DSPConnection conn : mDSPConnections) {
if (conn.getPackage().equals(sinfo.packageName)
&& conn.getServiceName().equals(sinfo.name)) {
found = true;
break;
}
}
}
if (!found) {
synchronized (mConnections) {
for (ProviderConnection conn : mConnections) {
if (conn.getPackage().equals(sinfo.packageName)
&& conn.getServiceName().equals(sinfo.name)) {
found = true;
conn.bindService();
break;
}
}
}
}
if (!found) {
if (isDSP) {
DSPConnection conn = new DSPConnection(mContext, providerName,
item.get(DATA_AUTHOR),
item.get(DATA_PACKAGE), item.get(DATA_SERVICE),
item.get(DATA_CONFIGCLASS));
conn.setListener(mProviderListener);
synchronized (mDSPConnections) {
mDSPConnections.add(conn);
}
} else {
ProviderConnection conn = new ProviderConnection(mContext, providerName,
item.get(DATA_AUTHOR),
item.get(DATA_PACKAGE), item.get(DATA_SERVICE),
item.get(DATA_CONFIGCLASS));
conn.setListener(mProviderListener);
synchronized (mConnections) {
mConnections.add(conn);
}
}
}
}
services.add(item);
}
}
return services;
}
public RecyclingBitmapDrawable getCachedLogo(final Resources res, final BoundEntity entity) {
return getCachedLogo(res, entity.getProvider(), entity.getLogo());
}
public RecyclingBitmapDrawable getCachedLogo(final Resources res, ProviderIdentifier id, String ref) {
RecyclingBitmapDrawable output = ImageCache.getDefault().get(res, ref, 500);
if (output == null && id != null) {
try {
IMusicProvider binder = getProvider(id).getBinder();
if (binder != null) {
Bitmap bmp = getProvider(id).getBinder().getLogo(ref);
if (bmp != null) {
output = ImageCache.getDefault().put(res, ref, bmp, true);
}
}
} catch (RemoteException e) {
Log.e(TAG, "Unable to get source logo", e);
}
}
return output;
}
}