From 8172618a098e37f8526aa784eb1a634d36c0cefa Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Sun, 11 Sep 2011 14:27:19 -0500 Subject: [PATCH] Add swipe actions to enqueue songs --- res/values/arrays.xml | 6 ++ res/values/strings.xml | 4 ++ src/org/kreed/vanilla/MediaUtils.java | 71 +++++++++++++++++---- src/org/kreed/vanilla/PlaybackActivity.java | 20 ++++++ src/org/kreed/vanilla/PlaybackService.java | 41 +++++++++++- src/org/kreed/vanilla/Song.java | 8 ++- src/org/kreed/vanilla/SongSelector.java | 2 +- src/org/kreed/vanilla/SongTimeline.java | 11 +++- 8 files changed, 145 insertions(+), 18 deletions(-) diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 75a7fcda..9757b60e 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -50,6 +50,9 @@ THE SOFTWARE. Previous Song Toggle Repeat Toggle Shuffle + Enqueue Current Album + Enqueue Current Artist + Enqueue Current Genre Starting up... diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java index 3ff35a8b..4f440bd3 100644 --- a/src/org/kreed/vanilla/MediaUtils.java +++ b/src/org/kreed/vanilla/MediaUtils.java @@ -59,30 +59,39 @@ public class MediaUtils { * @param type One of the TYPE_* constants, excluding playlists. * @param id The MediaStore id of the artist or album. * @param projection The columns to query. + * @param select An extra selection to pass to the query, or null. */ - private static Cursor getMediaCursor(int type, long id, String[] projection) + private static Cursor getMediaCursor(int type, long id, String[] projection, String select) { ContentResolver resolver = ContextApplication.getContext().getContentResolver(); Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - String selection; + StringBuilder selection = new StringBuilder(); switch (type) { case TYPE_SONG: - selection = MediaStore.Audio.Media._ID; + selection.append(MediaStore.Audio.Media._ID); break; case TYPE_ARTIST: - selection = MediaStore.Audio.Media.ARTIST_ID; + selection.append(MediaStore.Audio.Media.ARTIST_ID); break; case TYPE_ALBUM: - selection = MediaStore.Audio.Media.ALBUM_ID; + selection.append(MediaStore.Audio.Media.ALBUM_ID); break; default: throw new IllegalArgumentException("Invalid type specified: " + type); } - selection += "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0"; + selection.append('='); + selection.append(id); + selection.append(" AND is_music!=0"); + + if (select != null) { + selection.append(" AND "); + selection.append(select); + } + String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK; - return resolver.query(media, projection, selection, null, sort); + return resolver.query(media, projection, selection.toString(), null, sort); } /** @@ -124,24 +133,28 @@ public class MediaUtils { * constants. * @param id The id of the element in the MediaStore content provider for * the given type. + * @param selection An extra selection to be passed to the query. May be + * null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST */ - public static long[] getAllSongIdsWith(int type, long id) + public static long[] getAllSongIdsWith(int type, long id, String selection) { Cursor cursor; switch (type) { case TYPE_SONG: + assert(selection == null); return new long[] { id }; case TYPE_ARTIST: case TYPE_ALBUM: - cursor = getMediaCursor(type, id, new String[] { MediaStore.Audio.Media._ID }); + cursor = getMediaCursor(type, id, new String[] { MediaStore.Audio.Media._ID }, selection); break; case TYPE_PLAYLIST: + assert(selection == null); cursor = getPlaylistCursor(id, new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID }); break; case TYPE_GENRE: // NOTE: AUDIO_ID does not seem to work here, strangely. - cursor = queryGenre(id, new String[] { MediaStore.Audio.Genres.Members._ID }, null, null); + cursor = queryGenre(id, new String[] { MediaStore.Audio.Genres.Members._ID }, selection, null); break; default: throw new IllegalArgumentException("Specified type not valid: " + type); @@ -178,7 +191,7 @@ public class MediaUtils { ContentResolver resolver = ContextApplication.getContext().getContentResolver(); String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA }; - Cursor cursor = getMediaCursor(type, id, projection); + Cursor cursor = getMediaCursor(type, id, projection, null); if (cursor != null) { PlaybackService service = ContextApplication.hasService() ? ContextApplication.getService() : null; @@ -200,6 +213,42 @@ public class MediaUtils { return count; } + /** + * Query the MediaStore to determine the id of the genre the song belongs + * to. + */ + public static long queryGenreForSong(long id) + { + // This is terribly inefficient, but it seems to be the only way to do + // this. Honeycomb introduced an API to query the genre of the song. + // We should look into it when ICS is released. + + ContentResolver resolver = ContextApplication.getContext().getContentResolver(); + + // query ids of all the genres + Uri uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; + String[] projection = { "_id" }; + Cursor cursor = resolver.query(uri, projection, null, null, null); + + if (cursor != null) { + String selection = "_id=" + id; + while (cursor.moveToNext()) { + // check if the given song belongs to this genre + long genreId = cursor.getLong(0); + Uri genreUri = MediaStore.Audio.Genres.Members.getContentUri("external", genreId); + Cursor c = resolver.query(genreUri, projection, selection, null, null); + if (c != null) { + if (c.getCount() == 1) + return genreId; + c.close(); + } + } + cursor.close(); + } + + return -1; + } + /** * Shuffle an array using Fisher-Yates algorithm. * diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java index 4f4a860e..3e183631 100644 --- a/src/org/kreed/vanilla/PlaybackActivity.java +++ b/src/org/kreed/vanilla/PlaybackActivity.java @@ -36,6 +36,7 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.widget.Toast; public class PlaybackActivity extends Activity implements Handler.Callback, View.OnClickListener, CoverView.Callback { public static final int ACTION_NOTHING = 0; @@ -45,6 +46,9 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View public static final int ACTION_PREVIOUS_SONG = 4; public static final int ACTION_REPEAT = 5; public static final int ACTION_SHUFFLE = 6; + public static final int ACTION_ENQUEUE_ALBUM = 7; + public static final int ACTION_ENQUEUE_ARTIST = 8; + public static final int ACTION_ENQUEUE_GENRE = 9; public static int mUpAction; public static int mDownAction; @@ -359,6 +363,13 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View startActivity(new Intent(this, SongSelector.class)); } + public void enqueue(int type) + { + int count = ContextApplication.getService().enqueueFromCurrent(type); + String text = getResources().getQuantityString(R.plurals.enqueued_count, count, count); + Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); + } + public void performAction(int action) { switch (action) { @@ -382,6 +393,15 @@ public class PlaybackActivity extends Activity implements Handler.Callback, View case ACTION_SHUFFLE: toggleShuffle(); break; + case ACTION_ENQUEUE_ALBUM: + enqueue(MediaUtils.TYPE_ALBUM); + break; + case ACTION_ENQUEUE_ARTIST: + enqueue(MediaUtils.TYPE_ARTIST); + break; + case ACTION_ENQUEUE_GENRE: + enqueue(MediaUtils.TYPE_GENRE); + break; default: throw new IllegalArgumentException("Invalid action: " + action); } diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java index 7f436ed6..9bd63d38 100644 --- a/src/org/kreed/vanilla/PlaybackService.java +++ b/src/org/kreed/vanilla/PlaybackService.java @@ -981,7 +981,7 @@ public final class PlaybackService extends Service implements Handler.Callback, */ public Song playSongs(int type, long id) { - mTimeline.chooseSongs(false, type, id); + mTimeline.chooseSongs(false, type, id, null); return setCurrentSong(+1); } @@ -999,11 +999,48 @@ public final class PlaybackService extends Service implements Handler.Callback, */ public void enqueueSongs(int type, long id) { - mTimeline.chooseSongs(true, type, id); + mTimeline.chooseSongs(true, type, id, null); mHandler.removeMessages(SAVE_STATE); mHandler.sendEmptyMessageDelayed(SAVE_STATE, 5000); } + /** + * Enqueues all the songs with the same album/artist/genre as the current + * song. + * + * This will clear the queue and place the first song from the group after + * the playing song. + * + * @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST, + * or TYPE_GENRE + * @return The number of songs that were enqueued. + */ + public int enqueueFromCurrent(int type) + { + Song current = getSong(0); + if (current == null) + return 0; + + long id = -1; + switch (type) { + case MediaUtils.TYPE_ARTIST: + id = current.artistId; + break; + case MediaUtils.TYPE_ALBUM: + id = current.albumId; + break; + case MediaUtils.TYPE_GENRE: + id = MediaUtils.queryGenreForSong(current.id); + break; + default: + throw new IllegalArgumentException("Unsupported media type: " + type); + } + + String selection = "_id!=" + current.id; + int count = mTimeline.chooseSongs(false, type, id, selection); + return count; + } + /** * Reset the position at which songs are enqueued. That is, the next song * enqueued will be placed directly after the playing song. diff --git a/src/org/kreed/vanilla/Song.java b/src/org/kreed/vanilla/Song.java index cc3c13a6..6afa99ef 100644 --- a/src/org/kreed/vanilla/Song.java +++ b/src/org/kreed/vanilla/Song.java @@ -86,6 +86,7 @@ public class Song implements Parcelable { MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM_ID, + MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.DURATION }; @@ -97,6 +98,10 @@ public class Song implements Parcelable { * Id of this song's album in the MediaStore */ public long albumId; + /** + * Id of this song's artist in the MediaStore + */ + public long artistId; /** * Path to the data for this song @@ -293,7 +298,8 @@ public class Song implements Parcelable { album = cursor.getString(3); artist = cursor.getString(4); albumId = cursor.getLong(5); - duration = cursor.getLong(6); + artistId = cursor.getLong(6); + duration = cursor.getLong(7); } /** diff --git a/src/org/kreed/vanilla/SongSelector.java b/src/org/kreed/vanilla/SongSelector.java index 4fcf5b3e..3a8c7b79 100644 --- a/src/org/kreed/vanilla/SongSelector.java +++ b/src/org/kreed/vanilla/SongSelector.java @@ -466,7 +466,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem */ private void addToPlaylist(long playlistId, int type, long mediaId, CharSequence title) { - long[] ids = MediaUtils.getAllSongIdsWith(type, mediaId); + long[] ids = MediaUtils.getAllSongIdsWith(type, mediaId, null); Playlist.addToPlaylist(playlistId, ids); String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title); diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java index 0b2518f1..d25bc708 100644 --- a/src/org/kreed/vanilla/SongTimeline.java +++ b/src/org/kreed/vanilla/SongTimeline.java @@ -367,12 +367,15 @@ public final class SongTimeline { * MediaUtils.FIELD_* constants. * @param id The id of the element in the MediaStore content provider for * the given type. + * @param selection An extra selection to be passed to the query. May be + * null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST + * @return The number of songs that were enqueued. */ - public void chooseSongs(boolean enqueue, int type, long id) + public int chooseSongs(boolean enqueue, int type, long id, String selection) { - long[] songs = MediaUtils.getAllSongIdsWith(type, id); + long[] songs = MediaUtils.getAllSongIdsWith(type, id, selection); if (songs == null || songs.length == 0) - return; + return 0; if (mShuffle) MediaUtils.shuffle(songs); @@ -404,6 +407,8 @@ public final class SongTimeline { Song newSong = getSong(+1); if (newSong != oldSong && mCallback != null) mCallback.songReplaced(+1, newSong); + + return songs.length; } /**