Add support for playlist playback
No support for creation or editing yet
This commit is contained in:
parent
7090c468aa
commit
fad3b1160b
BIN
res/drawable/ic_tab_playlists_selected.png
Normal file
BIN
res/drawable/ic_tab_playlists_selected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 814 B |
BIN
res/drawable/ic_tab_playlists_unselected.png
Normal file
BIN
res/drawable/ic_tab_playlists_unselected.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 B |
9
res/drawable/tab_playlists.xml
Normal file
9
res/drawable/tab_playlists.xml
Normal 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>
|
@ -57,6 +57,12 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:divider="@null"
|
||||
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>
|
||||
</LinearLayout>
|
||||
</TabHost>
|
||||
|
@ -50,6 +50,7 @@
|
||||
<string name="artists">Artists</string>
|
||||
<string name="albums">Albums</string>
|
||||
<string name="songs">Songs</string>
|
||||
<string name="playlists">Playlists</string>
|
||||
|
||||
<string name="none">None</string>
|
||||
|
||||
|
@ -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 <code>type</code> 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
|
||||
* <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()
|
||||
{
|
||||
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;
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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)
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user