Add support for playlist playback

No support for creation or editing yet
This commit is contained in:
Christopher Eby 2010-05-16 11:46:04 -05:00
parent 7090c468aa
commit fad3b1160b
10 changed files with 186 additions and 123 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_selected="true"
android:state_pressed="false"
android:drawable="@drawable/ic_tab_playlists_selected" />
<item
android:drawable="@drawable/ic_tab_playlists_unselected" />
</selector>

View File

@ -57,6 +57,12 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:divider="@null" android:divider="@null"
android:fastScrollEnabled="true" /> android:fastScrollEnabled="true" />
<ListView
android:id="@+id/playlist_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="@null"
android:fastScrollEnabled="true" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
</TabHost> </TabHost>

View File

@ -50,6 +50,7 @@
<string name="artists">Artists</string> <string name="artists">Artists</string>
<string name="albums">Albums</string> <string name="albums">Albums</string>
<string name="songs">Songs</string> <string name="songs">Songs</string>
<string name="playlists">Playlists</string>
<string name="none">None</string> <string name="none">None</string>

View File

@ -55,16 +55,11 @@ import android.widget.FilterQueryProvider;
*/ */
public class MediaAdapter extends CursorAdapter implements FilterQueryProvider { public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
/** /**
* Type indicating that MediaStore.Audio.Artists should be used as the * The type of media represented by this adapter. Must be one of the
* provider backing this adapter. * Song.FIELD_* constants. Determines which content provider to query for
* media and what fields to display.
*/ */
public static final int TYPE_ARTIST = 1; int mType;
/**
* Type indicating that MediaStore.Audio.Albums should be used as the
* adapter backing this adapter.
*/
public static final int TYPE_ALBUM = 2;
Uri mStore; Uri mStore;
String[] mFields; String[] mFields;
private String[] mFieldKeys; private String[] mFieldKeys;
@ -73,16 +68,51 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
private CharSequence mConstraint; private CharSequence mConstraint;
/** /**
* Perform required setup during construction. See constructors for * Construct a MediaAdapter representing the given <code>type</code> of
* details. * 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; super(context, null, requery);
mFields = fields;
mFieldKeys = fieldKeys; mType = type;
mExpandable = expandable; 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); setFilterQueryProvider(this);
requery(); 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
* <code>fields</code>. 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 <code>type</code> 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() public final void requery()
{ {
changeCursor(runQuery(mConstraint)); changeCursor(runQuery(mConstraint));
@ -174,7 +145,8 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
protected String getSortOrder() protected String getSortOrder()
{ {
return mFieldKeys[mFieldKeys.length - 1]; String[] source = mFieldKeys == null ? mFields : mFieldKeys;
return source[source.length - 1];
} }
public Cursor runQuery(CharSequence constraint) public Cursor runQuery(CharSequence constraint)
@ -201,11 +173,20 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
} }
if (constraint != null && constraint.length() != 0) { if (constraint != null && constraint.length() != 0) {
String[] needles;
// 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 colKey = MediaStore.Audio.keyFor(constraint.toString());
String spaceColKey = DatabaseUtils.getCollationKey(" "); String spaceColKey = DatabaseUtils.getCollationKey(" ");
String[] colKeys = colKey.split(spaceColKey); needles = colKey.split(spaceColKey);
} else {
needles = constraint.toString().split("\\s+");
}
int size = colKeys.length; int size = needles.length;
if (limiter != null) if (limiter != null)
++size; ++size;
selectionArgs = new String[size]; selectionArgs = new String[size];
@ -215,22 +196,22 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
i = 1; i = 1;
} }
String keys = mFieldKeys[0]; String[] keySource = mFieldKeys == null ? mFields : mFieldKeys;
for (int j = 1; j != mFieldKeys.length; ++j) String keys = keySource[0];
keys += "||" + mFieldKeys[j]; for (int j = 1; j != keySource.length; ++j)
keys += "||" + keySource[j];
for (int j = 0; j != colKeys.length; ++i, ++j) { for (int j = 0; j != needles.length; ++i, ++j) {
selectionArgs[i] = '%' + colKeys[j] + '%'; selectionArgs[i] = '%' + needles[j] + '%';
if (j != 0 || selection.length() != 0) if (i != 0)
selection.append(" AND "); selection.append(" AND ");
selection.append(keys); selection.append(keys);
selection.append(" LIKE ?"); selection.append(" LIKE ?");
} }
} else { } else if (limiter != null) {
if (limiter != null)
selectionArgs = new String[] { limiter }; selectionArgs = new String[] { limiter };
else } else {
selectionArgs = null; selectionArgs = null;
} }
@ -391,6 +372,15 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
return mId; 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() public final String getTitle()
{ {
return mTitle; return mTitle;

View File

@ -45,6 +45,23 @@ public class Song implements Parcelable {
*/ */
public static final int FLAG_RANDOM = 0x1; 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 = { private static final String[] FILLED_PROJECTION = {
MediaStore.Audio.Media._ID, MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DATA, 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, * @param resolver The ContentResolver to run the query with.
* album or song, respectively. * @param type TYPE_ARTIST or TYPE_ALBUM, indicating the the id represents
* @param id Id of the element * 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) String selection = '=' + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0";
return new long[] { id };
String selection = "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0";
switch (type) { switch (type) {
case 2: case TYPE_ARTIST:
selection = MediaStore.Audio.Media.ALBUM_ID + selection;
break;
case 1:
selection = MediaStore.Audio.Media.ARTIST_ID + selection; selection = MediaStore.Audio.Media.ARTIST_ID + selection;
break; break;
case TYPE_ALBUM:
selection = MediaStore.Audio.Media.ALBUM_ID + selection;
break;
default: default:
return null; throw new IllegalArgumentException("Invalid type specified: " + type);
} }
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID }; 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; 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) if (cursor == null)
return null; return null;
@ -222,7 +280,7 @@ public class Song implements Parcelable {
for (int i = 0; i != count; ++i) { for (int i = 0; i != count; ++i) {
if (!cursor.moveToNext()) if (!cursor.moveToNext())
return null; return null;
songs[i] = cursor.getInt(0); songs[i] = cursor.getLong(0);
} }
cursor.close(); cursor.close();
@ -353,8 +411,8 @@ public class Song implements Parcelable {
if (fileDescriptor != null) { if (fileDescriptor != null) {
// Construct a MediaScanner // Construct a MediaScanner
Class mediaScannerClass = Class.forName("android.media.MediaScanner"); Class<?> mediaScannerClass = Class.forName("android.media.MediaScanner");
Constructor mediaScannerConstructor = mediaScannerClass.getDeclaredConstructor(Context.class); Constructor<?> mediaScannerConstructor = mediaScannerClass.getDeclaredConstructor(Context.class);
Object mediaScanner = mediaScannerConstructor.newInstance(ContextApplication.getContext()); Object mediaScanner = mediaScannerConstructor.newInstance(ContextApplication.getContext());
// Call extractAlbumArt(fileDescriptor) // Call extractAlbumArt(fileDescriptor)

View File

@ -38,11 +38,7 @@ public class SongMediaAdapter extends MediaAdapter {
*/ */
public SongMediaAdapter(Context context, boolean expandable, boolean requery) public SongMediaAdapter(Context context, boolean expandable, boolean requery)
{ {
super(context, super(context, Song.TYPE_SONG, expandable, requery);
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);
} }
@Override @Override

View File

@ -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_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_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_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); 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(); Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
long id = view.getMediaId(); long id = view.getMediaId();
int field = view.getFieldCount();
Intent intent = new Intent(this, PlaybackService.class); Intent intent = new Intent(this, PlaybackService.class);
intent.setAction(action); intent.setAction(action);
intent.putExtra("type", field); intent.putExtra("type", view.getMediaType());
intent.putExtra("id", id); intent.putExtra("id", id);
startService(intent); startService(intent);
@ -413,9 +413,10 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
{ {
switch (message.what) { switch (message.what) {
case MSG_INIT: case MSG_INIT:
setupView(R.id.artist_list, new MediaAdapter(this, MediaAdapter.TYPE_ARTIST, true, false)); setupView(R.id.artist_list, new MediaAdapter(this, Song.TYPE_ARTIST, true, false));
setupView(R.id.album_list, new MediaAdapter(this, MediaAdapter.TYPE_ALBUM, 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.song_list, new SongMediaAdapter(this, false, false));
setupView(R.id.playlist_list, new MediaAdapter(this, Song.TYPE_PLAYLIST, false, true));
ContentResolver resolver = getContentResolver(); ContentResolver resolver = getContentResolver();
Observer observer = new Observer(mHandler); Observer observer = new Observer(mHandler);
@ -440,7 +441,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() public void run()
{ {
for (int i = 0; i != 3; ++i) for (int i = 0; i != 4; ++i)
getAdapter(i).requery(); getAdapter(i).requery();
} }
}); });

View File

@ -352,8 +352,10 @@ public final class SongTimeline {
* will be ordered by album and then by track number. * will be ordered by album and then by track number.
* *
* @param enqueue If true, enqueue the set. If false, play the set. * @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 type The type represented by the id. Must be one of the
* @param id The MediaStore id of the artist, album, or song. * 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) public void chooseSongs(boolean enqueue, int type, long id)
{ {