From f524be719905179e4f290ae6953475cad3657825 Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Wed, 21 Sep 2011 16:32:34 -0500 Subject: [PATCH] Add a play all row --- res/values/strings.xml | 3 + src/org/kreed/vanilla/MediaAdapter.java | 135 ++++++-- src/org/kreed/vanilla/MediaUtils.java | 35 -- src/org/kreed/vanilla/MediaView.java | 63 ++-- src/org/kreed/vanilla/NewPlaylistDialog.java | 17 +- src/org/kreed/vanilla/Playlist.java | 38 +- src/org/kreed/vanilla/QueryTask.java | 20 ++ src/org/kreed/vanilla/SongSelector.java | 347 +++++++++++-------- 8 files changed, 413 insertions(+), 245 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 4f2792c7..0fbd30ec 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -93,6 +93,9 @@ THE SOFTWARE. None Unknown %1$s by %2$s + Play All + Enqueue All + All Songs diff --git a/src/org/kreed/vanilla/MediaAdapter.java b/src/org/kreed/vanilla/MediaAdapter.java index 1fa4cc44..540d9e61 100644 --- a/src/org/kreed/vanilla/MediaAdapter.java +++ b/src/org/kreed/vanilla/MediaAdapter.java @@ -22,7 +22,6 @@ package org.kreed.vanilla; -import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; @@ -90,6 +89,15 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { * The section indexer, for the letter pop-up when scrolling. */ private MusicAlphabetIndexer mIndexer; + /** + * True if this adapter should have a special MediaView with custom text in + * the first row. + */ + private boolean mHasHeader; + /** + * The text to show in the header. + */ + private String mHeaderText; /** * Construct a MediaAdapter representing the given type of @@ -101,15 +109,17 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { * 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 hasHeader Wether this view has a header row. * @param limiter An initial limiter to use */ - public MediaAdapter(SongSelector activity, int type, boolean expandable, Limiter limiter) + public MediaAdapter(SongSelector activity, int type, boolean expandable, boolean hasHeader, Limiter limiter) { super(activity, null, false); mActivity = activity; mType = type; mExpandable = expandable; + mHasHeader = hasHeader; mLimiter = limiter; mIndexer = new MusicAlphabetIndexer(1); @@ -145,6 +155,75 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { } } + @Override + public int getCount() + { + int count = super.getCount(); + if (count == 0) + return 0; + else if (mHasHeader) + return count + 1; + else + return count; + } + + @Override + public Object getItem(int pos) + { + if (mHasHeader) { + if (pos == 0) + return null; + else + pos -= 1; + } + + return super.getItem(pos); + } + + @Override + public long getItemId(int pos) + { + if (mHasHeader) { + if (pos == 0) + return -1; + else + pos -= 1; + } + + return super.getItemId(pos); + } + + @Override + public View getView(int pos, View convertView, ViewGroup parent) + { + if (mHasHeader) { + if (pos == 0) { + MediaView view; + if (convertView == null) + view = new MediaView(mActivity, mExpandable); + else + view = (MediaView)convertView; + view.makeHeader(mHeaderText); + return view; + } else { + pos -= 1; + } + } + + return super.getView(pos, convertView, parent); + } + + /** + * Modify the header text to be shown in the first row. + * + * @param text The new text. + */ + public void setHeaderText(String text) + { + mHeaderText = text; + notifyDataSetChanged(); + } + /** * Perform filtering on a background thread. * @@ -159,14 +238,13 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { } /** - * Query the backing content provider. Should be called on a background - * thread. + * Build the query to be run with runQuery(). + * + * @param forceMusicCheck Force the is_music check to be added to the + * selection. */ - public void runQuery() + public QueryTask buildQuery(boolean forceMusicCheck) { - ContentResolver resolver = mActivity.getContentResolver(); - Cursor cursor; - String constraint = mConstraint; Limiter limiter = mLimiter; @@ -187,7 +265,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { else sort = mFieldKeys[mFieldKeys.length - 1]; - if (mType == MediaUtils.TYPE_SONG) + if (mType == MediaUtils.TYPE_SONG || forceMusicCheck) selection.append("is_music!=0"); if (constraint != null && constraint.length() != 0) { @@ -228,8 +306,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { if (limiter != null && limiter.type == MediaUtils.TYPE_GENRE) { // Genre is not standard metadata for MediaStore.Audio.Media. // We have to query it through a separate provider. : / - QueryTask query = MediaUtils.buildGenreQuery(mLimiter.id, projection, selection.toString(), selectionArgs); - cursor = query.runQuery(resolver); + return MediaUtils.buildGenreQuery(mLimiter.id, projection, selection.toString(), selectionArgs); } else { if (limiter != null) { if (selection.length() != 0) @@ -237,10 +314,8 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { selection.append(mLimiter.selection); } - cursor = resolver.query(mStore, projection, selection.toString(), selectionArgs, sort); + return new QueryTask(mStore, projection, selection.toString(), selectionArgs, sort); } - - mActivity.changeCursor(this, cursor); } /** @@ -257,7 +332,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { * displayed media to only those that are children of a given parent * media item. * - * @param limiter The limiter, created by MediaView.getLimiter() + * @param limiter The limiter, created by {@link MediaAdapter#getLimiter(long)}. */ public final void setLimiter(Limiter limiter) { @@ -277,31 +352,39 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { /** * Builds a limiter based off of the media represented by the given row. * - * @param view The row to create the limiter from. + * @param id The id of the row. * @see MediaAdapter#getLimiter() * @see MediaAdapter#setLimiter(MediaAdapter.Limiter) */ - public Limiter getLimiter(MediaView view) + public Limiter getLimiter(long id) { - long id = view.getMediaId(); String[] fields; String selection = null; + Cursor cursor = getCursor(); + if (cursor == null) + return null; + for (int i = 0, count = cursor.getCount(); i != count; ++i) { + cursor.moveToPosition(i); + if (cursor.getLong(0) == id) + break; + } + switch (mType) { case MediaUtils.TYPE_ARTIST: { - fields = new String[] { view.getTitle() }; + fields = new String[] { cursor.getString(1) }; String field = MediaStore.Audio.Media.ARTIST_ID; selection = String.format("%s=%d", field, id); break; } case MediaUtils.TYPE_ALBUM: { - fields = new String[] { view.getSubTitle(), view.getTitle() }; + fields = new String[] { cursor.getString(2), cursor.getString(1) }; String field = MediaStore.Audio.Media.ALBUM_ID; selection = String.format("%s=%d", field, id); break; } case MediaUtils.TYPE_GENRE: - fields = new String[] { view.getTitle() }; + fields = new String[] { cursor.getString(1) }; break; default: throw new IllegalStateException("getLimiter() is not supported for media type: " + mType); @@ -326,7 +409,13 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { @Override public int getPositionForSection(int section) { - return mIndexer.getPositionForSection(section); + int offset = 0; + if (mHasHeader) { + if (section == 0) + return 0; + offset = 1; + } + return offset + mIndexer.getPositionForSection(section); } @Override @@ -351,7 +440,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return new MediaView(context, this, mExpandable); + return new MediaView(context, mExpandable); } /** diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java index 8155a2e5..babe3c23 100644 --- a/src/org/kreed/vanilla/MediaUtils.java +++ b/src/org/kreed/vanilla/MediaUtils.java @@ -187,41 +187,6 @@ public class MediaUtils { } } - /** - * Return an array containing all the song ids that match the specified parameters. Should be run on a background thread. - * - * @param context A context to use. - * @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(Context context, int type, long id) - { - if (type == TYPE_SONG) - return new long[] { id }; - - String[] projection = type == MediaUtils.TYPE_PLAYLIST ? - Song.EMPTY_PLAYLIST_PROJECTION : Song.EMPTY_PROJECTION; - Cursor cursor = buildQuery(type, id, projection, null).runQuery(context.getContentResolver()); - if (cursor == null) - return null; - - int count = cursor.getCount(); - if (count == 0) - return null; - - long[] songs = new long[count]; - for (int i = 0; i != count; ++i) { - if (!cursor.moveToNext()) - return null; - songs[i] = cursor.getLong(0); - } - - cursor.close(); - return songs; - } - /** * Delete all the songs in the given media set. Should be run on a * background thread. diff --git a/src/org/kreed/vanilla/MediaView.java b/src/org/kreed/vanilla/MediaView.java index 708eea90..3b87e80f 100644 --- a/src/org/kreed/vanilla/MediaView.java +++ b/src/org/kreed/vanilla/MediaView.java @@ -76,10 +76,6 @@ public final class MediaView extends View { sPaint.setAntiAlias(true); } - /** - * The MediaAdapter that owns this view. - */ - private MediaAdapter mOwner; /** * The MediaStore id of the media represented by this view. */ @@ -104,19 +100,21 @@ public final class MediaView extends View { * The cached measured view height. */ private int mViewHeight; + /** + * True if this view is a header. Will override expandable setting to false. + */ + private boolean mIsHeader; /** * Construct a MediaView. * * @param context A Context to use. - * @param owner The MediaAdapter that owns this view. * @param expandable True if the expander should be shown. */ - public MediaView(Context context, MediaAdapter owner, boolean expandable) + public MediaView(Context context, boolean expandable) { super(context); - mOwner = owner; mExpandable = expandable; mViewHeight = (int)(7 * sTextSize / 2); @@ -148,7 +146,7 @@ public final class MediaView extends View { Paint paint = sPaint; - if (mExpandable) { + if (mExpandable && !mIsHeader) { Bitmap expander = sExpander; width -= padding * 4 + expander.getWidth(); @@ -188,24 +186,6 @@ public final class MediaView extends View { paint.setShader(null); } - /** - * Returns the MediaAdapter that owns this view. - */ - public MediaAdapter getOwner() - { - return mOwner; - } - - /** - * Returns the type of media this view represents. - * - * @return One of MediaUtils.TYPE_*. - */ - public int getMediaType() - { - return mOwner.getMediaType(); - } - /** * Returns the MediaStore id of the media represented by this view. */ @@ -222,14 +202,6 @@ public final class MediaView extends View { return mTitle; } - /** - * Returns the secondary text in the view, displayed on the lower line. - */ - public String getSubTitle() - { - return mSubTitle; - } - /** * Returns true if the expander arrow was pressed in the last touch * event. @@ -247,6 +219,18 @@ public final class MediaView extends View { return mExpandable; } + /** + * Set this view to be a header (custom text, never expandable). + * + * @param text The text to show. + */ + public void makeHeader(String text) + { + mIsHeader = true; + mTitle = text; + mSubTitle = null; + } + /** * Update the fields in this view with the data from the given Cursor. * @@ -258,6 +242,7 @@ public final class MediaView extends View { */ public void updateMedia(Cursor cursor, boolean useSecondary) { + mIsHeader = false; mId = cursor.getLong(0); mTitle = cursor.getString(1); if (useSecondary) @@ -265,13 +250,21 @@ public final class MediaView extends View { invalidate(); } + /** + * Returns true if the view is set to be a "Play/Enqueue All" header. + */ + public boolean isHeader() + { + return mIsHeader; + } + /** * Update mExpanderPressed. */ @Override public boolean onTouchEvent(MotionEvent event) { - if (mExpandable) + if (mExpandable && !mIsHeader) mExpanderPressed = event.getX() > getWidth() - sExpander.getWidth() - 2 * sTextSize; return false; } diff --git a/src/org/kreed/vanilla/NewPlaylistDialog.java b/src/org/kreed/vanilla/NewPlaylistDialog.java index 0087c12d..476f0bb8 100644 --- a/src/org/kreed/vanilla/NewPlaylistDialog.java +++ b/src/org/kreed/vanilla/NewPlaylistDialog.java @@ -24,6 +24,7 @@ package org.kreed.vanilla; import android.app.Dialog; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; @@ -60,6 +61,10 @@ public class NewPlaylistDialog extends Dialog implements TextWatcher, View.OnCli * action (e.g. "Create" or "Rename"). */ private int mActionRes; + /** + * An intent that is simply stored in the dialog. + */ + private Intent mIntent; /** * Create a NewPlaylistDialog. @@ -69,12 +74,14 @@ public class NewPlaylistDialog extends Dialog implements TextWatcher, View.OnCli * disabled when the EditText contains this text. * @param actionText A string resource describing the default positive * action (e.g. "Create"). + * @param intent An optional intent to store with the dialog. */ - public NewPlaylistDialog(Context context, String initialText, int actionText) + public NewPlaylistDialog(Context context, String initialText, int actionText, Intent intent) { super(context); mInitialText = initialText; mActionRes = actionText; + mIntent = intent; } @Override @@ -106,6 +113,14 @@ public class NewPlaylistDialog extends Dialog implements TextWatcher, View.OnCli return mText.getText().toString(); } + /** + * Returns the stored intent. + */ + public Intent getIntent() + { + return mIntent; + } + public void afterTextChanged(Editable s) { // do nothing diff --git a/src/org/kreed/vanilla/Playlist.java b/src/org/kreed/vanilla/Playlist.java index 03c205aa..589ab870 100644 --- a/src/org/kreed/vanilla/Playlist.java +++ b/src/org/kreed/vanilla/Playlist.java @@ -142,17 +142,23 @@ public class Playlist { } /** - * Add the given set of song ids to the playlist with the given id. + * Run the given query and add the results to the given playlist. Should be + * run on a background thread. * * @param context A context to use. * @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. + * @param query The query to run. The audio id should be the first column. + * @return The number of songs that were added to the playlist. */ - public static void addToPlaylist(Context context, long playlistId, long[] toAdd) + public static int addToPlaylist(Context context, long playlistId, QueryTask query) { + if (playlistId == -1) + return 0; + ContentResolver resolver = context.getContentResolver(); + + // Find the greatest PLAY_ORDER in the playlist 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); @@ -161,13 +167,25 @@ public class Playlist { 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]); + Cursor from = query.runQuery(resolver); + if (from == null) + return 0; + + int count = from.getCount(); + if (count > 0) { + ContentValues[] values = new ContentValues[count]; + for (int i = 0; i != count; ++i) { + from.moveToPosition(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, from.getLong(0)); + } + resolver.bulkInsert(uri, values); } - resolver.bulkInsert(uri, values); + + from.close(); + + return count; } /** diff --git a/src/org/kreed/vanilla/QueryTask.java b/src/org/kreed/vanilla/QueryTask.java index ea7cd996..67833e17 100644 --- a/src/org/kreed/vanilla/QueryTask.java +++ b/src/org/kreed/vanilla/QueryTask.java @@ -49,6 +49,26 @@ public class QueryTask { mSortOrder = sortOrder; } + /** + * Modify the uri of the pending query. + * + * @param uri The new uri. + */ + public void setUri(Uri uri) + { + mUri = uri; + } + + /** + * Modify the projection of the pending query. + * + * @param projection The new projection. + */ + public void setProjection(String[] projection) + { + mProjection = projection; + } + /** * Run the query. Should be called on a background thread. * diff --git a/src/org/kreed/vanilla/SongSelector.java b/src/org/kreed/vanilla/SongSelector.java index d10fea79..fcfa41f1 100644 --- a/src/org/kreed/vanilla/SongSelector.java +++ b/src/org/kreed/vanilla/SongSelector.java @@ -151,11 +151,11 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem mLimiterViews = (ViewGroup)findViewById(R.id.limiter_layout); - mArtistAdapter = setupView(R.id.artist_list, MediaUtils.TYPE_ARTIST, true, null); - mAlbumAdapter = setupView(R.id.album_list, MediaUtils.TYPE_ALBUM, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_albums")); - mSongAdapter = setupView(R.id.song_list, MediaUtils.TYPE_SONG, false, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_songs")); - mPlaylistAdapter = setupView(R.id.playlist_list, MediaUtils.TYPE_PLAYLIST, false, null); - mGenreAdapter = setupView(R.id.genre_list, MediaUtils.TYPE_GENRE, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_genres")); + mArtistAdapter = setupView(R.id.artist_list, MediaUtils.TYPE_ARTIST, true, true, null); + mAlbumAdapter = setupView(R.id.album_list, MediaUtils.TYPE_ALBUM, true, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_albums")); + mSongAdapter = setupView(R.id.song_list, MediaUtils.TYPE_SONG, false, true, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_songs")); + mPlaylistAdapter = setupView(R.id.playlist_list, MediaUtils.TYPE_PLAYLIST, false, false, null); + mGenreAdapter = setupView(R.id.genre_list, MediaUtils.TYPE_GENRE, true, false, state == null ? null : (MediaAdapter.Limiter)state.getSerializable("limiter_genres")); // These should be in the same order as MediaUtils.TYPE_* mAdapters = new MediaAdapter[] { mArtistAdapter, mAlbumAdapter, mSongAdapter, mPlaylistAdapter, mGenreAdapter }; @@ -185,7 +185,8 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); mDefaultAction = Integer.parseInt(settings.getString("default_action_int", "0")); - mLastActedId = 0; + mLastActedId = -2; + updateHeaders(); } @Override @@ -239,37 +240,58 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem } /** - * Adds songs matching the type and id of the given view to the song timelime. - * - * @param view The MediaView to get type/id data from - * @param action One of SongSelector.ACTION_* + * Update the first row of the lists with the appropriate action (play all + * or enqueue all). */ - private void pickSongs(MediaView view, int action) + private void updateHeaders() { - int type = view.getMediaType(); - long id = view.getMediaId(); - + int action = mDefaultAction; if (action == ACTION_LAST_USED) action = mLastAction; - else - mLastAction = action; - int mode = modeForAction[action]; - String[] projection = type == MediaUtils.TYPE_PLAYLIST ? - Song.FILLED_PLAYLIST_PROJECTION : Song.FILLED_PROJECTION; - QueryTask query = MediaUtils.buildQuery(type, id, projection, null); - PlaybackService.get(this).addSongs(mode, query); + int res = action == ACTION_ENQUEUE ? R.string.enqueue_all : R.string.play_all; + String text = getString(res); + mArtistAdapter.setHeaderText(text); + mAlbumAdapter.setHeaderText(text); + mSongAdapter.setHeaderText(text); } /** - * "Expand" the view by setting the limiter from the given view and - * switching to the appropriate tab. + * Adds songs matching the data from the given intent to the song timelime. * - * @param view The view to expand from. + * @param intent An intent created with + * {@link SongSelector#createClickIntent(MediaAdapter,MediaView)}. + * @param action One of SongSelector.ACTION_* */ - private void expand(MediaView view) + private void pickSongs(Intent intent, int action) { - mTabHost.setCurrentTab(setLimiter(view.getOwner().getLimiter(view))); + if (action == ACTION_LAST_USED) + action = mLastAction; + + int mode = modeForAction[action]; + QueryTask query = buildQueryFromIntent(intent, false); + PlaybackService.get(this).addSongs(mode, query); + + mLastActedId = intent.getLongExtra("id", -1); + + if (mDefaultAction == ACTION_LAST_USED && mLastAction != action) { + mLastAction = action; + updateHeaders(); + } + } + + /** + * "Expand" the view represented by the given intent by setting the limiter + * from the view and switching to the appropriate tab. + * + * @param intent An intent created with + * {@link SongSelector#createClickIntent(MediaAdapter,MediaView)}. + */ + private void expand(Intent intent) + { + int type = intent.getIntExtra("type", 1); + long id = intent.getLongExtra("id", -1); + mTabHost.setCurrentTab(setLimiter(mAdapters[type - 1].getLimiter(id))); } /** @@ -311,11 +333,11 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem { MediaView mediaView = (MediaView)view; if (mediaView.isExpanderPressed()) - expand(mediaView); + expand(createClickIntent((MediaAdapter)list.getAdapter(), mediaView)); else if (id == mLastActedId) startActivity(new Intent(this, FullPlaybackActivity.class)); else - pickSongs(mediaView, mDefaultAction); + pickSongs(createClickIntent((MediaAdapter)list.getAdapter(), mediaView), mDefaultAction); } public void afterTextChanged(Editable editable) @@ -407,8 +429,7 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem Cursor cursor = resolver.query(uri, projection, limiter.selection, null, null); if (cursor != null) { if (cursor.moveToNext()) { - limiter = new MediaAdapter.Limiter(cursor.getLong(0), MediaUtils.TYPE_ARTIST, MediaStore.Audio.Media.ARTIST_ID, new String[] { limiter.names[0] }); - setLimiter(limiter); + setLimiter(mArtistAdapter.getLimiter(cursor.getLong(0))); updateLimiterViews(); cursor.close(); return; @@ -424,6 +445,54 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem } } + /** + * Creates an intent based off of the media represented by the given view. + * + * @param adapter The adapter that owns the view. + * @param view The MediaView to build from. + */ + private static Intent createClickIntent(MediaAdapter adapter, MediaView view) + { + Intent intent = new Intent(); + intent.putExtra("type", adapter.getMediaType()); + intent.putExtra("id", view.getMediaId()); + intent.putExtra("isHeader", view.isHeader()); + intent.putExtra("title", view.getTitle()); + return intent; + } + + /** + * Builds a media query based off the data stored in the given intent. + * + * @param intent An intent created with + * {@link SongSelector#createClickIntent(MediaAdapter,MediaView)}. + * @param empty If true, use the empty projection (only query id). + */ + private QueryTask buildQueryFromIntent(Intent intent, boolean empty) + { + int type = intent.getIntExtra("type", 1); + + String[] projection; + if (type == MediaUtils.TYPE_PLAYLIST) + projection = empty ? Song.EMPTY_PLAYLIST_PROJECTION : Song.FILLED_PLAYLIST_PROJECTION; + else + projection = empty ? Song.EMPTY_PROJECTION : Song.FILLED_PROJECTION; + + QueryTask query; + if (intent.getBooleanExtra("isHeader", false)) { + query = mAdapters[type - 1].buildQuery(true); + query.setProjection(projection); + // we want to query songs, not albums or artists + if (type != MediaUtils.TYPE_SONG) + query.setUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); + } else { + long id = intent.getLongExtra("id", -1); + query = MediaUtils.buildQuery(type, id, projection, null); + } + + return query; + } + private static final int MENU_PLAY = 0; private static final int MENU_ENQUEUE = 1; private static final int MENU_EXPAND = 2; @@ -432,70 +501,68 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem private static final int MENU_DELETE = 5; private static final int MENU_EDIT = 6; private static final int MENU_RENAME_PLAYLIST = 7; + private static final int MENU_SELECT_PLAYLIST = 8; @Override public void onCreateContextMenu(ContextMenu menu, View listView, ContextMenu.ContextMenuInfo absInfo) { + MediaAdapter adapter = (MediaAdapter)((ListView)listView).getAdapter(); MediaView view = (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 (view.getMediaType() == MediaUtils.TYPE_PLAYLIST) { - menu.add(0, MENU_RENAME_PLAYLIST, 0, R.string.rename); - menu.add(0, MENU_EDIT, 0, R.string.edit); + // Store view data in intent to avoid problems when the view data changes + // as worked is performed in the background. + Intent intent = createClickIntent(adapter, view); + + if (view.isHeader()) + menu.setHeaderTitle(getString(R.string.all_songs)); + else + menu.setHeaderTitle(view.getTitle()); + + menu.add(0, MENU_PLAY, 0, R.string.play).setIntent(intent); + menu.add(0, MENU_ENQUEUE, 0, R.string.enqueue).setIntent(intent); + if (adapter == mPlaylistAdapter) { + menu.add(0, MENU_RENAME_PLAYLIST, 0, R.string.rename).setIntent(intent); + menu.add(0, MENU_EDIT, 0, R.string.edit).setIntent(intent); } - SubMenu playlistMenu = menu.addSubMenu(0, MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist); + menu.addSubMenu(0, MENU_ADD_TO_PLAYLIST, 0, R.string.add_to_playlist).getItem().setIntent(intent); 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(this); - if (playlists != null) { - for (int i = 0; i != playlists.length; ++i) - playlistMenu.add(type, (int)playlists[i].id + 100, id, playlists[i].name); - } + menu.add(0, MENU_EXPAND, 0, R.string.expand).setIntent(intent); + if (!view.isHeader()) + menu.add(0, MENU_DELETE, 0, R.string.delete).setIntent(intent); } /** - * 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. + * Add a set of songs represented by the intent to a playlist. 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). + * @param playlistId The id of the playlist to add to. + * @param intent An intent created with + * {@link SongSelector#createClickIntent(MediaAdapter,MediaView)}. */ - private void addToPlaylist(long playlistId, int type, long mediaId, CharSequence title) + private void addToPlaylist(long playlistId, Intent intent) { - long[] ids = MediaUtils.getAllSongIdsWith(this, type, mediaId); - Playlist.addToPlaylist(this, playlistId, ids); + QueryTask query = buildQueryFromIntent(intent, true); + int count = Playlist.addToPlaylist(this, playlistId, query); - String message = getResources().getQuantityString(R.plurals.added_to_playlist, ids.length, ids.length, title); + String message = getResources().getQuantityString(R.plurals.added_to_playlist, count, count, intent.getStringExtra("playlistName")); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } /** - * Delete the media with the specified type and id and show a Toast + * Delete the media represented by the given intent 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. + * @param intent An intent created with + * {@link SongSelector#createClickIntent(MediaAdapter,MediaView)}. */ - private void delete(int type, long id, String title) + private void delete(Intent intent) { + int type = intent.getIntExtra("type", 1); + long id = intent.getLongExtra("id", -1); + if (type == MediaUtils.TYPE_PLAYLIST) { Playlist.deletePlaylist(this, id); - String message = getResources().getString(R.string.playlist_deleted, title); + String message = getResources().getString(R.string.playlist_deleted, intent.getStringExtra("title")); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } else { int count = MediaUtils.deleteMedia(this, type, id); @@ -507,58 +574,57 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem @Override public boolean onContextItemSelected(MenuItem item) { - int id = item.getItemId(); - int type = item.getGroupId(); - int mediaId = item.getOrder(); + Intent intent = item.getIntent(); - switch (id) { + switch (item.getItemId()) { case MENU_EXPAND: - expand((MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView); + expand(intent); break; case MENU_ENQUEUE: - pickSongs((MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView, ACTION_ENQUEUE); + pickSongs(intent, ACTION_ENQUEUE); break; case MENU_PLAY: - pickSongs((MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView, ACTION_PLAY); + pickSongs(intent, ACTION_PLAY); break; case MENU_NEW_PLAYLIST: { - NewPlaylistDialog dialog = new NewPlaylistDialog(this, null, R.string.create); - Message message = mHandler.obtainMessage(MSG_NEW_PLAYLIST, type, mediaId); - message.obj = dialog; - dialog.setDismissMessage(message); + NewPlaylistDialog dialog = new NewPlaylistDialog(this, null, R.string.create, intent); + dialog.setDismissMessage(mHandler.obtainMessage(MSG_NEW_PLAYLIST, dialog)); dialog.show(); break; } case MENU_RENAME_PLAYLIST: { - MediaView view = (MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView; - NewPlaylistDialog dialog = new NewPlaylistDialog(this, view.getTitle(), R.string.rename); - Message message = mHandler.obtainMessage(MSG_RENAME_PLAYLIST, view.getMediaType(), (int)view.getMediaId()); - message.obj = dialog; - dialog.setDismissMessage(message); + NewPlaylistDialog dialog = new NewPlaylistDialog(this, intent.getStringExtra("title"), R.string.rename, intent); + dialog.setDismissMessage(mHandler.obtainMessage(MSG_RENAME_PLAYLIST, dialog)); dialog.show(); break; } - case MENU_DELETE: { - MediaView view = (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); + case MENU_DELETE: + mHandler.sendMessage(mHandler.obtainMessage(MSG_DELETE, intent)); + break; + case MENU_ADD_TO_PLAYLIST: { + SubMenu playlistMenu = item.getSubMenu(); + playlistMenu.add(0, MENU_NEW_PLAYLIST, 0, R.string.new_playlist).setIntent(intent); + Playlist[] playlists = Playlist.getPlaylists(this); + if (playlists != null) { + for (int i = 0; i != playlists.length; ++i) { + Intent copy = new Intent(intent); + copy.putExtra("playlist", playlists[i].id); + copy.putExtra("playlistName", playlists[i].name); + playlistMenu.add(0, MENU_SELECT_PLAYLIST, 0, playlists[i].name).setIntent(copy); + } + } break; } - case MENU_EDIT: - MediaView view = (MediaView)((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).targetView; - Intent intent = new Intent(Intent.ACTION_EDIT); - intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); - intent.putExtra("playlist", String.valueOf(view.getMediaId())); - startActivity(intent); + case MENU_EDIT: { + Intent launch = new Intent(Intent.ACTION_EDIT); + launch.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); + launch.putExtra("playlist", String.valueOf(intent.getLongExtra("id", 0))); + startActivity(launch); + break; + } + case MENU_SELECT_PLAYLIST: + mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_TO_PLAYLIST, intent)); break; - default: - if (id > 100) - addToPlaylist(id - 100, type, mediaId, item.getTitle()); - return false; } return true; @@ -593,9 +659,10 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem * @param id The id of the ListView * @param type The media type for the adapter. * @param expandable True if the rows are expandable. + * @param hasHeader True if the view should have a header row. * @param limiter The initial limiter to set on the adapter. */ - private MediaAdapter setupView(int id, int type, boolean expandable, MediaAdapter.Limiter limiter) + private MediaAdapter setupView(int id, int type, boolean expandable, boolean hasHeader, MediaAdapter.Limiter limiter) { ListView view = (ListView)findViewById(id); view.setOnItemClickListener(this); @@ -604,32 +671,24 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem view.setDivider(null); view.setFastScrollEnabled(true); - MediaAdapter adapter = new MediaAdapter(this, type, expandable, limiter); + MediaAdapter adapter = new MediaAdapter(this, type, expandable, hasHeader, limiter); view.setAdapter(adapter); return adapter; } /** - * 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. + * Call addToPlaylist with the results from a NewPlaylistDialog stored in + * obj. */ 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. + * Delete the songs represented by the intent stored in obj. */ private static final int MSG_DELETE = 12; /** - * Rename the playlist 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. + * Call renamePlaylist with the results from a NewPlaylistDialog stored in + * obj. */ private static final int MSG_RENAME_PLAYLIST = 13; /** @@ -637,32 +696,55 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem * obj will contain the MediaAdapter. */ public static final int MSG_RUN_QUERY = 14; + /** + * Call addToPlaylist with data from the intent in obj. + */ + public static final int MSG_ADD_TO_PLAYLIST = 15; @Override public boolean handleMessage(Message message) { switch (message.what) { + case MSG_ADD_TO_PLAYLIST: { + Intent intent = (Intent)message.obj; + addToPlaylist(intent.getLongExtra("playlist", -1), intent); + break; + } case MSG_NEW_PLAYLIST: { NewPlaylistDialog dialog = (NewPlaylistDialog)message.obj; if (dialog.isAccepted()) { String name = dialog.getText(); long playlistId = Playlist.createPlaylist(this, name); - addToPlaylist(playlistId, message.arg1, message.arg2, name); + Intent intent = dialog.getIntent(); + intent.putExtra("playlistName", name); + addToPlaylist(playlistId, intent); } break; } case MSG_DELETE: - delete(message.arg1, message.arg2, (String)message.obj); + delete((Intent)message.obj); break; case MSG_RENAME_PLAYLIST: { NewPlaylistDialog dialog = (NewPlaylistDialog)message.obj; - if (dialog.isAccepted()) - Playlist.renamePlaylist(this, message.arg2, dialog.getText()); + if (dialog.isAccepted()) { + long playlistId = dialog.getIntent().getLongExtra("id", -1); + Playlist.renamePlaylist(this, playlistId, dialog.getText()); + } break; } - case MSG_RUN_QUERY: - ((MediaAdapter)message.obj).runQuery(); + case MSG_RUN_QUERY: { + final MediaAdapter adapter = (MediaAdapter)message.obj; + QueryTask query = adapter.buildQuery(false); + final Cursor cursor = query.runQuery(getContentResolver()); + runOnUiThread(new Runnable() { + @Override + public void run() + { + adapter.changeCursor(cursor); + } + }); break; + } default: return super.handleMessage(message); } @@ -681,23 +763,6 @@ public class SongSelector extends PlaybackActivity implements AdapterView.OnItem mHandler.sendMessage(mHandler.obtainMessage(MSG_RUN_QUERY, adapter)); } - /** - * Update the given adapter with the given cursor on the UI thread. - * - * @param adapter The adapter to update. - * @param cursor The cursor to update with. - */ - public void changeCursor(final MediaAdapter adapter, final Cursor cursor) - { - runOnUiThread(new Runnable() { - @Override - public void run() - { - adapter.changeCursor(cursor); - } - }); - } - @Override public void onMediaChange() {