Allow songs and playlists to be deleted.

This commit is contained in:
Christopher Eby 2010-05-22 11:39:14 -05:00
parent cc33825cfb
commit 5d4dd5e0a6
8 changed files with 211 additions and 23 deletions

View File

@ -70,5 +70,6 @@
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="8" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<supports-screens android:smallScreens="true" />
</manifest>

View File

@ -49,6 +49,7 @@
<string name="add_to_playlist">Add to Playlist...</string>
<string name="new_playlist">New Playlist...</string>
<string name="expand">Expand</string>
<string name="delete">Delete</string>
<string name="playback_view">Now Playing</string>
<string name="search">Search</string>
@ -58,6 +59,12 @@
<item quantity="one">1 song added to playlist %2$s.</item>
<item quantity="other">%d songs added to playlist %s.</item>
</plurals>
<string name="deleting">Deleting...</string>
<string name="playlist_deleted">Playlist %s deleted.</string>
<plurals name="deleted">
<item quantity="one">1 song deleted.</item>
<item quantity="other">%d songs deleted.</item>
</plurals>
<string name="artists">Artists</string>
<string name="albums">Albums</string>

View File

@ -18,6 +18,8 @@
package org.kreed.vanilla;
import java.io.File;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
@ -45,28 +47,31 @@ public class MediaUtils {
* Return a cursor containing the ids of all the songs with artist or
* album of the specified id.
*
* @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
* @param type One of the TYPE_* constants, excluding playlists.
* @param id The MediaStore id of the artist or album.
* @param projection The columns to query.
*/
private static Cursor getMediaCursor(int type, long id)
private static Cursor getMediaCursor(int type, long id, String[] projection)
{
String selection = "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0";
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String selection;
switch (type) {
case TYPE_SONG:
selection = MediaStore.Audio.Media._ID;
break;
case TYPE_ARTIST:
selection = MediaStore.Audio.Media.ARTIST_ID + selection;
selection = MediaStore.Audio.Media.ARTIST_ID;
break;
case TYPE_ALBUM:
selection = MediaStore.Audio.Media.ALBUM_ID + selection;
selection = MediaStore.Audio.Media.ALBUM_ID;
break;
default:
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 };
selection += "=" + id + " AND " + MediaStore.Audio.Media.IS_MUSIC + "!=0";
String sort = MediaStore.Audio.Media.ARTIST_KEY + ',' + MediaStore.Audio.Media.ALBUM_KEY + ',' + MediaStore.Audio.Media.TRACK;
return resolver.query(media, projection, selection, null, sort);
}
@ -76,12 +81,12 @@ public class MediaUtils {
* with the given id.
*
* @param id The id of the playlist in MediaStore.Audio.Playlists.
* @param projection The columns to query.
*/
private static Cursor getPlaylistCursor(long id)
private static Cursor getPlaylistCursor(long id, String[] projection)
{
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id);
String[] projection = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
return resolver.query(uri, projection, null, null, sort);
}
@ -96,6 +101,7 @@ public class MediaUtils {
*/
public static long[] getAllSongIdsWith(int type, long id)
{
String[] projection = { MediaStore.Audio.Media._ID };
Cursor cursor;
switch (type) {
@ -103,10 +109,10 @@ public class MediaUtils {
return new long[] { id };
case TYPE_ARTIST:
case TYPE_ALBUM:
cursor = getMediaCursor(type, id);
cursor = getMediaCursor(type, id, projection);
break;
case TYPE_PLAYLIST:
cursor = getPlaylistCursor(id);
cursor = getPlaylistCursor(id, projection);
break;
default:
throw new IllegalArgumentException("Specified type not valid: " + type);
@ -129,4 +135,39 @@ public class MediaUtils {
cursor.close();
return songs;
}
/**
* Delete all the songs in the given media set.
*
* @param type One of the TYPE_* constants, excluding playlists.
* @param id The MediaStore id of the media to delete.
* @return The number of songs deleted.
*/
public static int deleteMedia(int type, long id)
{
int count = 0;
ContentResolver resolver = ContextApplication.getContext().getContentResolver();
String[] projection = new String [] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA };
Cursor cursor = getMediaCursor(type, id, projection);
if (cursor != null) {
PlaybackService service = ContextApplication.hasService() ? ContextApplication.getService() : null;
while (cursor.moveToNext()) {
if (new File(cursor.getString(1)).delete()) {
long songId = cursor.getLong(0);
String where = MediaStore.Audio.Media._ID + '=' + songId;
resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where, null);
if (service != null)
service.removeSong(songId);
++count;
}
}
cursor.close();
}
return count;
}
}

View File

@ -570,6 +570,8 @@ public final class PlaybackService extends Service implements Handler.Callback,
/**
* Returns the song <code>delta</code> places away from the current
* position.
*
* @see SongTimeline#getSong(int)
*/
public Song getSong(int delta)
{
@ -835,6 +837,8 @@ public final class PlaybackService extends Service implements Handler.Callback,
/**
* Returns the position of the current song in the song timeline.
*
* @see SongTimeline#getCurrentPosition()
*/
public int getTimelinePos()
{
@ -869,4 +873,18 @@ public final class PlaybackService extends Service implements Handler.Callback,
{
broadcastReplaceSong(delta, song);
}
/**
* Remove the song with the given id from the timeline and advance to the
* next song if the given song is currently playing.
*
* @param id The MediaStore id of the song to remove.
* @see SongTimeline#removeSong(long)
*/
public void removeSong(long id)
{
boolean shouldAdvance = mTimeline.removeSong(id);
if (shouldAdvance)
setCurrentSong(0);
}
}

View File

@ -19,6 +19,7 @@
package org.kreed.vanilla;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
@ -159,4 +160,15 @@ public class Playlist {
}
resolver.bulkInsert(uri, values);
}
/**
* Delete the playlist with the given id.
*
* @param id The Media.Audio.Playlists id of the playlist.
*/
public static void deletePlaylist(long id)
{
Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id);
ContextApplication.getContext().getContentResolver().delete(uri, null, null);
}
}

