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; + } + } }