Implement the ability to add media to playlists.

This commit is contained in:
Christopher Eby 2010-05-18 14:05:27 -05:00
parent 0e7bfd8ad3
commit 7c082f4856
6 changed files with 422 additions and 42 deletions

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
This file is part of Vanilla Music Player.
Vanilla Music Player is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
Vanilla Music Player is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/playlist_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_marginBottom="8dip"
android:layout_marginLeft="8dip"
android:layout_marginRight="8dip">
<requestFocus />
</EditText>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="6dip"
android:background="#fff" >
<Button android:id="@+id/create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/create"
android:singleLine="true" />
<Button android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"
android:singleLine="true" />
</LinearLayout>
</LinearLayout>

View File

@ -37,15 +37,27 @@
<!-- Widgets -->
<string name="starting">Starting up...</string>
<!-- New Playlist Dialog -->
<string name="choose_playlist_name">Choose Playlist Name</string>
<string name="create">Create</string>
<string name="overwrite">Overwrite</string>
<string name="cancel">Cancel</string>
<!-- Song Chooser -->
<string name="enqueue">Enqueue</string>
<string name="play">Play</string>
<string name="add_to_playlist">Add to Playlist...</string>
<string name="new_playlist">New Playlist...</string>
<string name="expand">Expand</string>
<string name="playback_view">Now Playing</string>
<string name="search">Search</string>
<string name="enqueued">Enqueued %s</string>
<string name="playing">Playing %s</string>
<plurals name="added_to_playlist">
<item quantity="one">1 song added to playlist %2$s.</item>
<item quantity="other">%d songs added to playlist %s.</item>
</plurals>
<string name="artists">Artists</string>
<string name="albums">Albums</string>

View File

@ -176,7 +176,7 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
public void filter(CharSequence constraint, Filter.FilterListener listener)
{
mConstraint = constraint;
getFilter().filter(constraint, listener);
super.getFilter().filter(constraint, listener);
}
/**
@ -292,14 +292,6 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
return resolver.query(mStore, projection, selection.toString(), selectionArgs, getSortOrder());
}
/**
* Returns true if views has expander arrows displayed.
*/
public final boolean hasExpanders()
{
return mExpandable;
}
/**
* Set the limiter for the adapter. A limiter is intended to restrict
* displayed media to only those that are children of a given parent
@ -315,7 +307,7 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
{
mLimiter = limiter;
if (async)
getFilter().filter(mConstraint);
super.getFilter().filter(mConstraint);
else
requery();
}
@ -543,6 +535,14 @@ public class MediaAdapter extends CursorAdapter implements FilterQueryProvider {
return mExpanderPressed;
}
/**
* Returns true if views has expander arrows displayed.
*/
public final boolean hasExpanders()
{
return mExpandable;
}
/**
* Update the fields in this view with the data from the given Cursor.
*

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* This file is part of Vanilla Music Player.
*
* Vanilla Music Player is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Vanilla Music Player is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kreed.vanilla;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
/**
* Simple dialog to prompt to user to enter a playlist name. Has an EditText to
* enter the name and two buttons, create and cancel. Create changes to
* overwrite if a name that already exists is selected.
*/
public class NewPlaylistDialog extends Dialog implements TextWatcher, View.OnClickListener {
/**
* The create/overwrite button.
*/
private Button mPositiveButton;
/**
* The text entry view.
*/
private EditText mText;
/**
* Whether the dialog has been accepted. The dialog is accepted if create
* was clicked.
*/
private boolean mAccepted;
public NewPlaylistDialog(Context context)
{
super(context);
}
@Override
protected void onCreate(Bundle state)
{
super.onCreate(state);
setContentView(R.layout.new_playlist_dialog);
setTitle(R.string.choose_playlist_name);
mText = (EditText)findViewById(R.id.playlist_name);
mText.addTextChangedListener(this);
mPositiveButton = (Button)findViewById(R.id.create);
View negativeButton = findViewById(R.id.cancel);
mPositiveButton.setOnClickListener(this);
negativeButton.setOnClickListener(this);
}
/**
* Returns the playlist name currently entered in the dialog.
*/
public String getText()
{
return mText.getText().toString();
}
public void afterTextChanged(Editable s)
{
// do nothing
}
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
// do nothing
}
public void onTextChanged(CharSequence s, int start, int before, int count)
{
// Update the action button based on whether there is an
// existing playlist with the given name.
int res = Song.getPlaylist(s.toString()) == -1 ? R.string.create : R.string.overwrite;
mPositiveButton.setText(res);
}
/**
* Returns whether the dialog has been accepted. The dialog is accepted
* when the create/overwrite button is clicked.
*/
public boolean isAccepted()
{
return mAccepted;
}
public void onClick(View view)
{
switch (view.getId()) {
case R.id.create:
mAccepted = true;
// fall through
case R.id.cancel:
dismiss();
break;
}
}
}