View File

@ -179,6 +179,19 @@ public class Song implements Parcelable {
}
}
/**
* Get the id of the given song.
*
* @param song The Song to get the id from.
* @return The id, or 0 if the given song is null.
*/
public static long getId(Song song)
{
if (song == null)
return 0;
return song.id;
}
public static Parcelable.Creator<Song> CREATOR = new Parcelable.Creator<Song>() {
public Song createFromParcel(Parcel in)
{

View File

@ -329,6 +329,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
private static final int MENU_EXPAND = 2;
private static final int MENU_ADD_TO_PLAYLIST = 3;
private static final int MENU_NEW_PLAYLIST = 4;
private static final int MENU_DELETE = 5;
@Override
public void onCreateContextMenu(ContextMenu menu, View listView, ContextMenu.ContextMenuInfo absInfo)
@ -343,11 +344,14 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
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);
menu.add(0, MENU_DELETE, 0, R.string.delete);
playlistMenu.add(type, MENU_NEW_PLAYLIST, id, R.string.new_playlist);
Playlist[] playlists = Playlist.getPlaylists();
for (int i = 0; i != playlists.length; ++i)
playlistMenu.add(type, (int)playlists[i].id + 100, id, playlists[i].name);
if (playlists != null) {
for (int i = 0; i != playlists.length; ++i)
playlistMenu.add(type, (int)playlists[i].id + 100, id, playlists[i].name);
}
}
/**
@ -371,21 +375,41 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
/**
* Delete the media with the specified type and id and show a Toast
* informing the user of this.
*
* @param type The type of media; one of the MediaUtils.TYPE_* constants.
* @param id The MediaStore id of the media.
* @param title The title of the playlist, to be displayed in the Toast.
* Only used when deleting a playlist.
*/
private void delete(int type, long id, String title)
{
if (type == MediaUtils.TYPE_PLAYLIST) {
Playlist.deletePlaylist(id);
String message = getResources().getString(R.string.playlist_deleted, title);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
} else {
int count = MediaUtils.deleteMedia(type, id);
String message = getResources().getQuantityString(R.plurals.deleted, count, count);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
@Override
public boolean onContextItemSelected(MenuItem item)
{
String action = PlaybackService.ACTION_PLAY_ITEMS;
int id = item.getItemId();
final int type = item.getGroupId();
final int mediaId = item.getOrder();
int type = item.getGroupId();
int mediaId = item.getOrder();
switch (id) {
case MENU_EXPAND:
expand((MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView);
break;
case MENU_ADD_TO_PLAYLIST:
break;
case MENU_ENQUEUE:
action = PlaybackService.ACTION_ENQUEUE_ITEMS;
// fall through
@ -394,13 +418,24 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
mDefaultAction = action;
sendSongIntent((MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView, action);
break;
case MENU_NEW_PLAYLIST:
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;
}
case MENU_DELETE: {
MediaAdapter.MediaView view = (MediaAdapter.MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView;
type = view.getMediaType();
if (type != MediaUtils.TYPE_PLAYLIST)
Toast.makeText(this, R.string.deleting, Toast.LENGTH_SHORT).show();
Message message = mHandler.obtainMessage(MSG_DELETE, type, (int)view.getMediaId());
message.obj = view.getTitle();
mHandler.sendMessage(message);
break;
}
default:
if (id > 100)
addToPlaylist(id - 100, type, mediaId, item.getTitle());
@ -461,12 +496,20 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
*/
private static final int MSG_INIT = 10;
/**
* Call addToPlaylist with the paramaters from the given message. The
* Call addToPlaylist with the parameters 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;
/**
* Delete the songs in the set of media with the specified type and id,
* given as arg1 and arg2, respectively. If type is a playlist, the
* playlist itself will be deleted, not the songs it contains. The obj
* field should contain the playlist name (as a String) if type is a
* playlist.
*/
private static final int MSG_DELETE = 12;
@Override
public boolean handleMessage(Message message)
@ -485,6 +528,9 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem
addToPlaylist(playlistId, message.arg1, message.arg2, name);
}
break;
case MSG_DELETE:
delete(message.arg1, message.arg2, (String)message.obj);
break;
default:
return super.handleMessage(message);
}

View File

@ -428,4 +428,54 @@ public final class SongTimeline {
mQueueOffset = 0;
}
}
/**
* Remove the song with the given id from the timeline.
*
* @param id The MediaStore id of the song to remove.
* @return True if the current song has changed.
*/
public boolean removeSong(long id)
{
synchronized (this) {
boolean changed = false;
ArrayList<Song> songs = mSongs;
int i = mCurrentPos;
Song oldPrevious = getSong(-1);
Song oldCurrent = getSong(0);
Song oldNext = getSong(+1);
while (--i != -1) {
if (Song.getId(songs.get(i)) == id) {
songs.remove(i);
--mCurrentPos;
}
}
for (i = mCurrentPos; i != songs.size(); ++i) {
if (Song.getId(songs.get(i)) == id)
songs.remove(i);
}
i = mCurrentPos;
Song previous = getSong(-1);
Song current = getSong(0);
Song next = getSong(+1);
if (mCallback != null) {
if (Song.getId(oldPrevious) != Song.getId(previous))
mCallback.songReplaced(-1, previous);
if (Song.getId(oldNext) != Song.getId(next))
mCallback.songReplaced(1, next);
}
if (Song.getId(oldCurrent) != Song.getId(current)) {
if (mCallback != null)
mCallback.songReplaced(0, current);
changed = true;
}
return changed;
}
}
}