diff --git a/res/drawable/ic_tab_playlists_selected.png b/res/drawable/ic_tab_playlists_selected.png
new file mode 100644
index 00000000..78b128e5
Binary files /dev/null and b/res/drawable/ic_tab_playlists_selected.png differ
diff --git a/res/drawable/ic_tab_playlists_unselected.png b/res/drawable/ic_tab_playlists_unselected.png
new file mode 100644
index 00000000..969366b5
Binary files /dev/null and b/res/drawable/ic_tab_playlists_unselected.png differ
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)
{