diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a1fc1f6b..fff43ff0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -70,5 +70,6 @@
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 40a90e96..e62d6b80 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -49,6 +49,7 @@
Add to Playlist...
New Playlist...
Expand
+ Delete
Now Playing
Search
@@ -58,6 +59,12 @@
- 1 song added to playlist %2$s.
- %d songs added to playlist %s.
+ Deleting...
+ Playlist %s deleted.
+
+ - 1 song deleted.
+ - %d songs deleted.
+
Artists
Albums
diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java
index 9a26ca76..4bd8dd06 100644
--- a/src/org/kreed/vanilla/MediaUtils.java
+++ b/src/org/kreed/vanilla/MediaUtils.java
@@ -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;
+ }
}
diff --git a/src/org/kreed/vanilla/PlaybackService.java b/src/org/kreed/vanilla/PlaybackService.java
index d63d15c6..b92f37c8 100644
--- a/src/org/kreed/vanilla/PlaybackService.java
+++ b/src/org/kreed/vanilla/PlaybackService.java
@@ -570,6 +570,8 @@ public final class PlaybackService extends Service implements Handler.Callback,
/**
* Returns the song delta
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);
+ }
}
diff --git a/src/org/kreed/vanilla/Playlist.java b/src/org/kreed/vanilla/Playlist.java
index d804168b..4fa2ae76 100644
--- a/src/org/kreed/vanilla/Playlist.java
+++ b/src/org/kreed/vanilla/Playlist.java
@@ -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);
+ }
}
diff --git a/src/org/kreed/vanilla/Song.java b/src/org/kreed/vanilla/Song.java
index 74032f4d..69f51d0d 100644
--- a/src/org/kreed/vanilla/Song.java
+++ b/src/org/kreed/vanilla/Song.java
@@ -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 CREATOR = new Parcelable.Creator() {
public Song createFromParcel(Parcel in)
{
diff --git a/src/org/kreed/vanilla/SongSelector.java b/src/org/kreed/vanilla/SongSelector.java
index a19ad48d..5fd0be8b 100644
--- a/src/org/kreed/vanilla/SongSelector.java
+++ b/src/org/kreed/vanilla/SongSelector.java
@@ -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);
}
diff --git a/src/org/kreed/vanilla/SongTimeline.java b/src/org/kreed/vanilla/SongTimeline.java
index 713fca7d..b7101a1c 100644
--- a/src/org/kreed/vanilla/SongTimeline.java
+++ b/src/org/kreed/vanilla/SongTimeline.java
@@ -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 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;
+ }
+ }
}