diff options
60 files changed, 1329 insertions, 445 deletions
@@ -29,5 +29,5 @@ Media Icons are double standard size. On https://romannurik.github.io/AndroidAs ``` PARAM_RESOURCES.iconSize = {w: 64, h: 64} PARAM_RESOURCES['targetRect-clipart'] = { x: 12, y: 12, w: 40, h: 40 } -form.fields[0].params.maxFinalSize = {w: 512, h: 512} +form.fields_[0].params_.maxFinalSize = {w: 512, h: 512} ``` diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eeb0cc75..edd354d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -90,6 +90,15 @@ <service android:name=".service.DownloadService" android:label="DSub Playback Service"/> + + <service android:name=".service.AutoMediaBrowserService" + android:exported="true"> + + <intent-filter> + <action android:name="android.media.browse.MediaBrowserService"/> + </intent-filter> + </service> + <service android:name="org.fourthline.cling.android.AndroidUpnpServiceImpl"/> <service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService"> <intent-filter> @@ -241,6 +250,11 @@ <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> - </application> + + <meta-data android:name="com.google.android.gms.car.application" + android:resource="@xml/auto_app_description"/> + <meta-data android:name="com.google.android.gms.car.notification.SmallIcon" + android:resource="@drawable/stat_notify_playing" /> + </application> </manifest> diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 0dbb643f..72b63e69 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -850,8 +850,9 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) { - if(this.currentPlaying != currentPlaying || currentPlaying == null) { + if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { onSongChanged(currentPlaying, currentPlayingIndex); + onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); } } @@ -867,4 +868,9 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo startButton.setImageResource(typedArray.getResourceId(0, 0)); typedArray.recycle(); } + + @Override + public void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange) { + + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java index 3427f2ca..4d761b02 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchResult.java @@ -49,4 +49,14 @@ public class SearchResult implements Serializable { public List<MusicDirectory.Entry> getSongs() { return songs; } + + public boolean hasArtists() { + return !artists.isEmpty(); + } + public boolean hasAlbums() { + return !albums.isEmpty(); + } + public boolean hasSongs() { + return !songs.isEmpty(); + } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java index 281491af..5a3b8cf5 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -59,6 +59,7 @@ import github.daneren2005.dsub.activity.SubsonicFragmentActivity; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.audiofx.EqualizerController; import github.daneren2005.dsub.domain.Bookmark; +import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.domain.ServerInfo; @@ -192,24 +193,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis starButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - DownloadFile currentDownload = getDownloadService().getCurrentPlaying(); - if (currentDownload != null) { - final Entry currentSong = currentDownload.getSong(); - toggleStarred(currentSong, new OnStarChange() { - @Override - void starChange(boolean starred) { - if(currentSong.isStarred()) { - starButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_star)); - } else { - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); - } else { - starButton.setImageResource(R.drawable.ic_toggle_star_outline_dark); - } - } - } - }); - } + getDownloadService().toggleStarred(); } }); } else { @@ -356,37 +340,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if(downloadService == null) { return; } - - DownloadFile downloadFile = downloadService.getCurrentPlaying(); - if(downloadFile == null) { - return; - } - Entry entry = downloadFile.getSong(); - - // If rating == 1, already set so unset - if(entry.getRating() == 1) { - setRating(entry, 0); - - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - rateBadButton.setImageResource(R.drawable.ic_action_rating_bad_dark); - } else { - rateBadButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.rating_bad)); - } - } else { - // Immediately skip to the next song - downloadService.next(true); - - // Otherwise set rating to 1 - setRating(entry, 1); - rateBadButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_bad_selected)); - - // Make sure good rating is blank - if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - rateGoodButton.setImageResource(R.drawable.ic_action_rating_good_dark); - } else { - rateGoodButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.rating_good)); - } - } + downloadService.toggleRating(1); } }); rateGoodButton.setOnClickListener(new View.OnClickListener() { @@ -396,34 +350,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if(downloadService == null) { return; } - - DownloadFile downloadFile = downloadService.getCurrentPlaying(); - if(downloadFile == null) { - return; - } - Entry entry = downloadFile.getSong(); - - // If rating == 5, already set so unset - if(entry.getRating() == 5) { - setRating(entry, 0); - - if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - rateGoodButton.setImageResource(R.drawable.ic_action_rating_good_dark); - } else { - rateGoodButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.rating_good)); - } - } else { - // Otherwise set rating to maximum - setRating(entry, 5); - rateGoodButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_good_selected)); - - // Make sure bad rating is blank - if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - rateBadButton.setImageResource(R.drawable.ic_action_rating_bad_dark); - } else { - rateBadButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.rating_bad)); - } - } + downloadService.toggleRating(5); } }); @@ -497,7 +424,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if(downloadService != null && downloadService.getSleepTimer()) { int timeRemaining = downloadService.getSleepTimeRemaining(); timerMenu = menu.findItem(R.id.menu_toggle_timer); - timerMenu.setTitle(context.getResources().getString(R.string.download_stop_time_remaining, Util.formatDuration(timeRemaining))); + if(timeRemaining > 1){ + timerMenu.setTitle(context.getResources().getString(R.string.download_stop_time_remaining, Util.formatDuration(timeRemaining))); + } else { + timerMenu.setTitle(R.string.menu_set_timer); + } } if(downloadService != null && downloadService.getKeepScreenOn()) { menu.findItem(R.id.menu_screen_on_off).setChecked(true); @@ -690,10 +621,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis createNewPlaylist(entries, true); return true; case R.id.menu_star: - toggleStarred(song.getSong()); + UpdateHelper.toggleStarred(context, song.getSong()); return true; case R.id.menu_rate: - setRating(song.getSong()); + UpdateHelper.setRating(context, song.getSong()); return true; case R.id.menu_toggle_timer: if(getDownloadService().getSleepTimer()) { @@ -1089,7 +1020,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.createBookmark(currentSong, position, comment, context, null); - new EntryInstanceUpdater(currentSong) { + new UpdateHelper.EntryInstanceUpdater(currentSong) { @Override public void update(Entry found) { found.setBookmark(new Bookmark(position)); @@ -1231,54 +1162,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis Entry song = currentPlaying.getSong(); songTitleTextView.setText(song.getTitle()); getImageLoader().loadImage(albumArtImageView, song, true, true); - if(song.isStarred()) { - starButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_star)); - } else { - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); - } else { - starButton.setImageResource(R.drawable.ic_toggle_star_outline_dark); - } - } setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); - - int badRating, goodRating, bookmark; - if(song.getRating() == 1) { - rateBadButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_bad_selected)); - } else { - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - badRating = R.drawable.ic_action_rating_bad_dark; - } else { - badRating = DrawableTint.getDrawableRes(context, R.attr.rating_bad); - } - rateBadButton.setImageResource(badRating); - } - - if(song.getRating() == 5) { - rateGoodButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_good_selected)); - } else { - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - goodRating = R.drawable.ic_action_rating_good_dark; - } else { - goodRating = DrawableTint.getDrawableRes(context, R.attr.rating_good); - } - rateGoodButton.setImageResource(goodRating); - } - - if(song.getBookmark() != null) { - bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected)); - } else { - if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - bookmark = R.drawable.ic_menu_bookmark_dark; - } else { - bookmark = DrawableTint.getDrawableRes(context, R.attr.bookmark); - } - bookmarkButton.setImageResource(bookmark); - } } else { songTitleTextView.setText(null); getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); - starButton.setImageResource(R.drawable.ic_toggle_star_outline_dark); setSubtitle(null); } } @@ -1315,6 +1202,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); if(this.currentPlaying != currentPlaying) { onSongChanged(currentPlaying, currentPlayingIndex); + onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); } } @@ -1344,7 +1232,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis DownloadService downloadService = getDownloadService(); if(downloadService.getSleepTimer() && timerMenu != null) { int timeRemaining = downloadService.getSleepTimeRemaining(); - timerMenu.setTitle(context.getResources().getString(R.string.download_stop_time_remaining, Util.formatDuration(timeRemaining))); + if(timeRemaining > 1){ + timerMenu.setTitle(context.getResources().getString(R.string.download_stop_time_remaining, Util.formatDuration(timeRemaining))); + } else { + timerMenu.setTitle(R.string.menu_set_timer); + } } } @@ -1397,6 +1289,53 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } + @Override + public void onMetadataUpdate(Entry song, int fieldChange) { + if(song != null && song.isStarred()) { + starButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_star)); + } else { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); + } else { + starButton.setImageResource(R.drawable.ic_toggle_star_outline_dark); + } + } + + int badRating, goodRating, bookmark; + if(song != null && song.getRating() == 1) { + rateBadButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_bad_selected)); + } else { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + badRating = R.drawable.ic_action_rating_bad_dark; + } else { + badRating = DrawableTint.getDrawableRes(context, R.attr.rating_bad); + } + rateBadButton.setImageResource(badRating); + } + + if(song != null && song.getRating() == 5) { + rateGoodButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_action_rating_good_selected)); + } else { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + goodRating = R.drawable.ic_action_rating_good_dark; + } else { + goodRating = DrawableTint.getDrawableRes(context, R.attr.rating_good); + } + rateGoodButton.setImageResource(goodRating); + } + + if(song != null && song.getBookmark() != null) { + bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected)); + } else { + if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + bookmark = R.drawable.ic_menu_bookmark_dark; + } else { + bookmark = DrawableTint.getDrawableRes(context, R.attr.bookmark); + } + bookmarkButton.setImageResource(bookmark); + } + } + public void updateRepeatButton() { DownloadService downloadService = getDownloadService(); switch (downloadService.getRepeatMode()) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java index a1129a45..c7d8e191 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -57,6 +57,7 @@ import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.TabBackgroundTask; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.UserUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.FastScroller; @@ -1037,7 +1038,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section musicService.setStarred(entries, artists, albums, false, this, context); for(Entry entry: unstar) { - new EntryInstanceUpdater(entry) { + new UpdateHelper.EntryInstanceUpdater(entry) { @Override public void update(Entry found) { found.setStarred(false); @@ -1382,10 +1383,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section starButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - toggleStarred(directory, new OnStarChange() { + UpdateHelper.toggleStarred(context, directory, new UpdateHelper.OnStarChange() { @Override - void starChange(boolean starred) { - if(directory.isStarred()) { + public void starChange(boolean starred) { + if (directory.isStarred()) { starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); starButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_star)); } else { @@ -1406,9 +1407,9 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section ratingBarWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - setRating(directory, new OnRatingChange() { + UpdateHelper.setRating(context, directory, new UpdateHelper.OnRatingChange() { @Override - void ratingChange(int rating) { + public void ratingChange(int rating) { ratingBar.setRating(directory.getRating()); } }); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java index 8672f534..7baf9009 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -77,6 +77,7 @@ import github.daneren2005.dsub.util.MenuUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.LoadingTask; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.UserUtil; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.AlbumView; @@ -327,7 +328,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR deleteRecursively(artist); break; case R.id.artist_menu_star: - toggleStarred(artist); + UpdateHelper.toggleStarred(context, artist); break; case R.id.album_menu_play_now: artistOverride = true; @@ -354,7 +355,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR downloadRecursively(entry.getId(), true, true, false, false, true); break; case R.id.album_menu_star: - toggleStarred(entry); + UpdateHelper.toggleStarred(context, entry); break; case R.id.album_menu_delete: deleteRecursively(entry); @@ -387,7 +388,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR addToPlaylist(songs); break; case R.id.song_menu_star: - toggleStarred(entry); + UpdateHelper.toggleStarred(context, entry); break; case R.id.song_menu_play_external: playExternalPlayer(entry); @@ -411,7 +412,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR deleteBookmark(entry, null); break; case R.id.menu_rate: - setRating(entry); + UpdateHelper.setRating(context, entry); break; default: return false; @@ -790,106 +791,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR dialog.show(); } - public void toggleStarred(Entry entry) { - toggleStarred(entry, null); - } - - public void toggleStarred(final Entry entry, final OnStarChange onStarChange) { - final boolean starred = !entry.isStarred(); - entry.setStarred(starred); - if(onStarChange != null) { - onStarChange.starChange(starred); - } - - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) { - if(entry.isAlbum()) { - musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context); - } else { - musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context); - } - } else { - musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context); - } - - new EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(starred); - } - }.execute(); - - return null; - } - - @Override - protected void done(Void result) { - // UpdateView - Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle())); - } - - @Override - protected void error(Throwable error) { - Log.w(TAG, "Failed to star", error); - entry.setStarred(!starred); - if(onStarChange != null) { - onStarChange.starChange(!starred); - } - - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error); - } - - Util.toast(context, msg, false); - } - }.execute(); - } - - public void toggleStarred(final Artist entry) { - final boolean starred = !entry.isStarred(); - entry.setStarred(starred); - - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { - musicService.setStarred(null, Arrays.asList(new Entry(entry)), null, starred, null, context); - } else { - musicService.setStarred(Arrays.asList(new Entry(entry)), null, null, starred, null, context); - } - return null; - } - - @Override - protected void done(Void result) { - // UpdateView - Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getName())); - } - - @Override - protected void error(Throwable error) { - Log.w(TAG, "Failed to star", error); - entry.setStarred(!starred); - - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(R.string.starring_content_error, entry.getName()) + " " + getErrorMessage(error); - } - - Util.toast(context, msg, false); - } - }.execute(); - } - protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { downloadRecursively(id, "", true, save, append, autoplay, shuffle, background); } @@ -1737,7 +1638,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.deleteBookmark(entry, context, null); - new EntryInstanceUpdater(entry) { + new UpdateHelper.EntryInstanceUpdater(entry) { @Override public void update(Entry found) { found.setBookmark(null); @@ -1773,80 +1674,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR }); } - protected void setRating(Entry entry) { - setRating(entry, null); - } - protected void setRating(final Entry entry, final OnRatingChange onRatingChange) { - View layout = context.getLayoutInflater().inflate(R.layout.rating, null); - final RatingBar ratingBar = (RatingBar) layout.findViewById(R.id.rating_bar); - ratingBar.setRating((float) entry.getRating()); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(context.getResources().getString(R.string.rating_title, entry.getTitle())) - .setView(layout) - .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - int rating = (int) ratingBar.getRating(); - setRating(entry, rating, onRatingChange); - } - }) - .setNegativeButton(R.string.common_cancel, null); - - AlertDialog dialog = builder.create(); - dialog.show(); - } - - protected void setRating(Entry entry, int rating) { - setRating(entry, rating, null); - } - protected void setRating(final Entry entry, final int rating, final OnRatingChange onRatingChange) { - final int oldRating = entry.getRating(); - entry.setRating(rating); - - if(onRatingChange != null) { - onRatingChange.ratingChange(rating); - } - - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - musicService.setRating(entry, rating, context, null); - - new EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setRating(rating); - } - }.execute(); - return null; - } - - @Override - protected void done(Void result) { - Util.toast(context, context.getResources().getString(rating > 0 ? R.string.rating_set_rating : R.string.rating_remove_rating, entry.getTitle())); - } - - @Override - protected void error(Throwable error) { - entry.setRating(oldRating); - if(onRatingChange != null) { - onRatingChange.ratingChange(oldRating); - } - - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error); - } - - Util.toast(context, msg, false); - } - }.execute(); - } - public SectionAdapter getCurrentAdapter() { return null; } public void stopActionMode() { SectionAdapter adapter = getCurrentAdapter(); @@ -1965,47 +1792,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } - protected abstract class EntryInstanceUpdater { - private Entry entry; - - public EntryInstanceUpdater(Entry entry) { - this.entry = entry; - } - - public abstract void update(Entry found); - - public void execute() { - DownloadService downloadService = getDownloadService(); - if(downloadService != null && !entry.isDirectory()) { - boolean serializeChanges = false; - List<DownloadFile> downloadFiles = downloadService.getDownloads(); - for(DownloadFile file: downloadFiles) { - Entry check = file.getSong(); - if(entry.getId().equals(check.getId())) { - update(entry); - serializeChanges = true; - } - } - - if(serializeChanges) { - downloadService.serializeQueue(); - } - } - - Entry find = UpdateView.findEntry(entry); - if(find != null) { - update(find); - } - } - } - - public abstract class OnRatingChange { - abstract void ratingChange(int rating); - } - public abstract class OnStarChange { - abstract void starChange(boolean starred); - } - public abstract class RecursiveLoader extends LoadingTask<Boolean> { protected MusicService musicService; protected static final int MAX_SONGS = 500; diff --git a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java new file mode 100644 index 00000000..d579ef54 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java @@ -0,0 +1,231 @@ +/* + 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.service; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.media.MediaDescription; +import android.media.browse.MediaBrowser; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.service.media.MediaBrowserService; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.SilentServiceTask; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.RemoteControlClientLP; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class AutoMediaBrowserService extends MediaBrowserService { + private static final String TAG = AutoMediaBrowserService.class.getSimpleName(); + private static final String BROWSER_ROOT = "root"; + private static final String BROWSER_ALBUM_LISTS = "albumLists"; + private static final String BROWSER_LIBRARY = "library"; + private static final String BROWSER_PLAYLISTS = "playlists"; + private static final String PLAYLIST_PREFIX = "pl-"; + private static final String ALBUM_TYPE_PREFIX = "ty-"; + + private DownloadService downloadService; + private Handler handler = new Handler(); + + @Override + public void onCreate() { + super.onCreate(); + getDownloadService(); + } + + @Nullable + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + BrowserRoot root = new BrowserRoot(BROWSER_ROOT, null); + return root; + } + + @Override + public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) { + if(BROWSER_ROOT.equals(parentId)) { + getRootFolders(result); + } else if(BROWSER_ALBUM_LISTS.equals(parentId)) { + getAlbumLists(result); + } else if(parentId.startsWith(ALBUM_TYPE_PREFIX)) { + int id = Integer.valueOf(parentId.substring(ALBUM_TYPE_PREFIX.length())); + getAlbumList(result, id); + } else if(BROWSER_LIBRARY.equals(parentId)) { + getLibrary(result); + } else if(BROWSER_PLAYLISTS.equals(parentId)) { + getPlaylists(result); + } else if(parentId.startsWith(PLAYLIST_PREFIX)) { + getPlayOptions(result, parentId.substring(PLAYLIST_PREFIX.length()), Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + } else { + // No idea what it is, send empty result + result.sendResult(new ArrayList<MediaBrowser.MediaItem>()); + } + } + + private void getRootFolders(Result<List<MediaBrowser.MediaItem>> result) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + /*MediaDescription.Builder albumLists = new MediaDescription.Builder(); + albumLists.setTitle(downloadService.getString(R.string.main_albums_title)) + .setMediaId(BROWSER_ALBUM_LISTS); + mediaItems.add(new MediaBrowser.MediaItem(albumLists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + + MediaDescription.Builder library = new MediaDescription.Builder(); + library.setTitle(downloadService.getString(R.string.button_bar_browse)) + .setMediaId(BROWSER_LIBRARY); + mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));*/ + + MediaDescription.Builder playlists = new MediaDescription.Builder(); + playlists.setTitle(downloadService.getString(R.string.button_bar_playlists)) + .setMediaId(BROWSER_PLAYLISTS); + mediaItems.add(new MediaBrowser.MediaItem(playlists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + + result.sendResult(mediaItems); + } + + private void getAlbumLists(Result<List<MediaBrowser.MediaItem>> result) { + List<Integer> albums = new ArrayList<>(); + albums.add(R.string.main_albums_newest); + albums.add(R.string.main_albums_random); + if(ServerInfo.checkServerVersion(downloadService, "1.8")) { + albums.add(R.string.main_albums_alphabetical); + } + if(!Util.isTagBrowsing(downloadService)) { + albums.add(R.string.main_albums_highest); + } + // albums.add(R.string.main_albums_starred); + // albums.add(R.string.main_albums_genres); + // albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_recent); + albums.add(R.string.main_albums_frequent); + + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Integer id: albums) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(downloadService.getResources().getString(id)) + .setMediaId(ALBUM_TYPE_PREFIX + id) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + private void getAlbumList(Result<List<MediaBrowser.MediaItem>> result, int id) { + + } + + private void getLibrary(Result<List<MediaBrowser.MediaItem>> result) { + + } + + private void getPlaylists(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<List<Playlist>>(downloadService) { + @Override + protected List<Playlist> doInBackground(MusicService musicService) throws Throwable { + return musicService.getPlaylists(false, downloadService, null); + } + + @Override + protected void done(List<Playlist> playlists) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Playlist playlist: playlists) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(playlist.getName()) + .setMediaId(PLAYLIST_PREFIX + playlist.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + Bundle playAllExtras = new Bundle(); + playAllExtras.putString(idConstant, id); + + MediaDescription.Builder playAll = new MediaDescription.Builder(); + playAll.setTitle(downloadService.getString(R.string.menu_play)) + .setMediaId("play-" + id) + .setExtras(playAllExtras); + mediaItems.add(new MediaBrowser.MediaItem(playAll.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); + + Bundle shuffleExtras = new Bundle(); + shuffleExtras.putString(idConstant, id); + shuffleExtras.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true); + + MediaDescription.Builder shuffle = new MediaDescription.Builder(); + shuffle.setTitle(downloadService.getString(R.string.menu_shuffle)) + .setMediaId("shuffle-" + id) + .setExtras(shuffleExtras); + mediaItems.add(new MediaBrowser.MediaItem(shuffle.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); + + /*Bundle playLastExtras = new Bundle(); + playLastExtras.putString(idConstant, id); + playLastExtras.putBoolean(Constants.INTENT_EXTRA_PLAY_LAST, true); + + MediaDescription.Builder playLast = new MediaDescription.Builder(); + playLast.setTitle(downloadService.getString(R.string.menu_play_last)) + .setMediaId("playLast-" + id) + .setExtras(playLastExtras); + mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));*/ + + result.sendResult(mediaItems); + } + + public void getDownloadService() { + if(DownloadService.getInstance() == null) { + startService(new Intent(this, DownloadService.class)); + } + + waitForDownloadService(); + } + public void waitForDownloadService() { + downloadService = DownloadService.getInstance(); + if(downloadService == null) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + waitForDownloadService(); + } + }, 100); + } else { + RemoteControlClientLP remoteControlClient = (RemoteControlClientLP) downloadService.getRemoteControlClient(); + setSessionToken(remoteControlClient.getMediaSession().getSessionToken()); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java index 7af21831..d33fa527 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java @@ -48,8 +48,9 @@ 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.UpdateHelper; import github.daneren2005.dsub.util.Util; -import github.daneren2005.dsub.util.compat.RemoteControlClientHelper; +import github.daneren2005.dsub.util.compat.RemoteControlClientBase; import github.daneren2005.dsub.util.tags.BastpUtil; import github.daneren2005.dsub.view.UpdateView; import github.daneren2005.serverproxy.BufferProxy; @@ -102,11 +103,17 @@ public class DownloadService extends Service { public static final int REWIND = 10000; private static final double DELETE_CUTOFF = 0.84; private static final int REQUIRED_ALBUM_MATCHES = 4; + private static final int REMOTE_PLAYLIST_TOTAL = 3; private static final int SHUFFLE_MODE_NONE = 0; private static final int SHUFFLE_MODE_ALL = 1; private static final int SHUFFLE_MODE_ARTIST = 2; - private RemoteControlClientHelper mRemoteControl; + public static final int METADATA_UPDATED_ALL = 0; + public static final int METADATA_UPDATED_STAR = 1; + public static final int METADATA_UPDATED_RATING = 2; + public static final int METADATA_UPDATED_BOOKMARK = 4; + + private RemoteControlClientBase mRemoteControl; private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); private Looper mediaPlayerLooper; @@ -238,7 +245,7 @@ public class DownloadService extends Service { if (mRemoteControl == null) { // Use the remote control APIs (if available) to set the playback state - mRemoteControl = RemoteControlClientHelper.createInstance(); + mRemoteControl = RemoteControlClientBase.createInstance(); ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); mRemoteControl.register(this, mediaButtonReceiverComponent); } @@ -442,10 +449,21 @@ public class DownloadService extends Service { lifecycleSupport.serializeDownloadQueue(); } - private void updateRemotePlaylist() { + private synchronized void updateRemotePlaylist() { + List<DownloadFile> playlist = new ArrayList<>(); + if(currentPlaying != null) { + int index = downloadList.indexOf(currentPlaying); + int size = size(); + int end = index + REMOTE_PLAYLIST_TOTAL; + for(int i = index; i < size && i < end; i++) { + playlist.add(downloadList.get(i)); + } + } + if (remoteState != LOCAL && remoteController != null) { remoteController.updatePlaylist(); } + mRemoteControl.updatePlaylist(playlist); } public synchronized void restore(List<MusicDirectory.Entry> songs, List<MusicDirectory.Entry> toDelete, int currentPlayingIndex, int currentPlayingPosition) { @@ -1020,6 +1038,7 @@ public class DownloadService extends Service { proxy = null; } checkDownloads(); + updateRemotePlaylist(); } /** Plays or resumes the playback, depending on the current player state. */ @@ -1756,6 +1775,8 @@ public class DownloadService extends Service { setPlayerState(PAUSED); onSongProgress(); } + + updateRemotePlaylist(); } // Only call when starting, setPlayerState(PAUSED) already calls this @@ -2232,6 +2253,10 @@ public class DownloadService extends Service { } clearCurrentBookmark(downloadFile.getSong(), true); } + + public RemoteControlClientBase getRemoteControlClient() { + return mRemoteControl; + } private boolean isPastCutoff() { return isPastCutoff(getPlayerPosition(), getPlayerDuration()); @@ -2461,6 +2486,55 @@ public class DownloadService extends Service { } } + public void toggleStarred() { + final DownloadFile currentPlaying = this.currentPlaying; + if(currentPlaying == null) { + return; + } + + UpdateHelper.toggleStarred(this, currentPlaying.getSong(), new UpdateHelper.OnStarChange() { + @Override + public void starChange(boolean starred) { + if(currentPlaying == DownloadService.this.currentPlaying) { + onMetadataUpdate(METADATA_UPDATED_STAR); + } + } + }); + } + public void toggleRating(int rating) { + if(currentPlaying == null) { + return; + } + + MusicDirectory.Entry entry = currentPlaying.getSong(); + if(entry.getRating() == rating) { + setRating(0); + } else { + setRating(rating); + } + } + public void setRating(int rating) { + final DownloadFile currentPlaying = this.currentPlaying; + if(currentPlaying == null) { + return; + } + MusicDirectory.Entry entry = currentPlaying.getSong(); + + // Immediately skip to the next song if down thumbed + if(rating == 1) { + next(true); + } + + UpdateHelper.setRating(this, entry, rating, new UpdateHelper.OnRatingChange() { + @Override + public void ratingChange(int rating) { + if(currentPlaying == DownloadService.this.currentPlaying) { + onMetadataUpdate(METADATA_UPDATED_RATING); + } + } + }); + } + public void addOnSongChangedListener(OnSongChangedListener listener) { addOnSongChangedListener(listener, false); } @@ -2500,6 +2574,9 @@ public class DownloadService extends Service { public void run() { if(revision == atRevision) { listener.onSongChanged(currentPlaying, currentPlayingIndex); + + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); } } }); @@ -2542,6 +2619,13 @@ public class DownloadService extends Service { } }); } + + handler.post(new Runnable() { + @Override + public void run() { + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + } + }); } private void onStateUpdate() { final long atRevision = revision; @@ -2556,6 +2640,27 @@ public class DownloadService extends Service { }); } } + private synchronized void onMetadataUpdate() { + onMetadataUpdate(METADATA_UPDATED_ALL); + } + private synchronized void onMetadataUpdate(final int updateType) { + for(final OnSongChangedListener listener: onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } + }); + } + + handler.post(new Runnable() { + @Override + public void run() { + mRemoteControl.metadataChanged(currentPlaying.getSong()); + } + }); + } private class BufferTask extends SilentBackgroundTask<Void> { private final DownloadFile downloadFile; @@ -2668,5 +2773,6 @@ public class DownloadService extends Service { void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex); void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable); void onStateUpdate(DownloadFile downloadFile, PlayerState playerState); + void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java index 30081d7d..18f245d5 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java +++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java @@ -93,6 +93,9 @@ public abstract class BackgroundTask<T> implements ProgressListener { return (context instanceof Activity) ? ((Activity) context) : null; } + protected Context getContext() { + return context; + } protected Handler getHandler() { return handler; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java index 3ac5ee90..89e7de3b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java @@ -66,6 +66,7 @@ public final class Constants { public static final String INTENT_EXTRA_SEARCH_SONG = "searchSong"; public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks"; public static final String INTENT_EXTRA_SHOW_ALL = "showAll"; + public static final String INTENT_EXTRA_PLAY_LAST = "playLast"; // Preferences keys. public static final String PREFERENCES_KEY_SERVER_KEY = "server"; diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java index 8b027d70..470d72fb 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java @@ -29,7 +29,6 @@ import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; -import android.media.RemoteControlClient; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -46,6 +45,7 @@ import github.daneren2005.dsub.domain.Playlist; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.util.compat.RemoteControlClientBase; /** * Asynchronous loading of images, with caching. @@ -257,22 +257,22 @@ public class ImageLoader { return task; } - public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) { + public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClientBase remoteControl, MusicDirectory.Entry entry) { Bitmap bitmap; if (entry == null || entry.getCoverArt() == null) { bitmap = getUnknownImage(entry, imageSizeLarge); - setImage(remoteControl, Util.createDrawableFromBitmap(context, bitmap)); + setImage(entry, remoteControl, Util.createDrawableFromBitmap(context, bitmap)); return null; } bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge)); if (bitmap != null && !bitmap.isRecycled()) { Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); - setImage(remoteControl, drawable); + setImage(entry, remoteControl, drawable); return null; } - setImage(remoteControl, Util.createDrawableFromBitmap(context, null)); + setImage(entry, remoteControl, Util.createDrawableFromBitmap(context, null)); ImageTask task = new RemoteControlClientImageTask(context, entry, imageSizeLarge, imageSizeLarge, false, remoteControl); task.execute(); return task; @@ -367,28 +367,27 @@ public class ImageLoader { } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void setImage(RemoteControlClient remoteControl, Drawable drawable) { + private void setImage(MusicDirectory.Entry entry, RemoteControlClientBase remoteControl, Drawable drawable) { if(remoteControl != null && drawable != null) { Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap(); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && origBitmap != null) { origBitmap = origBitmap.copy(origBitmap.getConfig(), false); } if ( origBitmap != null && !origBitmap.isRecycled()) { - remoteControl.editMetadata(false).putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, origBitmap).apply(); + remoteControl.updateAlbumArt(entry, origBitmap); } else { if(origBitmap != null) { Log.e(TAG, "Tried to load a recycled bitmap."); } - remoteControl.editMetadata(false) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) - .apply(); + + remoteControl.updateAlbumArt(entry, null); } } } public abstract class ImageTask extends SilentBackgroundTask<Void> { private final Context mContext; - private final MusicDirectory.Entry mEntry; + protected final MusicDirectory.Entry mEntry; private final int mSize; private final int mSaveSize; private final boolean mIsNowPlaying; @@ -448,9 +447,9 @@ public class ImageLoader { } private class RemoteControlClientImageTask extends ImageTask { - private RemoteControlClient mRemoteControl; + private RemoteControlClientBase mRemoteControl; - public RemoteControlClientImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, RemoteControlClient remoteControl) { + public RemoteControlClientImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, RemoteControlClientBase remoteControl) { super(context, entry, size, saveSize, isNowPlaying); mRemoteControl = remoteControl; @@ -458,7 +457,7 @@ public class ImageLoader { @Override protected void done(Void result) { - setImage(mRemoteControl, mDrawable); + setImage(mEntry, mRemoteControl, mDrawable); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/SilentServiceTask.java b/app/src/main/java/github/daneren2005/dsub/util/SilentServiceTask.java new file mode 100644 index 00000000..9fdfe66f --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/SilentServiceTask.java @@ -0,0 +1,41 @@ +/* + 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; + +import android.content.Context; + +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; + +public abstract class SilentServiceTask<T> extends SilentBackgroundTask<T> { + protected MusicService musicService; + + public SilentServiceTask(Context context) { + super(context); + } + + @Override + protected T doInBackground() throws Throwable { + musicService = MusicServiceFactory.getMusicService(getContext()); + return doInBackground(musicService); + } + + protected abstract T doInBackground(MusicService musicService) throws Throwable; +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java new file mode 100644 index 00000000..c7e0a04b --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java @@ -0,0 +1,263 @@ +/* + 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; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; +import android.widget.RatingBar; + +import java.util.Arrays; +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.MusicDirectory.Entry; +import github.daneren2005.dsub.fragments.SubsonicFragment; +import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.service.OfflineException; +import github.daneren2005.dsub.service.ServerTooOldException; +import github.daneren2005.dsub.view.UpdateView; + +public final class UpdateHelper { + private static final String TAG = UpdateHelper.class.getSimpleName(); + + public static void toggleStarred(Context context, Entry entry) { + toggleStarred(context, entry, null); + } + + public static void toggleStarred(final Context context, final Entry entry, final OnStarChange onStarChange) { + final boolean starred = !entry.isStarred(); + entry.setStarred(starred); + if(onStarChange != null) { + onStarChange.starChange(starred); + } + + new SilentBackgroundTask<Void>(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) { + if(entry.isAlbum()) { + musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context); + } else { + musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context); + } + } else { + musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context); + } + + new EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setStarred(starred); + } + }.execute(); + + return null; + } + + @Override + protected void done(Void result) { + // UpdateView + Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle())); + } + + @Override + protected void error(Throwable error) { + Log.w(TAG, "Failed to star", error); + entry.setStarred(!starred); + if(onStarChange != null) { + onStarChange.starChange(!starred); + } + + String msg; + if (error instanceof OfflineException || error instanceof ServerTooOldException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + public static void toggleStarred(final Context context, final Artist entry) { + final boolean starred = !entry.isStarred(); + entry.setStarred(starred); + + new SilentBackgroundTask<Void>(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + musicService.setStarred(null, Arrays.asList(new Entry(entry)), null, starred, null, context); + } else { + musicService.setStarred(Arrays.asList(new Entry(entry)), null, null, starred, null, context); + } + return null; + } + + @Override + protected void done(Void result) { + // UpdateView + Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getName())); + } + + @Override + protected void error(Throwable error) { + Log.w(TAG, "Failed to star", error); + entry.setStarred(!starred); + + String msg; + if (error instanceof OfflineException || error instanceof ServerTooOldException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(R.string.starring_content_error, entry.getName()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + public static void setRating(Activity context, Entry entry) { + setRating(context, entry, null); + } + public static void setRating(final Activity context, final Entry entry, final OnRatingChange onRatingChange) { + View layout = context.getLayoutInflater().inflate(R.layout.rating, null); + final RatingBar ratingBar = (RatingBar) layout.findViewById(R.id.rating_bar); + ratingBar.setRating((float) entry.getRating()); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getResources().getString(R.string.rating_title, entry.getTitle())) + .setView(layout) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + int rating = (int) ratingBar.getRating(); + setRating(context, entry, rating, onRatingChange); + } + }) + .setNegativeButton(R.string.common_cancel, null); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + public static void setRating(Context context, Entry entry, int rating) { + setRating(context, entry, rating, null); + } + public static void setRating(final Context context, final Entry entry, final int rating, final OnRatingChange onRatingChange) { + final int oldRating = entry.getRating(); + entry.setRating(rating); + + if(onRatingChange != null) { + onRatingChange.ratingChange(rating); + } + + new SilentBackgroundTask<Void>(context) { + @Override + protected Void doInBackground() throws Throwable { + MusicService musicService = MusicServiceFactory.getMusicService(context); + musicService.setRating(entry, rating, context, null); + + new EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setRating(rating); + } + }.execute(); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, context.getResources().getString(rating > 0 ? R.string.rating_set_rating : R.string.rating_remove_rating, entry.getTitle())); + } + + @Override + protected void error(Throwable error) { + entry.setRating(oldRating); + if(onRatingChange != null) { + onRatingChange.ratingChange(oldRating); + } + + String msg; + if (error instanceof OfflineException || error instanceof ServerTooOldException) { + msg = getErrorMessage(error); + } else { + msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error); + } + + Util.toast(context, msg, false); + } + }.execute(); + } + + public static abstract class EntryInstanceUpdater { + private Entry entry; + + public EntryInstanceUpdater(Entry entry) { + this.entry = entry; + } + + public abstract void update(Entry found); + + public void execute() { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !entry.isDirectory()) { + boolean serializeChanges = false; + List<DownloadFile> downloadFiles = downloadService.getDownloads(); + for(DownloadFile file: downloadFiles) { + Entry check = file.getSong(); + if(entry.getId().equals(check.getId())) { + update(entry); + serializeChanges = true; + } + } + + if(serializeChanges) { + downloadService.serializeQueue(); + } + } + + Entry find = UpdateView.findEntry(entry); + if(find != null) { + update(find); + } + } + } + + public static abstract class OnStarChange { + public abstract void starChange(boolean starred); + } + public static abstract class OnRatingChange { + public abstract void ratingChange(int rating); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java index 320092e9..1f7035dc 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java @@ -1,43 +1,39 @@ package github.daneren2005.dsub.util.compat; -import github.daneren2005.dsub.domain.MusicDirectory.Entry; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.DownloadFile; + import android.content.ComponentName; import android.content.Context; +import android.graphics.Bitmap; import android.support.v7.media.MediaRouter; -import android.util.Log; - -public class RemoteControlClientBase extends RemoteControlClientHelper { - - private static final String TAG = RemoteControlClientBase.class.getSimpleName(); - - @Override - public void register(Context context, ComponentName mediaButtonReceiverComponent) { - - } - - @Override - public void unregister(Context context) { - - } - - @Override - public void setPlaybackState(int state) { - - } - - @Override - public void updateMetadata(Context context, Entry currentSong) { - - } - - @Override - public void registerRoute(MediaRouter router) { - +import android.os.Build; + +import java.util.List; + +public abstract class RemoteControlClientBase { + + public static RemoteControlClientBase createInstance() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return new RemoteControlClientLP(); + } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return new RemoteControlClientJB(); + } else { + return new RemoteControlClientICS(); + } } - - @Override - public void unregisterRoute(MediaRouter router) { - + + protected RemoteControlClientBase() { + // Avoid instantiation } - + + public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent); + public abstract void unregister(final Context context); + public abstract void setPlaybackState(int state); + public abstract void updateMetadata(Context context, MusicDirectory.Entry currentSong); + public abstract void metadataChanged(MusicDirectory.Entry currentSong); + public abstract void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap); + public abstract void registerRoute(MediaRouter router); + public abstract void unregisterRoute(MediaRouter router); + public abstract void updatePlaylist(List<DownloadFile> playlist); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java deleted file mode 100644 index 93075a28..00000000 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java +++ /dev/null @@ -1,32 +0,0 @@ -package github.daneren2005.dsub.util.compat; - -import github.daneren2005.dsub.domain.MusicDirectory; -import android.content.ComponentName; -import android.content.Context; -import android.support.v7.media.MediaRouter; -import android.os.Build; - -public abstract class RemoteControlClientHelper { - - public static RemoteControlClientHelper createInstance() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - return new RemoteControlClientBase(); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return new RemoteControlClientJB(); - } else { - return new RemoteControlClientICS(); - } - } - - protected RemoteControlClientHelper() { - // Avoid instantiation - } - - public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent); - public abstract void unregister(final Context context); - public abstract void setPlaybackState(final int state); - public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong); - public abstract void registerRoute(MediaRouter router); - public abstract void unregisterRoute(MediaRouter router); - -} diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java index 46b6b47d..55002c4b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java @@ -1,6 +1,7 @@ package github.daneren2005.dsub.util.compat; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.ImageLoader; import android.annotation.TargetApi; @@ -8,15 +9,18 @@ import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.media.AudioManager; import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; import android.support.v7.media.MediaRouter; +import java.util.List; + import github.daneren2005.dsub.activity.SubsonicActivity; @TargetApi(14) -public class RemoteControlClientICS extends RemoteControlClientHelper { +public class RemoteControlClientICS extends RemoteControlClientBase { private static String TAG = RemoteControlClientICS.class.getSimpleName(); protected RemoteControlClient mRemoteControl; @@ -72,15 +76,25 @@ public class RemoteControlClientICS extends RemoteControlClientHelper { updateMetadata(currentSong, editor); editor.apply(); if (currentSong == null || imageLoader == null) { - mRemoteControl.editMetadata(true) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) - .apply(); + updateAlbumArt(currentSong, null); } else { - imageLoader.loadImage(context, mRemoteControl, currentSong); + imageLoader.loadImage(context, this, currentSong); } } @Override + public void metadataChanged(MusicDirectory.Entry currentSong) { + + } + + @Override + public void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap) { + mRemoteControl.editMetadata(true) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, bitmap). + apply(); + } + + @Override public void registerRoute(MediaRouter router) { if(mRemoteControl == null) { return; @@ -98,6 +112,11 @@ public class RemoteControlClientICS extends RemoteControlClientHelper { router.removeRemoteControlClient(mRemoteControl); } + @Override + public void updatePlaylist(List<DownloadFile> playlist) { + + } + protected void updateMetadata(final MusicDirectory.Entry currentSong, final RemoteControlClient.MetadataEditor editor) { editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java new file mode 100644 index 00000000..456446f3 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java @@ -0,0 +1,495 @@ +/* + 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.v7.media.MediaRouter; + +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.MusicDirectory; +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_NEW_TASK | 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(); + } + + @Override + public void setPlaybackState(int state) { + 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); + builder.setActions(getPlaybackActions()); + + DownloadFile downloadFile = downloadService.getCurrentPlaying(); + if(downloadFile != null) { + MusicDirectory.Entry entry = downloadFile.getSong(); + addCustomActions(entry, builder); + builder.setActiveQueueItemId(entry.getId().hashCode()); + } + + PlaybackState playbackState = builder.build(); + mediaSession.setPlaybackState(playbackState); + previousState = state; + } + + @Override + public void updateMetadata(Context context, MusicDirectory.Entry currentSong) { + setMetadata(currentSong, null); + + if(currentSong != null && imageLoader != null) { + imageLoader.loadImage(context, this, currentSong); + } + } + + @Override + public void metadataChanged(MusicDirectory.Entry currentSong) { + setPlaybackState(previousState); + } + + public void setMetadata(MusicDirectory.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(MusicDirectory.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) { + MusicDirectory.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() { + long actions = PlaybackState.ACTION_PLAY | + PlaybackState.ACTION_PAUSE | + PlaybackState.ACTION_SEEK_TO | + PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM; + + int currentIndex = downloadService.getCurrentPlayingIndex(); + int size = downloadService.size(); + if(currentIndex > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if(currentIndex < size - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + + return actions; + } + protected void addCustomActions(MusicDirectory.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 MusicDirectory.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(MusicDirectory.Entry parent) throws Exception { + List<MusicDirectory.Entry> songs = new ArrayList<>(); + getSongsRecursively(parent, songs); + playSongs(songs); + } + private void getSongsRecursively(MusicDirectory.Entry parent, List<MusicDirectory.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 (MusicDirectory.Entry dir : musicDirectory.getChildren(true, false)) { + if (dir.getRating() == 1) { + continue; + } + + getSongsRecursively(dir, songs); + } + + for (MusicDirectory.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 playSong(MusicDirectory.Entry entry) { + List<MusicDirectory.Entry> entries = new ArrayList<>(); + entries.add(entry); + playSongs(entries); + } + private void playSongs(List<MusicDirectory.Entry> entries) { + playSongs(entries, false, false); + } + private void playSongs(List<MusicDirectory.Entry> entries, boolean shuffle, boolean append) { + if(!append) { + downloadService.clear(); + } + downloadService.download(entries, false, true, false, shuffle); + } + + private void noResults() { + + } + + 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.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.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); + } + } + } + + 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); + String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null); + if(playlistId != null) { + Playlist playlist = new Playlist(playlistId, null); + playPlaylist(playlist, shuffle, playLast); + } + } + + @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(); + } + } + } +} diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png Binary files differindex 74b11687..75960936 100644 --- a/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png +++ b/app/src/main/res/drawable-hdpi/action_toggle_list_dark.png diff --git a/app/src/main/res/drawable-hdpi/action_toggle_list_light.png b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png Binary files differindex 36a56aac..0b4b552d 100644 --- a/app/src/main/res/drawable-hdpi/action_toggle_list_light.png +++ b/app/src/main/res/drawable-hdpi/action_toggle_list_light.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png Binary files differnew file mode 100644 index 00000000..7f06020b --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_action_rating_bad.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png Binary files differindex 911eea25..b144900f 100644 --- a/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png +++ b/app/src/main/res/drawable-hdpi/ic_action_rating_bad_selected.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good.png Binary files differnew file mode 100644 index 00000000..42640dbb --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_action_rating_good.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png Binary files differindex 09417791..7096c8c2 100644 --- a/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png +++ b/app/src/main/res/drawable-hdpi/ic_action_rating_good_selected.png diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star.png b/app/src/main/res/drawable-hdpi/ic_toggle_star.png Binary files differindex f9ef8772..e62e5803 100644 --- a/app/src/main/res/drawable-hdpi/ic_toggle_star.png +++ b/app/src/main/res/drawable-hdpi/ic_toggle_star.png diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png Binary files differnew file mode 100644 index 00000000..fead34b3 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_toggle_star_outline.png diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png Binary files differindex 442db2fb..19fcae8f 100644 --- a/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png +++ b/app/src/main/res/drawable-mdpi/action_toggle_list_dark.png diff --git a/app/src/main/res/drawable-mdpi/action_toggle_list_light.png b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png Binary files differindex f078c702..12ecc9a0 100644 --- a/app/src/main/res/drawable-mdpi/action_toggle_list_light.png +++ b/app/src/main/res/drawable-mdpi/action_toggle_list_light.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png Binary files differnew file mode 100644 index 00000000..6cb87ce9 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_action_rating_bad.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png Binary files differindex ac563748..e96fc42e 100644 --- a/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png +++ b/app/src/main/res/drawable-mdpi/ic_action_rating_bad_selected.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good.png Binary files differnew file mode 100644 index 00000000..21ca0f92 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_action_rating_good.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png Binary files differindex 197aa106..9340e80f 100644 --- a/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png +++ b/app/src/main/res/drawable-mdpi/ic_action_rating_good_selected.png diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star.png b/app/src/main/res/drawable-mdpi/ic_toggle_star.png Binary files differindex 6d949fd0..570104dc 100644 --- a/app/src/main/res/drawable-mdpi/ic_toggle_star.png +++ b/app/src/main/res/drawable-mdpi/ic_toggle_star.png diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png Binary files differnew file mode 100644 index 00000000..656336d6 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_toggle_star_outline.png diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png Binary files differindex 1c973af3..7b05a06e 100644 --- a/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png +++ b/app/src/main/res/drawable-xhdpi/action_toggle_list_dark.png diff --git a/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png Binary files differindex 34946bee..4b890155 100644 --- a/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png +++ b/app/src/main/res/drawable-xhdpi/action_toggle_list_light.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png Binary files differnew file mode 100644 index 00000000..8d36c8e7 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png Binary files differindex 98494b1f..a88e9483 100644 --- a/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png +++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_bad_selected.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png Binary files differnew file mode 100644 index 00000000..88f6a6cb --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_good.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png Binary files differindex f5065d18..5e30e89e 100644 --- a/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png +++ b/app/src/main/res/drawable-xhdpi/ic_action_rating_good_selected.png diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star.png Binary files differindex 5c54298c..7b628888 100644 --- a/app/src/main/res/drawable-xhdpi/ic_toggle_star.png +++ b/app/src/main/res/drawable-xhdpi/ic_toggle_star.png diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png Binary files differnew file mode 100644 index 00000000..55a2d40b --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_toggle_star_outline.png diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png Binary files differindex 409cbb50..c6c78b1c 100644 --- a/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png +++ b/app/src/main/res/drawable-xxhdpi/action_toggle_list_dark.png diff --git a/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png Binary files differindex b04a1e6b..9898e130 100644 --- a/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png +++ b/app/src/main/res/drawable-xxhdpi/action_toggle_list_light.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png Binary files differnew file mode 100644 index 00000000..a09751b7 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png Binary files differindex 945bb594..5adde418 100644 --- a/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png +++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_bad_selected.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png Binary files differnew file mode 100644 index 00000000..7adc2c24 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png Binary files differindex 273615ea..de797727 100644 --- a/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png +++ b/app/src/main/res/drawable-xxhdpi/ic_action_rating_good_selected.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png Binary files differindex 4db2a37c..00e7c347 100644 --- a/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png +++ b/app/src/main/res/drawable-xxhdpi/ic_toggle_star.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png Binary files differnew file mode 100644 index 00000000..096ebba7 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_toggle_star_outline.png diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png Binary files differindex 5efdb642..93369e24 100644 --- a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png +++ b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_dark.png diff --git a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png Binary files differindex 957a3ac6..b0989dde 100644 --- a/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png +++ b/app/src/main/res/drawable-xxxhdpi/action_toggle_list_light.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png Binary files differnew file mode 100644 index 00000000..6c358480 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png Binary files differindex f585ba00..0088b4ef 100644 --- a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_bad_selected.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png Binary files differnew file mode 100644 index 00000000..b949b53b --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png Binary files differindex 9ee267f7..3fca95ef 100644 --- a/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_rating_good_selected.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png Binary files differindex 6116210d..ce14f384 100644 --- a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png +++ b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png Binary files differnew file mode 100644 index 00000000..c39bbcf5 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_toggle_star_outline.png diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f5cec71..33bbc1fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,8 @@ \nEstimated Size: %2$s</string> <string name="download.failed_to_load">Failed to load</string> <string name="download.restore_play_queue">continue from where you left off on another device at</string> + <string name="download.thumbs_up">Thumbs Up</string> + <string name="download.thumbs_down">Thumbs Down</string> <string name="sync.new_podcasts">New podcasts available</string> <string name="sync.new_playlists">New songs in playlists</string> diff --git a/app/src/main/res/xml/auto_app_description.xml b/app/src/main/res/xml/auto_app_description.xml new file mode 100644 index 00000000..48316457 --- /dev/null +++ b/app/src/main/res/xml/auto_app_description.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<automotiveApp> + <uses name="media"/> +</automotiveApp>
\ No newline at end of file |