/*
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 2015 (C) Scott Jackson
*/
package github.daneren2005.dsub.util.compat;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.AudioAttributes;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.RemoteControlClient;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v7.media.MediaRouter;
import android.util.Log;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import github.daneren2005.dsub.R;
import github.daneren2005.dsub.activity.SubsonicActivity;
import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.MusicDirectory.Entry;
import github.daneren2005.dsub.domain.Playlist;
import github.daneren2005.dsub.domain.SearchCritera;
import github.daneren2005.dsub.domain.SearchResult;
import github.daneren2005.dsub.service.DownloadFile;
import github.daneren2005.dsub.service.DownloadService;
import github.daneren2005.dsub.service.MusicService;
import github.daneren2005.dsub.util.Constants;
import github.daneren2005.dsub.util.ImageLoader;
import github.daneren2005.dsub.util.SilentServiceTask;
import github.daneren2005.dsub.util.Util;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RemoteControlClientLP extends RemoteControlClientBase {
private static final String TAG = RemoteControlClientLP.class.getSimpleName();
private static final String CUSTOM_ACTION_THUMBS_UP = "github.daneren2005.dsub.THUMBS_UP";
private static final String CUSTOM_ACTION_THUMBS_DOWN = "github.daneren2005.dsub.THUMBS_DOWN";
private static final String CUSTOM_ACTION_STAR = "github.daneren2005.dsub.STAR";
// Copied from MediaControlConstants so I did not have to include the entire Wear SDK just for these constant
private static final String SHOW_ON_WEAR = "android.support.wearable.media.extra.CUSTOM_ACTION_SHOW_ON_WEAR";
private static final String WEAR_RESERVE_SKIP_TO_NEXT = "android.support.wearable.media.extra.RESERVE_SLOT_SKIP_TO_NEXT";
private static final String WEAR_RESERVE_SKIP_TO_PREVIOUS = "android.support.wearable.media.extra.RESERVE_SLOT_SKIP_TO_PREVIOUS";
private static final String WEAR_BACKGROUND_THEME = "android.support.wearable.media.extra.BACKGROUND_COLOR_FROM_THEME";
// These constants don't seem to exist anywhere in the SDK. Grabbed from Google's sample media player app
private static final String AUTO_RESERVE_SKIP_TO_NEXT = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
private static final String AUTO_RESERVE_SKIP_TO_PREVIOUS = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
protected MediaSession mediaSession;
protected DownloadService downloadService;
protected ImageLoader imageLoader;
protected List<DownloadFile> currentQueue;
protected int previousState;
@Override
public void register(Context context, ComponentName mediaButtonReceiverComponent) {
downloadService = (DownloadService) context;
mediaSession = new MediaSession(downloadService, "DSub MediaSession");
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mediaButtonReceiverComponent);
PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0);
mediaSession.setMediaButtonReceiver(mediaPendingIntent);
Intent activityIntent = new Intent(context, SubsonicFragmentActivity.class);
activityIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0);
mediaSession.setSessionActivity(activityPendingIntent);
mediaSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
mediaSession.setCallback(new EventCallback());
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
mediaSession.setPlaybackToLocal(audioAttributesBuilder.build());
mediaSession.setActive(true);
Bundle sessionExtras = new Bundle();
sessionExtras.putBoolean(WEAR_BACKGROUND_THEME, true);
sessionExtras.putBoolean(WEAR_RESERVE_SKIP_TO_PREVIOUS, true);
sessionExtras.putBoolean(WEAR_RESERVE_SKIP_TO_NEXT, true);
sessionExtras.putBoolean(AUTO_RESERVE_SKIP_TO_PREVIOUS, true);
sessionExtras.putBoolean(AUTO_RESERVE_SKIP_TO_NEXT, true);
mediaSession.setExtras(sessionExtras);
imageLoader = SubsonicActivity.getStaticImageLoader(context);
}
@Override
public void unregister(Context context) {
mediaSession.release();
}
private void setPlaybackState(int state) {
setPlaybackState(state, downloadService.getCurrentPlayingIndex(), downloadService.size());
}
@Override
public void setPlaybackState(int state, int index, int queueSize) {
PlaybackState.Builder builder = new PlaybackState.Builder();
int newState = PlaybackState.STATE_NONE;
switch(state) {
case RemoteControlClient.PLAYSTATE_PLAYING:
newState = PlaybackState.STATE_PLAYING;
break;
case RemoteControlClient.PLAYSTATE_STOPPED:
newState = PlaybackState.STATE_STOPPED;
break;
case RemoteControlClient.PLAYSTATE_PAUSED:
newState = PlaybackState.STATE_PAUSED;
break;
case RemoteControlClient.PLAYSTATE_BUFFERING:
newState = PlaybackState.STATE_BUFFERING;
break;
}
long position = -1;
if(state == RemoteControlClient.PLAYSTATE_PLAYING || state == RemoteControlClient.PLAYSTATE_PAUSED) {
position = downloadService.getPlayerPosition();
}
builder.setState(newState, position, 1.0f);
DownloadFile downloadFile = downloadService.getCurrentPlaying();
Entry entry = null;
boolean isSong = true;
if(downloadFile != null) {
entry = downloadFile.getSong();
isSong = entry.isSong();
}
builder.setActions(getPlaybackActions(isSong, index, queueSize));
if(entry != null) {
addCustomActions(entry, builder);
builder.setActiveQueueItemId(entry.getId().hashCode());
}
PlaybackState playbackState = builder.build();
mediaSession.setPlaybackState(playbackState);
previousState = state;
}
@Override
public void updateMetadata(Context context, Entry currentSong) {
setMetadata(currentSong, null);
if(currentSong != null && imageLoader != null) {
imageLoader.loadImage(context, this, currentSong);
}
}
@Override
public void metadataChanged(Entry currentSong) {
setPlaybackState(previousState);
}
public void setMetadata(Entry currentSong, Bitmap bitmap) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();
builder.putString(MediaMetadata.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
.putString(MediaMetadata.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum())
.putString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
.putString(MediaMetadata.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle())
.putString(MediaMetadata.METADATA_KEY_GENRE, (currentSong) == null ? null : currentSong.getGenre())
.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, (currentSong == null) ?
0 : ((currentSong.getTrack() == null) ? 0 : currentSong.getTrack()))
.putLong(MediaMetadata.METADATA_KEY_DURATION, (currentSong == null) ?
0 : ((currentSong.getDuration() == null) ? 0 : (currentSong.getDuration() * 1000)));
if(bitmap != null) {
builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
}
mediaSession.setMetadata(builder.build());
}
@Override
public void updateAlbumArt(Entry currentSong, Bitmap bitmap) {
setMetadata(currentSong, bitmap);
}
@Override
public void registerRoute(MediaRouter router) {
router.setMediaSession(mediaSession);
}
@Override
public void unregisterRoute(MediaRouter router) {
router.setMediaSession(null);
}
@Override
public void updatePlaylist(List<DownloadFile> playlist) {
List<MediaSession.QueueItem> queue = new ArrayList<>();
for(DownloadFile file: playlist) {
Entry entry = file.getSong();
MediaDescription description = new MediaDescription.Builder()
.setMediaId(entry.getId())
.setTitle(entry.getTitle())
.setSubtitle(entry.getAlbumDisplay())
.build();
MediaSession.QueueItem item = new MediaSession.QueueItem(description, entry.getId().hashCode());
queue.add(item);
}
mediaSession.setQueue(queue);
currentQueue = playlist;
}
public MediaSession getMediaSession() {
return mediaSession;
}
protected long getPlaybackActions(boolean isSong, int currentIndex, int size) {
long actions = PlaybackState.ACTION_PLAY |
PlaybackState.ACTION_PAUSE |
PlaybackState.ACTION_SEEK_TO |
PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM;
if(isSong) {
if (currentIndex > 0) {
actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
}
if (currentIndex < size - 1) {
actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
}
} else {
actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
}
return actions;
}
protected void addCustomActions(Entry currentSong, PlaybackState.Builder builder) {
Bundle showOnWearExtras = new Bundle();
showOnWearExtras.putBoolean(SHOW_ON_WEAR, true);
int rating = currentSong.getRating();
PlaybackState.CustomAction thumbsUp = new PlaybackState.CustomAction.Builder(CUSTOM_ACTION_THUMBS_UP,
downloadService.getString(R.string.download_thumbs_up),
rating == 5 ? R.drawable.ic_action_rating_good_selected : R.drawable.ic_action_rating_good)
.setExtras(showOnWearExtras).build();
PlaybackState.CustomAction thumbsDown = new PlaybackState.CustomAction.Builder(CUSTOM_ACTION_THUMBS_DOWN,
downloadService.getString(R.string.download_thumbs_down),
rating == 1 ? R.drawable.ic_action_rating_bad_selected : R.drawable.ic_action_rating_bad)
.setExtras(showOnWearExtras).build();
PlaybackState.CustomAction star = new PlaybackState.CustomAction.Builder(CUSTOM_ACTION_STAR,
downloadService.getString(R.string.common_star),
currentSong.isStarred() ? R.drawable.ic_toggle_star : R.drawable.ic_toggle_star_outline)
.setExtras(showOnWearExtras).build();
builder.addCustomAction(thumbsDown).addCustomAction(star).addCustomAction(thumbsUp);
}
private void searchPlaylist(final String name) {
new SilentServiceTask<Void>(downloadService) {
@Override
protected Void doInBackground(MusicService musicService) throws Throwable {
List<Playlist> playlists = musicService.getPlaylists(false, downloadService, null);
for(Playlist playlist: playlists) {
if(playlist.getName().equals(name)) {
getPlaylist(playlist);
return null;
}
}
noResults();
return null;
}
private void getPlaylist(Playlist playlist) throws Exception {
MusicDirectory musicDirectory = musicService.getPlaylist(false, playlist.getId(), playlist.getName(), downloadService, null);
playSongs(musicDirectory.getChildren());
}
}.execute();
}
private void searchCriteria(final SearchCritera searchCritera) {
new SilentServiceTask<Void>(downloadService) {
@Override
protected Void doInBackground(MusicService musicService) throws Throwable {
SearchResult results = musicService.search(searchCritera, downloadService, null);
if(results.hasArtists()) {
playFromParent(new Entry(results.getArtists().get(0)));
} else if(results.hasAlbums()) {
playFromParent(results.getAlbums().get(0));
} else if(results.hasSongs()) {
playSong(results.getSongs().get(0));
} else {
noResults();
}
return null;
}
private void playFromParent(Entry parent) throws Exception {
List<Entry> songs = new ArrayList<>();
getSongsRecursively(parent, songs);
playSongs(songs);
}
private void getSongsRecursively(Entry parent, List<Entry> songs) throws Exception {
MusicDirectory musicDirectory;
if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
musicDirectory = musicService.getAlbum(parent.getId(), parent.getTitle(), false, downloadService, this);
} else {
musicDirectory = musicService.getMusicDirectory(parent.getId(), parent.getTitle(), false, downloadService, this);
}
for (Entry dir : musicDirectory.getChildren(true, false)) {
if (dir.getRating() == 1) {
continue;
}
getSongsRecursively(dir, songs);
}
for (Entry song : musicDirectory.getChildren(false, true)) {
if (!song.isVideo() && song.getRating() != 1) {
songs.add(song);
}
}
}
}.execute();
}
private void playPlaylist(final Playlist playlist, final boolean shuffle, final boolean append) {
new SilentServiceTask<Void>(downloadService) {
@Override
protected Void doInBackground(MusicService musicService) throws Throwable {
MusicDirectory musicDirectory = musicService.getPlaylist(false, playlist.getId(), playlist.getName(), downloadService, null);
playSongs(musicDirectory.getChildren(), shuffle, append);
return null;
}
}.execute();
}
private void playMusicDirectory(Entry dir, boolean shuffle, boolean append, boolean playFromBookmark) {
playMusicDirectory(dir.getId(), shuffle, append, playFromBookmark);
}
private void playMusicDirectory(final String dirId, final boolean shuffle, final boolean append, final boolean playFromBookmark) {
new SilentServiceTask<Void>(downloadService) {
@Override
protected Void doInBackground(MusicService musicService) throws Throwable {
MusicDirectory musicDirectory;
if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
musicDirectory = musicService.getAlbum(dirId, "dir", false, downloadService, null);
} else {
musicDirectory = musicService.getMusicDirectory(dirId, "dir", false, downloadService, null);
}
List<Entry> playEntries = new ArrayList<>();
List<Entry> allEntries = musicDirectory.getChildren(false, true);
for(Entry song: allEntries) {
if (!song.isVideo() && song.getRating() != 1) {
playEntries.add(song);
}
}
playSongs(playEntries, shuffle, append, playFromBookmark);
return null;
}
}.execute();
}
private void playSong(Entry entry) {
}
private void playSong(Entry entry, boolean resumeFromBookmark) {
List<Entry> entries = new ArrayList<>();
entries.add(entry);
playSongs(entries, false, false, resumeFromBookmark);
}
private void playSongs(List<Entry> entries) {
playSongs(entries, false, false);
}
private void playSongs(List<Entry> entries, boolean shuffle, boolean append) {
playSongs(entries, shuffle, append, false);
}
private void playSongs(List<Entry> entries, boolean shuffle, boolean append, boolean resumeFromBookmark) {
if(!append) {
downloadService.clear();
}
int startIndex = 0;
int startPosition = 0;
if(resumeFromBookmark) {
int bookmarkIndex = 0;
for(Entry entry: entries) {
if(entry.getBookmark() != null) {
Bookmark bookmark = entry.getBookmark();
startIndex = bookmarkIndex;
startPosition = bookmark.getPosition();
break;
}
bookmarkIndex++;
}
}
downloadService.download(entries, false, !append, false, shuffle, startIndex, startPosition);
}
private void noResults() {
// Keep getting emails from Google that not playing something with no results is bad
downloadService.clear();
downloadService.setShufflePlayEnabled(true);
}
private class EventCallback extends MediaSession.Callback {
@Override
public void onPlay() {
downloadService.start();
}
@Override
public void onStop() {
downloadService.pause();
}
@Override
public void onPause() {
downloadService.pause();
}
@Override
public void onSeekTo(long position) {
downloadService.seekTo((int) position);
}
@Override
public void onSkipToNext() {
downloadService.next();
}
@Override
public void onSkipToPrevious() {
downloadService.previous();
}
@Override
public void onSkipToQueueItem(long queueId) {
if(currentQueue != null) {
for(DownloadFile file: currentQueue) {
if(file.getSong().getId().hashCode() == queueId) {
downloadService.play(file);
return;
}
}
}
}
@Override
public void onPlayFromSearch (String query, Bundle extras) {
// User just asked to playing something
if("".equals(query)) {
downloadService.clear();
downloadService.setShufflePlayEnabled(true);
} else {
String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS);
// Play a specific playlist
if (MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE.equals(mediaFocus)) {
String playlist = extras.getString(MediaStore.EXTRA_MEDIA_PLAYLIST);
searchPlaylist(playlist);
}
// Play a specific genre
else if (MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE.equals(mediaFocus)) {
String genre = extras.getString(MediaStore.EXTRA_MEDIA_GENRE);
SharedPreferences.Editor editor = Util.getPreferences(downloadService).edit();
editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, null);
editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, null);
editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
editor.commit();
downloadService.clear();
downloadService.setShufflePlayEnabled(true);
}
else {
int artists = 10;
int albums = 10;
int songs = 10;
// Play a specific artist
if (MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE.equals(mediaFocus)) {
query = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST);
albums = 0;
songs = 0;
}
// Play a specific album
else if (MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE.equals(mediaFocus)) {
query = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM);
artists = 0;
songs = 0 ;
}
// Play a specific song
else if (MediaStore.Audio.Media.ENTRY_CONTENT_TYPE.equals(mediaFocus)) {
query = extras.getString(MediaStore.EXTRA_MEDIA_TITLE);
artists = 0;
albums = 0;
}
SearchCritera criteria = new SearchCritera(query, artists, albums, songs);
searchCriteria(criteria);
}
}
}
@Override
public void onPlayFromMediaId (String mediaId, Bundle extras) {
if(extras == null) {
return;
}
boolean shuffle = extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false);
boolean playLast = extras.getBoolean(Constants.INTENT_EXTRA_PLAY_LAST, false);
Entry entry = (Entry) extras.getSerializable(Constants.INTENT_EXTRA_ENTRY);
String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null);
if(playlistId != null) {
Playlist playlist = new Playlist(playlistId, null);
playPlaylist(playlist, shuffle, playLast);
}
String musicDirectoryId = extras.getString(Constants.INTENT_EXTRA_NAME_ID);
if(musicDirectoryId != null) {
Entry dir = new Entry(musicDirectoryId);
playMusicDirectory(dir, shuffle, playLast, true);
}
String podcastId = extras.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, null);
if(podcastId != null) {
playSong(entry, true);
}
// Currently only happens when playing bookmarks so we should be looking up parent
String childId = extras.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID, null);
if(childId != null) {
if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) {
playMusicDirectory(entry.getAlbumId(), shuffle, playLast, true);
} else {
playMusicDirectory(entry.getParent(), shuffle, playLast, true);
}
}
}
@Override
public void onCustomAction(String action, Bundle extras) {
if(CUSTOM_ACTION_THUMBS_UP.equals(action)) {
downloadService.toggleRating(5);
} else if(CUSTOM_ACTION_THUMBS_DOWN.equals(action)) {
downloadService.toggleRating(1);
} else if(CUSTOM_ACTION_STAR.equals(action)) {
downloadService.toggleStarred();
}
}
@Override
public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
if (getMediaSession() != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
KeyEvent keyEvent = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent != null) {
downloadService.handleKeyEvent(keyEvent);
return true;
}
}
return super.onMediaButtonEvent(mediaButtonIntent);
}
}
}