aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/github/daneren2005/dsub/provider
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:02 -0700
committerScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:05 -0700
commitcfd014d38cba03ba05f571597b361ab253bff578 (patch)
tree4256723561dec7ef3ed3507382eb7020724ec570 /app/src/main/java/github/daneren2005/dsub/provider
parent8a332a20ec272d59fe74520825b18017a8f0cac3 (diff)
downloaddsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz
dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2
dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip
Update to gradle
Diffstat (limited to 'app/src/main/java/github/daneren2005/dsub/provider')
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java425
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java191
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java28
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java304
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java131
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java61
-rw-r--r--app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java61
12 files changed, 1407 insertions, 0 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
new file mode 100644
index 00000000..73d4b5de
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java
@@ -0,0 +1,425 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2014 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.provider;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.os.IBinder;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteDiscoveryRequest;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+import android.util.Log;
+
+import org.eclipse.jetty.util.log.Logger;
+import org.fourthline.cling.android.AndroidUpnpService;
+import org.fourthline.cling.android.AndroidUpnpServiceImpl;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.LocalDevice;
+import org.fourthline.cling.model.meta.RemoteDevice;
+import org.fourthline.cling.model.meta.StateVariable;
+import org.fourthline.cling.model.meta.StateVariableAllowedValueRange;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.registry.Registry;
+import org.fourthline.cling.registry.RegistryListener;
+import org.fourthline.cling.support.renderingcontrol.callback.GetVolume;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DLNAController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+public class DLNARouteProvider extends MediaRouteProvider {
+ private static final String TAG = DLNARouteProvider.class.getSimpleName();
+ public static final String CATEGORY_DLNA = "github.daneren2005.dsub.DLNA";
+
+ private DownloadService downloadService;
+ private RemoteController controller;
+
+ private HashMap<String, DLNADevice> devices = new HashMap<String, DLNADevice>();
+ private List<String> adding = new ArrayList<String>();
+ private List<String> removing = new ArrayList<String>();
+ private AndroidUpnpService dlnaService;
+ private ServiceConnection dlnaServiceConnection;
+ private boolean searchOnConnect = false;
+
+ public DLNARouteProvider(Context context) {
+ super(context);
+
+ // Use custom logger
+ org.eclipse.jetty.util.log.Log.setLog(new JettyAndroidLog());
+
+ this.downloadService = (DownloadService) context;
+ dlnaServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ dlnaService = (AndroidUpnpService) service;
+ dlnaService.getRegistry().addListener(new RegistryListener() {
+ @Override
+ public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
+
+ }
+
+ @Override
+ public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
+ // Error is displayed in log anyways under W/trieveRemoteDescriptors
+ }
+
+ @Override
+ public void remoteDeviceAdded(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceUpdated(Registry registry, RemoteDevice remoteDevice) {
+ deviceAdded(remoteDevice);
+ }
+
+ @Override
+ public void remoteDeviceRemoved(Registry registry, RemoteDevice remoteDevice) {
+ deviceRemoved(remoteDevice);
+ }
+
+ @Override
+ public void localDeviceAdded(Registry registry, LocalDevice localDevice) {
+ deviceAdded(localDevice);
+ }
+
+ @Override
+ public void localDeviceRemoved(Registry registry, LocalDevice localDevice) {
+ deviceRemoved(localDevice);
+ }
+
+ @Override
+ public void beforeShutdown(Registry registry) {
+
+ }
+
+ @Override
+ public void afterShutdown() {
+
+ }
+ });
+
+ for (Device<?, ?, ?> device : dlnaService.getControlPoint().getRegistry().getDevices()) {
+ deviceAdded(device);
+ }
+ if(searchOnConnect) {
+ dlnaService.getControlPoint().search();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ dlnaService = null;
+ }
+ };
+
+ if(!context.getApplicationContext().bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Failed to bind to DLNA service");
+ }
+ }
+
+ private void broadcastDescriptors() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_DLNA);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+
+ // Create route descriptor
+ for(Map.Entry<String, DLNADevice> deviceEntry: devices.entrySet()) {
+ DLNADevice device = deviceEntry.getValue();
+
+ int volume;
+ if(device.volumeMax == 0) {
+ volume = 5;
+ } else {
+ int increments = device.volumeMax / 10;
+ volume = controller == null ? device.volume : (int) controller.getVolume();
+ volume = volume / increments;
+ }
+
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder(device.id, device.name);
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription(device.description)
+ .setVolume(volume)
+ .setVolumeMax(10)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+ providerBuilder.addRoute(routeBuilder.build());
+ }
+
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (request != null && request.isActiveScan()) {
+ if(dlnaService != null) {
+ dlnaService.getControlPoint().search();
+ } else {
+ searchOnConnect = true;
+ }
+ }
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ DLNADevice device = devices.get(routeId);
+ if(device == null) {
+ Log.w(TAG, "No device exists for " + routeId);
+ return null;
+ }
+
+ return new DLNARouteController(device);
+ }
+
+ private void deviceAdded(final Device device) {
+ final org.fourthline.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl"));
+ if(renderingControl == null) {
+ return;
+ }
+
+ final String id = device.getIdentity().getUdn().toString();
+ // In the process of looking up it's details already
+ if(adding.contains(id)) {
+ return;
+ }
+ // Just a temp disconnect, already have it's info
+ if(removing.contains(id)) {
+ removing.remove(id);
+ return;
+ }
+ adding.add(id);
+
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ try {
+ dlnaService.getControlPoint().execute(new GetVolume(renderingControl) {
+ @Override
+ public void received(ActionInvocation actionInvocation, int currentVolume) {
+ int maxVolume = 100;
+ StateVariable volume = renderingControl.getStateVariable("Volume");
+ if (volume != null) {
+ StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange();
+ maxVolume = (int) volumeRange.getMaximum();
+ }
+
+ // Create a new DLNADevice to represent this item
+ String id = device.getIdentity().getUdn().toString();
+ String name = device.getDetails().getFriendlyName();
+ String displayName = device.getDisplayString();
+
+ DLNADevice newDevice = new DLNADevice(device, id, name, displayName, currentVolume, maxVolume);
+ devices.put(id, newDevice);
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ broadcastDescriptors();
+ }
+ });
+ adding.remove(id);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get default volume for DLNA route");
+ Log.w(TAG, "Reason: " + s);
+ adding.remove(id);
+ }
+ });
+ } catch(Exception e) {
+ Log.e(TAG, "Failed to add device", e);
+ }
+ } else {
+ adding.remove(id);
+ }
+ }
+ private void deviceRemoved(Device device) {
+ if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) {
+ final String id = device.getIdentity().getUdn().toString();
+ removing.add(id);
+
+ // Delay removal for a few seconds to make sure that it isn't just a temp disconnect
+ dlnaService.getControlPoint().search();
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if(removing.contains(id)) {
+ devices.remove(id);
+ removing.remove(id);
+ broadcastDescriptors();
+ }
+ }
+ }, 5000L);
+ }
+ }
+
+ private class DLNARouteController extends RouteController {
+ private DLNADevice device;
+
+ public DLNARouteController(DLNADevice device) {
+ this.device = device;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_DLNA)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ controller = new DLNAController(downloadService, dlnaService.getControlPoint(), device);
+ downloadService.setRemoteEnabled(RemoteControlState.DLNA, controller);
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptors();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptors();
+ }
+ }
+
+ public static class JettyAndroidLog implements Logger {
+ final private static java.util.logging.Logger log = java.util.logging.Logger.getLogger("Jetty");
+
+ public static boolean __isIgnoredEnabled = false;
+ public String _name;
+
+ public JettyAndroidLog() {
+ this (JettyAndroidLog.class.getName());
+ }
+
+ public JettyAndroidLog(String name) {
+ _name = name;
+ }
+
+ public String getName () {
+ return _name;
+ }
+
+ public void debug(Throwable th) {
+ // Log.d(TAG, "", th);
+ }
+
+ public void debug(String msg, Throwable th) {
+ // Log.d(TAG, msg, th);
+ }
+
+ public void debug(String msg, Object... args) {
+ // Log.d(TAG, msg);
+ }
+
+ public Logger getLogger(String name) {
+ return new JettyAndroidLog(name);
+ }
+
+ public void info(String msg, Object... args) {
+ // Log.i(TAG, msg);
+ }
+
+ public void info(Throwable th) {
+ // Log.i(TAG, "", th);
+ }
+
+ public void info(String msg, Throwable th) {
+ // Log.i(TAG, msg, th);
+ }
+
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ public void warn(Throwable th) {
+ // Log.w(TAG, "", th);
+ }
+
+ public void warn(String msg, Object... args) {
+ // Log.w(TAG, msg);
+ }
+
+ public void warn(String msg, Throwable th) {
+ // Log.w(TAG, msg, th);
+ }
+
+ public boolean isIgnoredEnabled () {
+ return __isIgnoredEnabled;
+ }
+
+
+ public void ignore(Throwable ignored) {
+ if (__isIgnoredEnabled) {
+ warn("IGNORED", ignored);
+ }
+ }
+
+ public void setIgnoredEnabled(boolean enabled) {
+ __isIgnoredEnabled = enabled;
+ }
+
+ public void setDebugEnabled(boolean enabled) {
+
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
new file mode 100644
index 00000000..63bbaaa4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java
@@ -0,0 +1,191 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Provides search suggestions based on recent searches.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubSearchProvider extends ContentProvider {
+ private static final String TAG = DSubSearchProvider.class.getSimpleName();
+
+ private static final String RESOURCE_PREFIX = "android.resource://github.daneren2005.dsub/";
+ private static final String[] COLUMNS = {"_id",
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
+ SearchManager.SUGGEST_COLUMN_ICON_1};
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String query = selectionArgs[0] + "*";
+ SearchResult searchResult = search(query);
+ return createCursor(selectionArgs[0], searchResult);
+ }
+
+ private SearchResult search(String query) {
+ MusicService musicService = MusicServiceFactory.getMusicService(getContext());
+ if (musicService == null) {
+ return null;
+ }
+
+ try {
+ return musicService.search(new SearchCritera(query, 5, 10, 10), getContext(), null);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private Cursor createCursor(String query, SearchResult searchResult) {
+ MatrixCursor cursor = new MatrixCursor(COLUMNS);
+ if (searchResult == null) {
+ return cursor;
+ }
+
+ // Add all results into one pot
+ List<Object> results = new ArrayList<Object>();
+ results.addAll(searchResult.getArtists());
+ results.addAll(searchResult.getAlbums());
+ results.addAll(searchResult.getSongs());
+
+ // For each, calculate its string distance to the query
+ for(Object obj: results) {
+ if(obj instanceof Artist) {
+ Artist artist = (Artist) obj;
+ artist.setCloseness(Util.getStringDistance(query, artist.getName()));
+ } else {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) obj;
+ entry.setCloseness(Util.getStringDistance(query, entry.getTitle()));
+ }
+ }
+
+ // Sort based on the closeness paramater
+ Collections.sort(results, new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ // Get the closeness of the two objects
+ int left, right;
+ boolean leftArtist = lhs instanceof Artist;
+ boolean rightArtist = rhs instanceof Artist;
+ if (leftArtist) {
+ left = ((Artist) lhs).getCloseness();
+ } else {
+ left = ((MusicDirectory.Entry) lhs).getCloseness();
+ }
+ if (rightArtist) {
+ right = ((Artist) rhs).getCloseness();
+ } else {
+ right = ((MusicDirectory.Entry) rhs).getCloseness();
+ }
+
+ if (left == right) {
+ if(leftArtist && rightArtist) {
+ return 0;
+ } else if(leftArtist) {
+ return -1;
+ } else if(rightArtist) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (left > right) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ });
+
+ // Done sorting, add results to cursor
+ for(Object obj: results) {
+ if(obj instanceof Artist) {
+ Artist artist = (Artist) obj;
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_artist;
+ cursor.addRow(new Object[]{artist.getId().hashCode(), artist.getName(), null, "ar-" + artist.getId(), artist.getName(), icon});
+ } else {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) obj;
+
+ if(entry.isDirectory()) {
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_album;
+ cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), entry.getId(), entry.getTitle(), icon});
+ } else {
+ String icon = RESOURCE_PREFIX + R.drawable.ic_action_song;
+ String id;
+ if(Util.isTagBrowsing(getContext())) {
+ id = entry.getAlbumId();
+ } else {
+ id = entry.getParent();
+ }
+ cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), "so-" + id, entry.getTitle(), icon});
+ }
+ }
+ }
+ return cursor;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return false;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String s, String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
+ return 0;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java
new file mode 100644
index 00000000..c78257c4
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x1.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x1 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x1;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java
new file mode 100644
index 00000000..4c7d637c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x2.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x2 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x2;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java
new file mode 100644
index 00000000..b4f91a45
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x3.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x3 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x3;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java
new file mode 100644
index 00000000..a591ff29
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidget4x4.java
@@ -0,0 +1,28 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x4 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x4;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
new file mode 100644
index 00000000..444b6cff
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java
@@ -0,0 +1,304 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Environment;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerQueue;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceLifecycleSupport;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * Simple widget to show currently playing album art along
+ * with play/pause and next track buttons.
+ * <p/>
+ * Based on source code from the stock Android Music app.
+ *
+ * @author Sindre Mehus
+ */
+public class DSubWidgetProvider extends AppWidgetProvider {
+ private static final String TAG = DSubWidgetProvider.class.getSimpleName();
+ private static DSubWidget4x1 instance4x1;
+ private static DSubWidget4x2 instance4x2;
+ private static DSubWidget4x3 instance4x3;
+ private static DSubWidget4x4 instance4x4;
+
+ public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) {
+ if(instance4x1 == null) {
+ instance4x1 = new DSubWidget4x1();
+ }
+ if(instance4x2 == null) {
+ instance4x2 = new DSubWidget4x2();
+ }
+ if(instance4x3 == null) {
+ instance4x3 = new DSubWidget4x3();
+ }
+ if(instance4x4 == null) {
+ instance4x4 = new DSubWidget4x4();
+ }
+
+ instance4x1.notifyChange(context, service, playing);
+ instance4x2.notifyChange(context, service, playing);
+ instance4x3.notifyChange(context, service, playing);
+ instance4x4.notifyChange(context, service, playing);
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+ notifyInstances(context, DownloadService.getInstance(), false);
+ }
+
+ protected int getLayout() {
+ return 0;
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Subsonic on default click
+ * and hide actions if service not running.
+ */
+ private void defaultAppWidget(Context context, int[] appWidgetIds) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
+ if(getLayout() == R.layout.appwidget4x2) {
+ views.setTextViewText(R.id.album, "");
+ }
+
+ linkButtons(context, views, false);
+ performUpdate(context, null, appWidgetIds, false);
+ }
+
+ private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) {
+ // Update specific list of appWidgetIds if given, otherwise default to all
+ final AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ manager.updateAppWidget(appWidgetIds, views);
+ } else {
+ manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from {@link DownloadService}
+ */
+ public void notifyChange(Context context, DownloadService service, boolean playing) {
+ if (hasInstances(context)) {
+ performUpdate(context, service, null, playing);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this widget.
+ */
+ private boolean hasInstances(Context context) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
+ return (appWidgetIds.length > 0);
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout());
+
+ if(playing) {
+ views.setViewVisibility(R.id.widget_root, View.VISIBLE);
+ } else {
+ // Hide widget
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_HIDE_WIDGET, false)) {
+ views.setViewVisibility(R.id.widget_root, View.GONE);
+ }
+ }
+
+ // Get Entry from current playing DownloadFile
+ MusicDirectory.Entry currentPlaying = null;
+ if(service == null) {
+ // Deserialize from playling list to setup
+ PlayerQueue state = FileUtil.deserialize(context, DownloadServiceLifecycleSupport.FILENAME_DOWNLOADS_SER, PlayerQueue.class);
+ if(state != null && state.currentPlayingIndex != -1) {
+ currentPlaying = state.songs.get(state.currentPlayingIndex);
+ }
+ } else {
+ currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong();
+ }
+
+ String title = currentPlaying == null ? null : currentPlaying.getTitle();
+ CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist();
+ CharSequence album = currentPlaying == null ? null : currentPlaying.getAlbum();
+ CharSequence errorState = null;
+
+ // Show error message?
+ String status = Environment.getExternalStorageState();
+ if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ errorState = res.getText(R.string.widget_sdcard_busy);
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ errorState = res.getText(R.string.widget_sdcard_missing);
+ } else if (currentPlaying == null) {
+ errorState = res.getText(R.string.widget_initial_text);
+ }
+
+ if (errorState != null) {
+ // Show error state to user
+ views.setTextViewText(R.id.title,null);
+ views.setTextViewText(R.id.artist, errorState);
+ views.setTextViewText(R.id.album, "");
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default);
+ }
+ } else {
+ // No error, so show normal titles
+ views.setTextViewText(R.id.title, title);
+ views.setTextViewText(R.id.artist, artist);
+ if(getLayout() != R.layout.appwidget4x1) {
+ views.setTextViewText(R.id.album, album);
+ }
+ }
+
+ // Set correct drawable for pause state
+ if (playing) {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause);
+ } else {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play);
+ }
+
+ // Set the cover art
+ try {
+ boolean large = false;
+ if(getLayout() != R.layout.appwidget4x1 && getLayout() != R.layout.appwidget4x2) {
+ large = true;
+ }
+ ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ Bitmap bitmap = imageLoader == null ? null : imageLoader.getCachedImage(context, currentPlaying, large);
+
+ if (bitmap == null) {
+ // Set default cover art
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ } else {
+ bitmap = getRoundedCornerBitmap(bitmap);
+ views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load cover art", x);
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(context, views, currentPlaying != null);
+
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ /**
+ * Round the corners of a bitmap for the cover art image
+ */
+ private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final float roundPx = 10;
+
+ // Add extra width to the rect so the right side wont be rounded.
+ final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntent}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link DownloadActivity},
+ * otherwise we launch {@link github.daneren2005.dsub.activity.SubsonicFragmentActivity}.
+ */
+ private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
+ Intent intent = new Intent(context, SubsonicFragmentActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
+ views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
+
+ // Emulate media button clicks.
+ intent = new Intent("DSub.PLAY_PAUSE");
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_TOGGLEPAUSE);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
+
+ intent = new Intent("DSub.NEXT"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_NEXT);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+
+ intent = new Intent("DSub.PREVIOUS"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadService.class));
+ intent.setAction(DownloadService.CMD_PREVIOUS);
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
new file mode 100644
index 00000000..0d2a5ff5
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/JukeboxRouteProvider.java
@@ -0,0 +1,131 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2014 (C) Scott Jackson
+*/
+package github.daneren2005.dsub.provider;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaRouter;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteProviderDescriptor;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by Scott on 11/28/13.
+ */
+public class JukeboxRouteProvider extends MediaRouteProvider {
+ public static final String CATEGORY_JUKEBOX_ROUTE = "github.daneren2005.dsub.SERVER_JUKEBOX";
+ private RemoteController controller;
+ private static final int MAX_VOLUME = 10;
+
+ private DownloadService downloadService;
+
+ public JukeboxRouteProvider(Context context) {
+ super(context);
+ this.downloadService = (DownloadService) context;
+
+ broadcastDescriptor();
+ }
+
+ private void broadcastDescriptor() {
+ // Create intents
+ IntentFilter routeIntentFilter = new IntentFilter();
+ routeIntentFilter.addCategory(CATEGORY_JUKEBOX_ROUTE);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_START_SESSION);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ routeIntentFilter.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+ // Create route descriptor
+ MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder("Jukebox Route", "Subsonic Jukebox");
+ routeBuilder.addControlFilter(routeIntentFilter)
+ .setPlaybackStream(AudioManager.STREAM_MUSIC)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+ .setDescription("Subsonic Jukebox")
+ .setVolume(controller == null ? 5 : (int) (controller.getVolume() * 10))
+ .setVolumeMax(MAX_VOLUME)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+
+ // Create descriptor
+ MediaRouteProviderDescriptor.Builder providerBuilder = new MediaRouteProviderDescriptor.Builder();
+ providerBuilder.addRoute(routeBuilder.build());
+ setDescriptor(providerBuilder.build());
+ }
+
+ @Override
+ public MediaRouteProvider.RouteController onCreateRouteController(String routeId) {
+ return new JukeboxRouteController(downloadService);
+ }
+
+ private class JukeboxRouteController extends RouteController {
+ private DownloadService downloadService;
+
+ public JukeboxRouteController(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, android.support.v7.media.MediaRouter.ControlRequestCallback callback) {
+ if (intent.hasCategory(CATEGORY_JUKEBOX_ROUTE)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onSelect() {
+ downloadService.setRemoteEnabled(RemoteControlState.JUKEBOX_SERVER);
+ controller = downloadService.getRemoteController();
+ }
+
+ @Override
+ public void onUnselect() {
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ controller = null;
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if(controller != null) {
+ controller.updateVolume(delta > 0);
+ }
+ broadcastDescriptor();
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if(controller != null) {
+ controller.setVolume(volume);
+ }
+ broadcastDescriptor();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java
new file mode 100644
index 00000000..80dd93cc
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/MostRecentStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class MostRecentStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java
new file mode 100644
index 00000000..1481557f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/PlaylistStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PlaylistStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java
new file mode 100644
index 00000000..7d9bfca1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/PodcastStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class PodcastStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java
new file mode 100644
index 00000000..f638c348
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/provider/StarredStubProvider.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic 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.
+
+ Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+
+package github.daneren2005.dsub.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * Created by Scott on 8/28/13.
+ */
+
+public class StarredStubProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}