diff options
Diffstat (limited to 'src/github/daneren2005/dsub')
11 files changed, 694 insertions, 25 deletions
diff --git a/src/github/daneren2005/dsub/fragments/DownloadFragment.java b/src/github/daneren2005/dsub/fragments/DownloadFragment.java index d7f58e88..d87d8432 100644 --- a/src/github/daneren2005/dsub/fragments/DownloadFragment.java +++ b/src/github/daneren2005/dsub/fragments/DownloadFragment.java @@ -13,6 +13,7 @@ import android.content.SharedPreferences; import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
+import android.support.v7.app.MediaRouteButton;
import android.view.ContextMenu;
import android.view.Display;
import android.view.GestureDetector;
@@ -41,7 +42,6 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.PlayerState;
-import github.daneren2005.dsub.domain.RemoteControlState;
import github.daneren2005.dsub.domain.RepeatMode;
import github.daneren2005.dsub.service.DownloadFile;
import github.daneren2005.dsub.service.DownloadService;
@@ -88,7 +88,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe private ImageButton repeatButton;
private Button equalizerButton;
private Button visualizerButton;
- private Button jukeboxButton;
private View toggleListButton;
private ImageButton starButton;
private ImageButton bookmarkButton;
@@ -170,7 +169,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat);
equalizerButton = (Button)rootView.findViewById(R.id.download_equalizer);
visualizerButton = (Button)rootView.findViewById(R.id.download_visualizer);
- jukeboxButton = (Button)rootView.findViewById(R.id.download_jukebox);
bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark);
LinearLayout visualizerViewLayout = (LinearLayout)rootView.findViewById(R.id.download_visualizer_view_layout);
toggleListButton =rootView.findViewById(R.id.download_toggle_list);
@@ -203,7 +201,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe startButton.setOnTouchListener(touchListener);
equalizerButton.setOnTouchListener(touchListener);
visualizerButton.setOnTouchListener(touchListener);
- jukeboxButton.setOnTouchListener(touchListener);
bookmarkButton.setOnTouchListener(touchListener);
emptyTextView.setOnTouchListener(touchListener);
albumArtImageView.setOnTouchListener(touchListener);
@@ -375,17 +372,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe }
});
- jukeboxButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- boolean jukeboxEnabled = !getDownloadService().isRemoteEnabled();
- getDownloadService().setRemoteEnabled(jukeboxEnabled ? RemoteControlState.JUKEBOX_SERVER : RemoteControlState.LOCAL);
- updateButtons();
- Util.toast(context, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
- setControlsVisible(true);
- }
- });
-
bookmarkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -516,6 +502,11 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe if(downloadService != null && downloadService.getKeepScreenOn() && nowPlaying) {
menu.findItem(R.id.menu_screen_on_off).setTitle(R.string.download_menu_screen_off);
}
+ if(downloadService != null) {
+ MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute);
+ MediaRouteButton mediaRouteButton = (MediaRouteButton) mediaRouteItem.getActionView();
+ mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector());
+ }
}
@Override
@@ -758,6 +749,9 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) {
getImageLoader().loadImage(albumArtImageView, null, true, false);
}
+ if(downloadService != null) {
+ downloadService.startRemoteScan();
+ }
}
@Override
@@ -767,6 +761,9 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe if (visualizerView != null && visualizerView.isActive()) {
visualizerView.setActive(false);
}
+ if(getDownloadService() != null) {
+ getDownloadService().stopRemoteScan();
+ }
}
@Override
@@ -832,9 +829,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe if (visualizerView != null) {
visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
}
-
- boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isRemoteEnabled();
- jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
if(Util.isOffline(context)) {
bookmarkButton.setVisibility(View.GONE);
@@ -1195,7 +1189,6 @@ public class DownloadFragment extends SubsonicFragment implements OnGestureListe break;
}
- jukeboxButton.setTextColor(isJukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
onProgressChangedTask = null;
}
};
diff --git a/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java b/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java new file mode 100644 index 00000000..eaefbf55 --- /dev/null +++ b/src/github/daneren2005/dsub/provider/JukeboxRouteProvider.java @@ -0,0 +1,106 @@ +/* + 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; + +/** + * Created by Scott on 11/28/13. + */ +public class JukeboxRouteProvider extends MediaRouteProvider { + public static final String CATEGORY_SAMPLE_ROUTE = "github.daneren2005.dsub.SERVER_JUKEBOX"; + private static int MAX_VOLUME = 10; + + private DownloadService downloadService; + + public JukeboxRouteProvider(Context context) { + super(context); + this.downloadService = (DownloadService) context; + + // Create intents + IntentFilter routeIntentFilter = new IntentFilter(); + routeIntentFilter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + 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(5) + .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 static 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(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { + return true; + } else { + return false; + } + } + + @Override + public void onRelease() { + downloadService.setRemoteEnabled(RemoteControlState.LOCAL); + } + + @Override + public void onSelect() { + downloadService.setRemoteEnabled(RemoteControlState.JUKEBOX_SERVER); + } + + @Override + public void onUnselect() { + downloadService.setRemoteEnabled(RemoteControlState.LOCAL); + } + } +} diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java index b8b440d7..0826e967 100644 --- a/src/github/daneren2005/dsub/service/CachedMusicService.java +++ b/src/github/daneren2005/dsub/service/CachedMusicService.java @@ -283,7 +283,12 @@ public class CachedMusicService implements MusicService { return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener); } - @Override + @Override + public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception { + return musicService.getCoverArtUrl(context, entry); + } + + @Override public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception { return musicService.getCoverArt(context, entry, size, progressListener); } @@ -293,7 +298,12 @@ public class CachedMusicService implements MusicService { return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task); } - @Override + @Override + public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception { + return musicService.getMusicUrl(context, song, maxBitrate); + } + + @Override public Version getLocalVersion(Context context) throws Exception { return musicService.getLocalVersion(context); } diff --git a/src/github/daneren2005/dsub/service/ChromeCastController.java b/src/github/daneren2005/dsub/service/ChromeCastController.java new file mode 100644 index 00000000..49f36997 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ChromeCastController.java @@ -0,0 +1,345 @@ +/* + 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.service; + +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.Cast; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaStatus; +import com.google.android.gms.cast.RemoteMediaPlayer; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.images.WebImage; + +import java.io.IOException; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.CastCompat; + +/** + * Created by owner on 2/9/14. + */ +public class ChromeCastController extends RemoteController { + private static final String TAG = ChromeCastController.class.getSimpleName(); + + private CastDevice castDevice; + private GoogleApiClient apiClient; + private ConnectionCallbacks connectionCallbacks; + private ConnectionFailedListener connectionFailedListener; + private Cast.Listener castClientListener; + + private boolean applicationStarted = false; + private boolean waitingForReconnect = false; + private boolean error = false; + + private RemoteMediaPlayer mediaPlayer; + private double gain = 0.5; + + public ChromeCastController(DownloadServiceImpl downloadService, CastDevice castDevice) { + this.downloadService = downloadService; + this.castDevice = castDevice; + + connectionCallbacks = new ConnectionCallbacks(); + connectionFailedListener = new ConnectionFailedListener(); + castClientListener = new Cast.Listener() { + @Override + public void onApplicationStatusChanged() { + if (apiClient != null) { + Log.d(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient)); + } + } + + @Override + public void onVolumeChanged() { + if (apiClient != null) { + gain = Cast.CastApi.getVolume(apiClient); + } + } + + @Override + public void onApplicationDisconnected(int errorCode) { + shutdown(); + } + + }; + + Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener); + apiClient = new GoogleApiClient.Builder(downloadService) + .addApi(Cast.API, apiOptionsBuilder.build()) + .addConnectionCallbacks(connectionCallbacks) + .addOnConnectionFailedListener(connectionFailedListener) + .build(); + + apiClient.connect(); + } + + @Override + public void start() { + if(error) { + error = false; + Log.w(TAG, "Attempting to restart song"); + startSong(downloadService.getCurrentPlaying(), true); + return; + } + + try { + mediaPlayer.play(apiClient); + } catch(Exception e) { + Log.e(TAG, "Failed to start"); + } + } + + @Override + public void stop() { + try { + mediaPlayer.pause(apiClient); + } catch(Exception e) { + Log.e(TAG, "Failed to pause"); + } + } + + @Override + public void shutdown() { + try { + if(mediaPlayer != null && !error) { + mediaPlayer.stop(apiClient); + } + } catch(Exception e) { + Log.e(TAG, "Failed to stop mediaPlayer", e); + } + + try { + if(apiClient != null) { + Cast.CastApi.stopApplication(apiClient); + Cast.CastApi.removeMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace()); + mediaPlayer = null; + applicationStarted = false; + } + } catch(Exception e) { + Log.e(TAG, "Failed to shutdown application", e); + } + + if(apiClient != null && apiClient.isConnected()) { + apiClient.disconnect(); + } + apiClient = null; + } + + @Override + public void updatePlaylist() { + + } + + @Override + public void changePosition(int seconds) { + try { + mediaPlayer.seek(apiClient, seconds * 1000L); + } catch(Exception e) { + Log.e(TAG, "FAiled to seek to " + seconds); + } + } + + @Override + public void changeTrack(int index, DownloadFile song) { + startSong(song, true); + } + + @Override + public void setVolume(boolean up) { + double delta = up ? 0.1 : -0.1; + gain += delta; + gain = Math.max(gain, 0.0); + gain = Math.min(gain, 1.0); + + getVolumeToast().setVolume((float) gain); + try { + Cast.CastApi.setVolume(apiClient, gain); + } catch(Exception e) { + Log.e(TAG, "Failed to the volume"); + } + } + + @Override + public int getRemotePosition() { + if(mediaPlayer != null) { + return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L); + } else { + return 0; + } + } + + void startSong(DownloadFile currentPlaying, boolean autoStart) { + if(currentPlaying == null) { + // Don't start anything + return; + } + downloadService.setPlayerState(PlayerState.PREPARING); + MusicDirectory.Entry song = currentPlaying.getSong(); + + try { + MusicService musicService = MusicServiceFactory.getMusicService(downloadService); + String url = song.isVideo() ? musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService) : musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate()); + // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC + url = url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID); + + // Setup song/video information + MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK); + meta.putString(MediaMetadata.KEY_TITLE, song.getTitle()); + if(song.getTrack() != null) { + meta.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrack()); + } + if(!song.isVideo()) { + meta.putString(MediaMetadata.KEY_ARTIST, song.getArtist()); + meta.putString(MediaMetadata.KEY_ALBUM_ARTIST, song.getArtist()); + meta.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbum()); + String coverArt = musicService.getCoverArtUrl(downloadService, song); + meta.addImage(new WebImage(Uri.parse(coverArt))); + } + + // Load it into a MediaInfo wrapper + MediaInfo mediaInfo = new MediaInfo.Builder(url) + .setContentType(song.isVideo() ? "application/x-mpegURL" : song.getTranscodedContentType()) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(meta) + .build(); + + mediaPlayer.load(apiClient, mediaInfo, autoStart).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult result) { + if (result.getStatus().isSuccess()) { + if(mediaPlayer.getMediaStatus().getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING) { + downloadService.setPlayerState(PlayerState.STARTED); + } else { + downloadService.setPlayerState(PlayerState.PREPARED); + } + } else if(result.getStatus().getStatusCode() != ConnectionResult.SIGN_IN_REQUIRED) { + Log.e(TAG, "Failed to load: " + result.getStatus().toString()); + downloadService.setPlayerState(PlayerState.STOPPED); + error = true; + Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); + } + } + }); + } catch (IllegalStateException e) { + Log.e(TAG, "Problem occurred with media during loading", e); + } catch (Exception e) { + Log.e(TAG, "Problem opening media during loading", e); + } + } + + + private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { + @Override + public void onConnected(Bundle connectionHint) { + if (waitingForReconnect) { + waitingForReconnect = false; + // reconnectChannels(); + } else { + launchApplication(); + } + } + + @Override + public void onConnectionSuspended(int cause) { + waitingForReconnect = true; + } + + void launchApplication() { + try { + Cast.CastApi.launchApplication(apiClient, CastCompat.APPLICATION_ID, false).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() { + @Override + public void onResult(Cast.ApplicationConnectionResult result) { + Status status = result.getStatus(); + if (status.isSuccess()) { + ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); + String sessionId = result.getSessionId(); + String applicationStatus = result.getApplicationStatus(); + boolean wasLaunched = result.getWasLaunched(); + + applicationStarted = true; + setupChannel(); + } else { + shutdown(); + } + } + }); + } catch (Exception e) { + Log.e(TAG, "Failed to launch application", e); + } + } + void setupChannel() { + mediaPlayer = new RemoteMediaPlayer(); + mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { + @Override + public void onStatusUpdated() { + MediaStatus mediaStatus = mediaPlayer.getMediaStatus(); + Log.d(TAG, "state: " + mediaStatus.getPlayerState()); + switch(mediaStatus.getPlayerState()) { + case MediaStatus.PLAYER_STATE_PLAYING: + downloadService.setPlayerState(PlayerState.STARTED); + break; + case MediaStatus.PLAYER_STATE_PAUSED: + downloadService.setPlayerState(PlayerState.PAUSED); + break; + case MediaStatus.PLAYER_STATE_BUFFERING: + downloadService.setPlayerState(PlayerState.PREPARING); + break; + case MediaStatus.PLAYER_STATE_IDLE: + downloadService.setPlayerState(PlayerState.COMPLETED); + downloadService.next(); + break; + } + } + }); + mediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() { + @Override + public void onMetadataUpdated() { + MediaInfo mediaInfo = mediaPlayer.getMediaInfo(); + // TODO: Do I care about this? + } + }); + + try { + Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); + } catch (IOException e) { + Log.e(TAG, "Exception while creating channel", e); + } + + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + startSong(currentPlaying, true); + } + } + + private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { + @Override + public void onConnectionFailed(ConnectionResult result) { + shutdown(); + } + } +} diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java index 1a254c73..471b6d5d 100644 --- a/src/github/daneren2005/dsub/service/DownloadService.java +++ b/src/github/daneren2005/dsub/service/DownloadService.java @@ -18,6 +18,8 @@ */ package github.daneren2005.dsub.service; +import android.support.v7.media.MediaRouteSelector; + import java.util.List; import github.daneren2005.dsub.audiofx.EqualizerController; @@ -27,6 +29,7 @@ import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; import github.daneren2005.dsub.domain.RepeatMode; +import github.daneren2005.dsub.util.MediaRouteManager; /** * @author Sindre Mehus @@ -124,11 +127,17 @@ public interface DownloadService { VisualizerController getVisualizerController(); + MediaRouteSelector getRemoteSelector(); + boolean isRemoteEnabled(); void setRemoteEnabled(RemoteControlState newState); void setRemoteVolume(boolean up); + + void startRemoteScan(); + + void stopRemoteScan(); void setSleepTimerDuration(int duration); diff --git a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java index 9f1e5ee9..e83be019 100644 --- a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java +++ b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -37,6 +37,7 @@ import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; import github.daneren2005.dsub.util.CancellableTask; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.MediaRouteManager; import github.daneren2005.dsub.util.ShufflePlayBuffer; import github.daneren2005.dsub.util.SimpleServiceBinder; import github.daneren2005.dsub.util.Util; @@ -63,6 +64,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; +import android.support.v7.media.MediaRouteSelector; import android.util.Log; import android.support.v4.util.LruCache; import java.net.URLEncoder; @@ -136,6 +138,8 @@ public class DownloadServiceImpl extends Service implements DownloadService { private int timerDuration; private boolean autoPlayStart = false; + private MediaRouteManager mediaRouter; + static { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { equalizerAvailable = true; @@ -210,6 +214,8 @@ public class DownloadServiceImpl extends Service implements DownloadService { getVisualizerController(); showVisualization = true; } + + mediaRouter = new MediaRouteManager(this); } @Override @@ -366,7 +372,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { SharedPreferences prefs = Util.getPreferences(this); remoteState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)]; if(remoteState != RemoteControlState.LOCAL) { - setRemoteState(remoteState); + setRemoteState(remoteState, null); } boolean startShufflePlay = prefs.getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, false); download(songs, false, false, false, false); @@ -1118,19 +1124,27 @@ public class DownloadServiceImpl extends Service implements DownloadService { } @Override + public MediaRouteSelector getRemoteSelector() { + return mediaRouter.getSelector(); + } + + @Override public boolean isRemoteEnabled() { return remoteState != RemoteControlState.LOCAL; } @Override public void setRemoteEnabled(RemoteControlState newState) { - setRemoteState(newState); + setRemoteEnabled(newState, null); + } + public void setRemoteEnabled(RemoteControlState newState, Object ref) { + setRemoteState(newState, ref); SharedPreferences.Editor editor = Util.getPreferences(this).edit(); editor.putInt(Constants.PREFERENCES_KEY_CONTROL_MODE, newState.getValue()); editor.commit(); } - private void setRemoteState(RemoteControlState newState) { + private void setRemoteState(RemoteControlState newState, Object ref) { if(remoteController != null) { remoteController.stop(); setPlayerState(PlayerState.IDLE); @@ -1143,6 +1157,14 @@ public class DownloadServiceImpl extends Service implements DownloadService { case JUKEBOX_SERVER: remoteController = new JukeboxController(this, handler); break; + case CHROMECAST: + // TODO: Fix case where starting up with chromecast set + if(ref == null) { + remoteState = RemoteControlState.LOCAL; + break; + } + remoteController = (RemoteController) ref; + break; case LOCAL: default: break; } @@ -1169,6 +1191,16 @@ public class DownloadServiceImpl extends Service implements DownloadService { remoteController.setVolume(up); } + @Override + public void startRemoteScan() { + mediaRouter.startScan(); + } + + @Override + public void stopRemoteScan() { + mediaRouter.stopScan(); + } + private synchronized void bufferAndPlay() { bufferAndPlay(0); } diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java index 3674fd01..d5fdc251 100644 --- a/src/github/daneren2005/dsub/service/MusicService.java +++ b/src/github/daneren2005/dsub/service/MusicService.java @@ -91,10 +91,14 @@ public interface MusicService { MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; + String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception; + Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception; HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception; + String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception; + Version getLocalVersion(Context context) throws Exception; Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception; diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java index 43b5f887..37e2bf44 100644 --- a/src/github/daneren2005/dsub/service/RESTMusicService.java +++ b/src/github/daneren2005/dsub/service/RESTMusicService.java @@ -644,6 +644,14 @@ public class RESTMusicService implements MusicService { } } + @Override + public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception { + StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt", false)); + builder.append("&id=").append(entry.getId()); + String url = rewriteUrlWithRedirect(context, builder.toString()); + return url; + } + @Override public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener) throws Exception { @@ -723,7 +731,18 @@ public class RESTMusicService implements MusicService { return response; } - @Override + @Override + public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception { + StringBuilder builder = new StringBuilder(getRestUrl(context, "stream", false)); + builder.append("&id=").append(song.getId()); + builder.append("&maxBitRate=").append(maxBitrate); + + String url = rewriteUrlWithRedirect(context, builder.toString()); + Log.i(TAG, "Using music URL: " + url); + return url; + } + + @Override public String getVideoUrl(int maxBitrate, Context context, String id) { StringBuilder builder = new StringBuilder(getRestUrl(context, "videoPlayer")); builder.append("&id=").append(id); diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java index 5d80a961..2b96ac4c 100644 --- a/src/github/daneren2005/dsub/util/Constants.java +++ b/src/github/daneren2005/dsub/util/Constants.java @@ -31,6 +31,7 @@ public final class Constants { // Note: Keep it as low as possible to maintain compatibility with older servers. public static final String REST_PROTOCOL_VERSION = "1.2.0"; public static final String REST_CLIENT_ID = "DSub"; + public static final String CHROMECAST_CLIENT_ID = "DSubCC"; public static final String LAST_VERSION = "subsonic.version"; // Names for intent extras. diff --git a/src/github/daneren2005/dsub/util/MediaRouteManager.java b/src/github/daneren2005/dsub/util/MediaRouteManager.java new file mode 100644 index 00000000..7c0d33e6 --- /dev/null +++ b/src/github/daneren2005/dsub/util/MediaRouteManager.java @@ -0,0 +1,93 @@ +/* + 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.util; + +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +import github.daneren2005.dsub.domain.RemoteControlState; +import github.daneren2005.dsub.provider.JukeboxRouteProvider; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.service.RemoteController; +import github.daneren2005.dsub.util.compat.CastCompat; + +/** + * Created by owner on 2/8/14. + */ +public class MediaRouteManager extends MediaRouter.Callback { + private static final String TAG = MediaRouteManager.class.getSimpleName(); + private static boolean castAvailable = false; + + private DownloadServiceImpl downloadService; + private MediaRouter router; + private MediaRouteSelector selector; + + static { + try { + CastCompat.checkAvailable(); + castAvailable = true; + } catch(Throwable t) { + castAvailable = false; + } + } + + public MediaRouteManager(DownloadServiceImpl downloadService) { + this.downloadService = downloadService; + router = MediaRouter.getInstance(downloadService); + addProviders(); + buildSelector(); + } + + @Override + public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) { + if(castAvailable) { + RemoteController controller = CastCompat.getController(downloadService, info); + if(controller != null) { + downloadService.setRemoteEnabled(RemoteControlState.CHROMECAST, controller); + } + } + } + @Override + public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) { + downloadService.setRemoteEnabled(RemoteControlState.LOCAL); + } + + public void startScan() { + router.addCallback(selector, this, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + } + public void stopScan() { + router.removeCallback(this); + } + + public MediaRouteSelector getSelector() { + return selector; + } + + private void addProviders() { + JukeboxRouteProvider routeProvider = new JukeboxRouteProvider(downloadService); + router.addProvider(routeProvider); + } + private void buildSelector() { + MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder(); + builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + if(castAvailable) { + builder.addControlCategory(CastCompat.getCastControlCategory()); + } + selector = builder.build(); + } +} diff --git a/src/github/daneren2005/dsub/util/compat/CastCompat.java b/src/github/daneren2005/dsub/util/compat/CastCompat.java new file mode 100644 index 00000000..31581816 --- /dev/null +++ b/src/github/daneren2005/dsub/util/compat/CastCompat.java @@ -0,0 +1,57 @@ +/* + 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.util.compat; + +import android.support.v7.media.MediaRouter; + +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.CastMediaControlIntent; + +import github.daneren2005.dsub.service.ChromeCastController; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.service.RemoteController; + +/** + * Created by owner on 2/9/14. + */ +public final class CastCompat { + public static final String APPLICATION_ID = "5F85EBEB"; + + static { + try { + Class.forName("com.google.android.gms.cast.CastDevice"); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public static void checkAvailable() throws Throwable { + // Calling here forces class initialization. + } + + public static RemoteController getController(DownloadServiceImpl downloadService, MediaRouter.RouteInfo info) { + CastDevice device = CastDevice.getFromBundle(info.getExtras()); + if(device != null) { + return new ChromeCastController(downloadService, device); + } else { + return null; + } + } + + public static String getCastControlCategory() { + return CastMediaControlIntent.categoryForCast(APPLICATION_ID); + } +} |