View File

@ -19,6 +19,7 @@
package org.kreed.vanilla;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
@ -200,14 +201,13 @@ public class Song implements Parcelable {
* Return a cursor containing the ids of all the songs with artist or
* album of the specified id.
*
* @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
*/
private static Cursor getMediaCursor(ContentResolver resolver, int type, long id)
private static Cursor getMediaCursor(int type, 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 TYPE_ARTIST:
@ -220,7 +220,7 @@ public class Song implements Parcelable {
throw new IllegalArgumentException("Invalid type specified: " + type);
}
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID };
String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK;
@ -231,15 +231,151 @@ public class Song implements Parcelable {
* 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)
private static Cursor getPlaylistCursor(long id)
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
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);
String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
return resolver.query(uri, projection, null, null, sort);
}
/**
* Class simply containing metadata about a playlist.
*/
public static class Playlist {
/**
* Create a Playlist with the given id and name.
*/
public Playlist(long id, String name)
{
this.id = id;
this.name = name;
}
/**
* The MediaStore.Audio.Playlists id of the playlist.
*/
public long id;
/**
* The name of the playlist.
*/
public String name;
}
/**
* Queries all the playlists known to the MediaStore.
*
* @return An array of Playlists
* @see Playlist
*/
public static Playlist[] getPlaylists()
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Uri media = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME };
String sort = MediaStore.Audio.Playlists.NAME;
Cursor cursor = resolver.query(media, projection, null, null, sort);
if (cursor == null)
return null;
int count = cursor.getCount();
if (count == 0)
return null;
Playlist[] playlists = new Playlist[count];
for (int i = 0; i != count; ++i) {
if (!cursor.moveToNext())
return null;
playlists[i] = new Playlist(cursor.getLong(0), cursor.getString(1));
}
cursor.close();
return playlists;
}
/**
* Retrieves the id for a playlist with the given name.
*
* @param name The name of the playlist.
* @return The id of the playlist, or -1 if there is no playlist with the
* given name.
*/
public static long getPlaylist(String name)
{
long id = -1;
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Cursor cursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Audio.Playlists._ID },
MediaStore.Audio.Playlists.NAME + "=?",
new String[] { name }, null);
if (cursor != null) {
if (cursor.moveToNext())
id = cursor.getLong(0);
cursor.close();
}
return id;
}
/**
* Create a new playlist with the given name. If a playlist with the given
* name already exists, it will be overwritten.
*
* @param name The name of the playlist.
* @return The id of the new playlist.
*/
public static long createPlaylist(String name)
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
long id = getPlaylist(name);
if (id == -1) {
// We need to create a new playlist.
ContentValues values = new ContentValues(1);
values.put(MediaStore.Audio.Playlists.NAME, name);
Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values);
id = Long.parseLong(uri.getLastPathSegment());
} else {
// We are overwriting an existing playlist. Clear existing songs.
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
resolver.delete(uri, null, null);
}
return id;
}
/**
* Add the given set of song ids to the playlist with the given id.
*
* @param playlistId The MediaStore.Audio.Playlist id of the playlist to
* modify.
* @param toAdd The MediaStore ids of all the songs to be added to the
* playlist.
*/
public static void addToPlaylist(long playlistId, long[] toAdd)
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
String[] projection = new String[] { MediaStore.Audio.Playlists.Members.PLAY_ORDER };
Cursor cursor = resolver.query(uri, projection, null, null, null);
int base = 0;
if (cursor.moveToLast())
base = cursor.getInt(0) + 1;
cursor.close();
ContentValues[] values = new ContentValues[toAdd.length];
for (int i = 0; i != values.length; ++i) {
values[i] = new ContentValues(1);
values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, toAdd[i]);
}
resolver.bulkInsert(uri, values);
}
/**
@ -252,7 +388,6 @@ public class Song implements Parcelable {
*/
public static long[] getAllSongIdsWith(int type, long id)
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Cursor cursor;
switch (type) {
@ -260,10 +395,10 @@ public class Song implements Parcelable {
return new long[] { id };
case TYPE_ARTIST:
case TYPE_ALBUM:
cursor = getMediaCursor(resolver, type, id);
cursor = getMediaCursor(type, id);
break;
case TYPE_PLAYLIST:
cursor = getPlaylistCursor(resolver, id);
cursor = getPlaylistCursor(id);
break;
default:
throw new IllegalArgumentException("Specified type not valid: " + type);

View File

@ -40,6 +40,7 @@ import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
@ -102,6 +103,11 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
mLimiterViews = (ViewGroup)findViewById(R.id.limiter_layout);
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));
mHandler.sendEmptyMessage(MSG_INIT);
}
@ -201,7 +207,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
}
/**
* Tell the PlaybackService that we are finished enqueuing songs.
* Tell the PlaybackService that we are finished enqueueing songs.
*/
private void sendFinishEnqueueing()
{
@ -321,25 +327,64 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
private static final int MENU_PLAY = 0;
private static final int MENU_ENQUEUE = 1;
private static final int MENU_EXPAND = 2;
private static final int MENU_ADD_TO_PLAYLIST = 3;
private static final int MENU_NEW_PLAYLIST = 4;
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo absInfo)
public void onCreateContextMenu(ContextMenu menu, View listView, ContextMenu.ContextMenuInfo absInfo)
{
menu.setHeaderTitle(((MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)absInfo).targetView).getTitle());
MediaAdapter.MediaView view = (MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)absInfo).targetView;
int type = view.getMediaType();
int id = (int)view.getMediaId();
menu.setHeaderTitle(view.getTitle());
menu.add(0, MENU_PLAY, 0, R.string.play);
menu.add(0, MENU_ENQUEUE, 0, R.string.enqueue);
if (((MediaAdapter)((ListView)view).getAdapter()).hasExpanders())
SubMenu playlistMenu = menu.addSubMenu(0, MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
if (view.hasExpanders())
menu.add(0, MENU_EXPAND, 0, R.string.expand);
playlistMenu.add(type, MENU_NEW_PLAYLIST, id, R.string.new_playlist);
Song.Playlist[] playlists = Song.getPlaylists();
for (int i = 0; i != playlists.length; ++i)
playlistMenu.add(type, (int)playlists[i].id + 100, id, playlists[i].name);
}
/**
* Add a set of songs to a playlists. Sets can be all songs from an artist,
* album, playlist, or a single song. Displays a Toast notifying of
* success.
*
* @param playlistId The MediaStore.Audio.Playlists id of the playlist to
* be modified.
* @param type The type of media the mediaId represents; one of the
* Song.TYPE_* constants.
* @param mediaId The MediaStore id of the element to be added.
* @param title The title of the playlist being added to (used for the
* Toast).
*/
private void addToPlaylist(long playlistId, int type, long mediaId, CharSequence title)
{
long[] ids = Song.getAllSongIdsWith(type, mediaId);
Song.addToPlaylist(playlistId, ids);
String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
public boolean onContextItemSelected(MenuItem item)
{
MediaAdapter.MediaView view = (MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView;
String action = PlaybackService.ACTION_PLAY_ITEMS;
switch (item.getItemId()) {
int id = item.getItemId();
final int type = item.getGroupId();
final int mediaId = item.getOrder();
switch (id) {
case MENU_EXPAND:
expand(view);
expand((MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView);
break;
case MENU_ADD_TO_PLAYLIST:
break;
case MENU_ENQUEUE:
action = PlaybackService.ACTION_ENQUEUE_ITEMS;
@ -347,15 +392,24 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
case MENU_PLAY:
if (mDefaultIsLastAction)
mDefaultAction = action;
sendSongIntent(view, action);
sendSongIntent((MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView, action);
break;
case MENU_NEW_PLAYLIST:
NewPlaylistDialog dialog = new NewPlaylistDialog(this);
Message message = mHandler.obtainMessage(MSG_NEW_PLAYLIST, type, mediaId);
message.obj = dialog;
dialog.setDismissMessage(message);
dialog.show();
break;
default:
if (id > 100)
addToPlaylist(id - 100, type, mediaId, item.getTitle());
return false;
}
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
@ -398,32 +452,41 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
final ListView view = (ListView)findViewById(id);
view.setOnItemClickListener(this);
view.setOnCreateContextMenuListener(this);
runOnUiThread(new Runnable() {
public void run()
{
view.setAdapter(adapter);
}
});
view.setAdapter(adapter);
}
/**
* Perform the initialization that may be done in the background outside
* of onCreate.
*/
private static final int MSG_INIT = 10;
/**
* Call addToPlaylist with the paramaters from the given message. The
* message must contain the type and id of the media to be added in
* arg1 and arg2, respectively. The obj field must be a NewPlaylistDialog
* that the name will be taken from.
*/
private static final int MSG_NEW_PLAYLIST = 11;
@Override
public boolean handleMessage(Message message)
{
switch (message.what) {
case MSG_INIT:
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);
resolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, observer);
break;
case MSG_NEW_PLAYLIST:
NewPlaylistDialog dialog = (NewPlaylistDialog)message.obj;
if (dialog.isAccepted()) {
String name = dialog.getText();
long playlistId = Song.createPlaylist(name);
addToPlaylist(playlistId, message.arg1, message.arg2, name);
}
break;
default:
return false;
return super.handleMessage(message);
}
return true;