From fad3b1160b0ceb1223fc8fd80b07bb5e27422c94 Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Sun, 16 May 2010 11:46:04 -0500 Subject: [PATCH] Add support for playlist playback No support for creation or editing yet --- res/drawable/ic_tab_playlists_selected.png | Bin 0 -> 814 bytes res/drawable/ic_tab_playlists_unselected.png | Bin 0 -> 477 bytes res/drawable/tab_playlists.xml | 9 + res/layout/song_selector.xml | 6 + res/values/strings.xml | 1 + src/org/kreed/vanilla/MediaAdapter.java | 172 +++++++++---------- src/org/kreed/vanilla/Song.java | 98 ++++++++--- src/org/kreed/vanilla/SongMediaAdapter.java | 6 +- src/org/kreed/vanilla/SongSelector.java | 11 +- src/org/kreed/vanilla/SongTimeline.java | 6 +- 10 files changed, 186 insertions(+), 123 deletions(-) create mode 100644 res/drawable/ic_tab_playlists_selected.png create mode 100644 res/drawable/ic_tab_playlists_unselected.png create mode 100644 res/drawable/tab_playlists.xml diff --git a/res/drawable/ic_tab_playlists_selected.png b/res/drawable/ic_tab_playlists_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..78b128e559d54187a66db2ab4040a06e7b58ba8e GIT binary patch literal 814 zcmV+}1JV46P);T#&1GN(z${?SMPMJ6{??0Wu+?g}WfOd*O?PlNtVuhzqp4_FE13-3x z>;%~jiho`pmI4ORH5L{YY9~Cb;Lo2w|5#aBwSdfLOt3^qFUvsyY8NPxGvc-M#DDIGykdX5D;lrEMP6r@6L3T4j9Uub~yNc|pw{PCO5)l>k z0x~}f2?}ygpE@C>lg+1c5DK?@YjQs>`4reD8)vUz!VoB0Cu zq02KeGHL^7cdm5lMASw-f{`B$PmCIL-f!O@~{6erOxOx5hsdw+*y&}dUd3kva zDH$0RlA{0wjvhX|0Vrn&#Fn}`I^HmG5F03F48)p1tbqfAd}z_q*jRxt3YZuM7~qWp zT|HgfV@HlK!=eBtzIxTFI#QwlZ(11WL^!yk08|-GoX|fX#1<11=WA?f=?1Y$DT>|Q zJxl_EgHy?J05G0k&X_&t29Wt1nDT{y4giZI6~*6hIRLpR20DPe5D*d)63<9Y(SU^j zOgua^R2Nzl6AS@DMe%4N925=!#o=FiG`)};rLf)+6RgMb>gCG^^l|{yE^yC?3EEft zwq?`C$@Fr-j;&i~LbLE6i*`d88T6a#rLEDnx sC}2eEp2NE4wCtY4y6E8UIY58`0ObXi!xaK92mk;807*qoM6N<$f>sh|ZvX%Q literal 0 HcmV?d00001 diff --git a/res/drawable/ic_tab_playlists_unselected.png b/res/drawable/ic_tab_playlists_unselected.png new file mode 100644 index 0000000000000000000000000000000000000000..969366b50493cfaa3468fb7653960683b9a11120 GIT binary patch literal 477 zcmV<30V4j1P)f9tnya05n7eNc94-A5_Q^DE6AjP}U{a0X#rU zgn)Dr6mufUZKp|5j0QlKzolzkfCan&;!RMt1X2`$%JN4<+5xIp49IoBdSGJ9f~Et0 zAoT$%9zjAP#9PMgxZbU+h^1AzG&RP6=<@o^ej3^F?i)+<8knvK|801#jR|D9=G TzB-=C00000NkvXXu0mjfSlqJB literal 0 HcmV?d00001 diff --git a/res/drawable/tab_playlists.xml b/res/drawable/tab_playlists.xml new file mode 100644 index 00000000..d3943ec3 --- /dev/null +++ b/res/drawable/tab_playlists.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/layout/song_selector.xml b/res/layout/song_selector.xml index e8f34717..17852019 100644 --- a/res/layout/song_selector.xml +++ b/res/layout/song_selector.xml @@ -57,6 +57,12 @@ android:layout_height="fill_parent" android:divider="@null" android:fastScrollEnabled="true" /> + diff --git a/res/values/strings.xml b/res/values/strings.xml index 4f7d3cc2..7776dac3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -50,6 +50,7 @@ Artists Albums Songs + Playlists None diff --git a/src/org/kreed/vanilla/MediaAdapter.java b/src/org/kreed/vanilla/MediaAdapter.java index 96f41642..2dd1338c 100644 --- a/src/org/kreed/vanilla/MediaAdapter.java +++ b/src/org/kreed/vanilla/MediaAdapter.java @@ -55,16 +55,11 @@ import android.widget.FilterQueryProvider; */ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { /** - * Type indicating that MediaStore.Audio.Artists should be used as the - * provider backing this adapter. + * The type of media represented by this adapter. Must be one of the + * Song.FIELD_* constants. Determines which content provider to query for + * media and what fields to display. */ - public static final int TYPE_ARTIST = 1; - /** - * Type indicating that MediaStore.Audio.Albums should be used as the - * adapter backing this adapter. - */ - public static final int TYPE_ALBUM = 2; - + int mType; Uri mStore; String[] mFields; private String[] mFieldKeys; @@ -73,16 +68,51 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { private CharSequence mConstraint; /** - * Perform required setup during construction. See constructors for - * details. + * Construct a MediaAdapter representing the given type of + * media. + * + * @param context A Context to use + * @param type The type of media to represent. Must be one of the + * Song.TYPE_* constants. This determines which content provider to query + * and what fields to display in the views. + * @param expandable Whether an expand arrow should be shown to the right + * of the views' text + * @param requery If true, automatically update the adapter when the + * provider backing it changes */ - private void init(Context context, Uri store, String[] fields, String[] fieldKeys, boolean expandable) + public MediaAdapter(Context context, int type, boolean expandable, boolean requery) { - mStore = store; - mFields = fields; - mFieldKeys = fieldKeys; + super(context, null, requery); + + mType = type; mExpandable = expandable; + switch (type) { + case Song.TYPE_ARTIST: + mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; + mFields = new String[] { MediaStore.Audio.Artists.ARTIST }; + mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY }; + break; + case Song.TYPE_ALBUM: + mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; + mFields = new String[] { MediaStore.Audio.Albums.ARTIST, MediaStore.Audio.Albums.ALBUM }; + // Why is there no artist_key column constant in the album MediaStore? The column does seem to exist. + mFieldKeys = new String[] { "artist_key", MediaStore.Audio.Albums.ALBUM_KEY }; + break; + case Song.TYPE_SONG: + mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + mFields = new String[] { MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE }; + mFieldKeys = new String[] { MediaStore.Audio.Media.ARTIST_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.TITLE_KEY }; + break; + case Song.TYPE_PLAYLIST: + mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; + mFields = new String[] { MediaStore.Audio.Playlists.NAME }; + mFieldKeys = null; + break; + default: + throw new IllegalArgumentException("Invalid value for type: " + type); + } + setFilterQueryProvider(this); requery(); @@ -97,65 +127,6 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { } } - /** - * Construct a MediaAdapter representing an arbitrary media content - * provider. - * - * @param context A Context to use - * @param store The external content uri of the provider - * @param fields The fields to use in the provider. The last field will be - * displayed as the first line in views. If more than one field is given, - * the first field will be displayed as the bottom line. - * @param fieldKeys The sorting keys corresponding to each field from - * fields. Used for filtering. - * @param expandable Whether an expand arrow should be shown to the right - * of the views' text - * @param requery If true, automatically update the adapter when the - * provider backing it changes - */ - protected MediaAdapter(Context context, Uri store, String[] fields, String[] fieldKeys, boolean expandable, boolean requery) - { - super(context, null, requery); - init(context, store, fields, fieldKeys, expandable); - } - - /** - * Construct a MediaAdapter representing the given type of - * media. - * - * @param context A Context to use - * @param type The type of media; one of TYPE_ALBUM or TYPE_ARTIST - * @param expandable Whether an expand arrow should be shown to the right - * of the views' text - * @param requery If true, automatically update the adapter when the - * provider backing it changes - */ - public MediaAdapter(Context context, int type, boolean expandable, boolean requery) - { - super(context, null, requery); - - Uri store; - String[] fields; - String[] fieldKeys; - switch (type) { - case TYPE_ARTIST: - store = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; - fields = new String[] { MediaStore.Audio.Artists.ARTIST }; - fieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY }; - break; - case TYPE_ALBUM: - store = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; - fields = new String[] { MediaStore.Audio.Albums.ARTIST, MediaStore.Audio.Albums.ALBUM }; - // Why is there no artist_key column constant in the album MediaStore? The column does seem to exist. - fieldKeys = new String[] { "artist_key", MediaStore.Audio.Albums.ALBUM_KEY }; - break; - default: - throw new IllegalArgumentException("Invalid value for type: " + type); - } - - init(context, store, fields, fieldKeys, expandable); - } - public final void requery() { changeCursor(runQuery(mConstraint)); @@ -174,7 +145,8 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { protected String getSortOrder() { - return mFieldKeys[mFieldKeys.length - 1]; + String[] source = mFieldKeys == null ? mFields : mFieldKeys; + return source[source.length - 1]; } public Cursor runQuery(CharSequence constraint) @@ -201,11 +173,20 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { } if (constraint != null && constraint.length() != 0) { - String colKey = MediaStore.Audio.keyFor(constraint.toString()); - String spaceColKey = DatabaseUtils.getCollationKey(" "); - String[] colKeys = colKey.split(spaceColKey); + String[] needles; - int size = colKeys.length; + // If we are using sorting keys, we need to change our constraint + // into a list of collation keys. Otherwise, just split the + // constraint with no modification. + if (mFieldKeys != null) { + String colKey = MediaStore.Audio.keyFor(constraint.toString()); + String spaceColKey = DatabaseUtils.getCollationKey(" "); + needles = colKey.split(spaceColKey); + } else { + needles = constraint.toString().split("\\s+"); + } + + int size = needles.length; if (limiter != null) ++size; selectionArgs = new String[size]; @@ -215,23 +196,23 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { i = 1; } - String keys = mFieldKeys[0]; - for (int j = 1; j != mFieldKeys.length; ++j) - keys += "||" + mFieldKeys[j]; + String[] keySource = mFieldKeys == null ? mFields : mFieldKeys; + String keys = keySource[0]; + for (int j = 1; j != keySource.length; ++j) + keys += "||" + keySource[j]; - for (int j = 0; j != colKeys.length; ++i, ++j) { - selectionArgs[i] = '%' + colKeys[j] + '%'; + for (int j = 0; j != needles.length; ++i, ++j) { + selectionArgs[i] = '%' + needles[j] + '%'; - if (j != 0 || selection.length() != 0) + if (i != 0) selection.append(" AND "); - selection.append(keys); - selection.append(" LIKE ?"); + selection.append(keys); + selection.append(" LIKE ?"); } + } else if (limiter != null) { + selectionArgs = new String[] { limiter }; } else { - if (limiter != null) - selectionArgs = new String[] { limiter }; - else - selectionArgs = null; + selectionArgs = null; } String[] projection; @@ -391,6 +372,15 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { return mId; } + /** + * Returns the type of media contained in the adapter containing this + * view. Will be one of the Song.TYPE_* constants. + */ + public int getMediaType() + { + return mType; + } + public final String getTitle() { return mTitle; diff --git a/src/org/kreed/vanilla/Song.java b/src/org/kreed/vanilla/Song.java index fd0ebc13..924eb2b0 100644 --- a/src/org/kreed/vanilla/Song.java +++ b/src/org/kreed/vanilla/Song.java @@ -45,6 +45,23 @@ public class Song implements Parcelable { */ public static final int FLAG_RANDOM = 0x1; + /** + * Type indicating an id represents an artist. + */ + public static final int TYPE_ARTIST = 1; + /** + * Type indicating an id represents an album. + */ + public static final int TYPE_ALBUM = 2; + /** + * Type indicating an id represents a song. + */ + public static final int TYPE_SONG = 3; + /** + * Type indicating an id represents a playlist. + */ + public static final int TYPE_PLAYLIST = 4; + private static final String[] FILLED_PROJECTION = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA, @@ -180,36 +197,77 @@ public class Song implements Parcelable { } /** - * Return an array containing all the song ids that match the specified parameters + * Return a cursor containing the ids of all the songs with artist or + * album of the specified id. * - * @param type Type the id represent. May be 1, 2 or 3, meaning artist, - * album or song, respectively. - * @param id Id of the element + * @param resolver The ContentResolver to run the query with. + * @param type TYPE_ARTIST or TYPE_ALBUM, indicating the the id represents + * an artist or album + * @param id The MediaStore id of the artist or album */ - public static long[] getAllSongIdsWith(int type, long id) + private static Cursor getMediaCursor(ContentResolver resolver, int type, long id) { - if (type == 3) - return new long[] { id }; - - String selection = "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0"; + String selection = '=' + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0"; switch (type) { - case 2: - selection = MediaStore.Audio.Media.ALBUM_ID + selection; - break; - case 1: + case TYPE_ARTIST: selection = MediaStore.Audio.Media.ARTIST_ID + selection; break; + case TYPE_ALBUM: + selection = MediaStore.Audio.Media.ALBUM_ID + selection; + break; default: - return null; + throw new IllegalArgumentException("Invalid type specified: " + type); } + Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String[] projection = { MediaStore.Audio.Media._ID }; - - ContentResolver resolver = ContextApplication.getContext().getContentResolver(); String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK; - Cursor cursor = resolver.query(media, projection, selection, null, sort); + return resolver.query(media, projection, selection, null, sort); + } + + /** + * Return a cursor containing the ids of all the songs in the playlist + * with the given id. + * + * @param resolver The ContentResolver to run the query with. + * @param id The id of the playlist in MediaStore.Audio.Playlists. + */ + private static Cursor getPlaylistCursor(ContentResolver resolver, long id) + { + Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id); + String[] projection = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID }; + return resolver.query(uri, projection, null, null, + MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER); + } + + /** + * Return an array containing all the song ids that match the specified parameters + * + * @param type Type the id represents. Must be one of the Song.TYPE_* + * constants. + * @param id The id of the element in the MediaStore content provider for + * the given type. + */ + public static long[] getAllSongIdsWith(int type, long id) + { + ContentResolver resolver = ContextApplication.getContext().getContentResolver(); + Cursor cursor; + + switch (type) { + case TYPE_SONG: + return new long[] { id }; + case TYPE_ARTIST: + case TYPE_ALBUM: + cursor = getMediaCursor(resolver, type, id); + break; + case TYPE_PLAYLIST: + cursor = getPlaylistCursor(resolver, id); + break; + default: + throw new IllegalArgumentException("Specified type not valid: " + type); + } if (cursor == null) return null; @@ -222,7 +280,7 @@ public class Song implements Parcelable { for (int i = 0; i != count; ++i) { if (!cursor.moveToNext()) return null; - songs[i] = cursor.getInt(0); + songs[i] = cursor.getLong(0); } cursor.close(); @@ -353,8 +411,8 @@ public class Song implements Parcelable { if (fileDescriptor != null) { // Construct a MediaScanner - Class mediaScannerClass = Class.forName("android.media.MediaScanner"); - Constructor mediaScannerConstructor = mediaScannerClass.getDeclaredConstructor(Context.class); + Class mediaScannerClass = Class.forName("android.media.MediaScanner"); + Constructor mediaScannerConstructor = mediaScannerClass.getDeclaredConstructor(Context.class); Object mediaScanner = mediaScannerConstructor.newInstance(ContextApplication.getContext()); // Call extractAlbumArt(fileDescriptor) diff --git a/src/org/kreed/vanilla/SongMediaAdapter.java b/src/org/kreed/vanilla/SongMediaAdapter.java index f61bf959..b9d21b51 100644 --- a/src/org/kreed/vanilla/SongMediaAdapter.java +++ b/src/org/kreed/vanilla/SongMediaAdapter.java @@ -38,11 +38,7 @@ public class SongMediaAdapter extends MediaAdapter { */ public SongMediaAdapter(Context context, boolean expandable, boolean requery) { - super(context, - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - new String[] { MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE }, - new String[] { MediaStore.Audio.Media.ARTIST_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.TITLE_KEY }, - expandable, requery); + super(context, Song.TYPE_SONG, expandable, requery); } @Override diff --git a/src/org/kreed/vanilla/SongSelector.java b/src/org/kreed/vanilla/SongSelector.java index 9817fbbc..8f073fbc 100644 --- a/src/org/kreed/vanilla/SongSelector.java +++ b/src/org/kreed/vanilla/SongSelector.java @@ -90,6 +90,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem mTabHost.addTab(mTabHost.newTabSpec("tab_artists").setIndicator(res.getText(R.string.artists), res.getDrawable(R.drawable.tab_artists)).setContent(R.id.artist_list)); mTabHost.addTab(mTabHost.newTabSpec("tab_albums").setIndicator(res.getText(R.string.albums), res.getDrawable(R.drawable.tab_albums)).setContent(R.id.album_list)); mTabHost.addTab(mTabHost.newTabSpec("tab_songs").setIndicator(res.getText(R.string.songs), res.getDrawable(R.drawable.tab_songs)).setContent(R.id.song_list)); + mTabHost.addTab(mTabHost.newTabSpec("tab_playlists").setIndicator(res.getText(R.string.playlists), res.getDrawable(R.drawable.tab_playlists)).setContent(R.id.playlist_list)); mSearchBox = findViewById(R.id.search_box); @@ -189,11 +190,10 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); long id = view.getMediaId(); - int field = view.getFieldCount(); Intent intent = new Intent(this, PlaybackService.class); intent.setAction(action); - intent.putExtra("type", field); + intent.putExtra("type", view.getMediaType()); intent.putExtra("id", id); startService(intent); @@ -413,9 +413,10 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem { switch (message.what) { case MSG_INIT: - setupView(R.id.artist_list, new MediaAdapter(this, MediaAdapter.TYPE_ARTIST, true, false)); - setupView(R.id.album_list, new MediaAdapter(this, MediaAdapter.TYPE_ALBUM, true, false)); + setupView(R.id.artist_list, new MediaAdapter(this, Song.TYPE_ARTIST, true, false)); + setupView(R.id.album_list, new MediaAdapter(this, Song.TYPE_ALBUM, true, false)); setupView(R.id.song_list, new SongMediaAdapter(this, false, false)); + setupView(R.id.playlist_list, new MediaAdapter(this, Song.TYPE_PLAYLIST, false, true)); ContentResolver resolver = getContentResolver(); Observer observer = new Observer(mHandler); @@ -440,7 +441,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem runOnUiThread(new Runnable() { public void run() { - for (int i = 0; i != 3; ++i) + for (int i = 0; i != 4; ++i) getAdapter(i).requery(); } }); diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java index 1db5d229..35ac65b1 100644 --- a/src/org/kreed/vanilla/SongTimeline.java +++ b/src/org/kreed/vanilla/SongTimeline.java @@ -352,8 +352,10 @@ public final class SongTimeline { * will be ordered by album and then by track number. * * @param enqueue If true, enqueue the set. If false, play the set. - * @param type 1, 2, or 3, indicating artist, album, or song, respectively. - * @param id The MediaStore id of the artist, album, or song. + * @param type The type represented by the id. Must be one of the + * Song.FIELD_* constants. + * @param id The id of the element in the MediaStore content provider for + * the given type. */ public void chooseSongs(boolean enqueue, int type, long id) {