diff --git a/res/drawable-hdpi/ic_menu_sort_alphabetically.png b/res/drawable-hdpi/ic_menu_sort_alphabetically.png new file mode 100644 index 00000000..94ee3bed Binary files /dev/null and b/res/drawable-hdpi/ic_menu_sort_alphabetically.png differ diff --git a/res/drawable-mdpi/ic_menu_sort_alphabetically.png b/res/drawable-mdpi/ic_menu_sort_alphabetically.png new file mode 100644 index 00000000..06d5c744 Binary files /dev/null and b/res/drawable-mdpi/ic_menu_sort_alphabetically.png differ diff --git a/res/values/translatable.xml b/res/values/translatable.xml index 130b6d60..306ce65f 100644 --- a/res/values/translatable.xml +++ b/res/values/translatable.xml @@ -55,6 +55,7 @@ THE SOFTWARE. Expand Delete Now Playing + Sort By Search Done @@ -88,6 +89,15 @@ THE SOFTWARE. Enqueue All All Songs + Name + Number of tracks + Year + Date added + Artist, album + Artist, album, track no. + Artist, album, title + Artist, year + %s (Paused) diff --git a/src/org/kreed/vanilla/LibraryActivity.java b/src/org/kreed/vanilla/LibraryActivity.java index 17aeefd9..186481aa 100644 --- a/src/org/kreed/vanilla/LibraryActivity.java +++ b/src/org/kreed/vanilla/LibraryActivity.java @@ -361,6 +361,9 @@ public class LibraryActivity } } + loadSortOrder(mSongAdapter); + loadSortOrder(mArtistAdapter); + requestRequery(mSongAdapter); requestRequery(mAlbumAdapter); @@ -702,14 +705,31 @@ public class LibraryActivity @Override public boolean onCreateOptionsMenu(Menu menu) { - menu.add(0, MENU_PLAYBACK, 0, R.string.playback_view).setIcon(R.drawable.ic_menu_gallery); + menu.addSubMenu(0, MENU_SORT, 0, R.string.sort_by).setIcon(R.drawable.ic_menu_sort_alphabetically); menu.add(0, MENU_SEARCH, 0, R.string.search).setIcon(R.drawable.ic_menu_search); + menu.add(0, MENU_PLAYBACK, 0, R.string.playback_view).setIcon(R.drawable.ic_menu_gallery); return super.onCreateOptionsMenu(menu); } + public static final int GROUP_SORT = 100; + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (item.getGroupId() == GROUP_SORT) { + MediaAdapter adapter = mCurrentAdapter; + ListView view = (ListView)mTabHost.getCurrentView(); + int i = item.getItemId(); + adapter.setSortMode(i); + requestRequery(adapter); + // Force a new FastScroller to be created so the scroll section + // are updated. + view.setFastScrollEnabled(false); + view.setFastScrollEnabled(true); + mHandler.sendMessage(mHandler.obtainMessage(MSG_SAVE_SORT, adapter)); + return true; + } + switch (item.getItemId()) { case MENU_SEARCH: setSearchBoxVisible(!mSearchBoxVisible); @@ -717,6 +737,9 @@ public class LibraryActivity case MENU_PLAYBACK: startActivity(new Intent(this, FullPlaybackActivity.class)); return true; + case MENU_SORT: + mCurrentAdapter.buildSortMenu(item.getSubMenu()); + return true; default: return super.onOptionsItemSelected(item); } @@ -744,6 +767,7 @@ public class LibraryActivity MediaAdapter adapter = new MediaAdapter(this, type, expandable, hasHeader, limiter); view.setAdapter(adapter); + loadSortOrder(adapter); Resources res = getResources(); String labelRes = res.getString(label); @@ -779,6 +803,10 @@ public class LibraryActivity * Call addToPlaylist with data from the intent in obj. */ private static final int MSG_ADD_TO_PLAYLIST = 15; + /** + * Save the sort mode for the adapter passed in obj. + */ + private static final int MSG_SAVE_SORT = 16; @Override public boolean handleMessage(Message message) @@ -824,6 +852,13 @@ public class LibraryActivity }); break; } + case MSG_SAVE_SORT: { + MediaAdapter adapter = (MediaAdapter)message.obj; + SharedPreferences.Editor editor = PlaybackService.getSettings(this).edit(); + editor.putInt(String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()), adapter.getSortMode()); + editor.commit(); + break; + } default: return super.handleMessage(message); } @@ -928,4 +963,18 @@ public class LibraryActivity runQuery(adapter); updateLimiterViews(); } + + /** + * Set the saved sort mode for the given adapter. The adapter should + * be re-queried after calling this. + * + * @param adapter The adapter to load for. + */ + private void loadSortOrder(MediaAdapter adapter) + { + String key = String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()); + int def = adapter.getDefaultSortMode(); + int sort = PlaybackService.getSettings(this).getInt(key, def); + adapter.setSortMode(sort); + } } diff --git a/src/org/kreed/vanilla/MediaAdapter.java b/src/org/kreed/vanilla/MediaAdapter.java index bfaac90c..497c6b58 100644 --- a/src/org/kreed/vanilla/MediaAdapter.java +++ b/src/org/kreed/vanilla/MediaAdapter.java @@ -28,6 +28,7 @@ import android.database.DatabaseUtils; import android.net.Uri; import android.provider.BaseColumns; import android.provider.MediaStore; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; @@ -113,6 +114,9 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { * True if the data is stale and the query should be re-run. */ private boolean mNeedsRequery; + private int[] mSortEntries; + private String[] mSortValues; + private int mSortMode; /** * Construct a MediaAdapter representing the given type of @@ -124,7 +128,7 @@ 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 hasHeader Whether this view has a header row. * @param limiter An initial limiter to use */ public MediaAdapter(Context context, int type, boolean expandable, boolean hasHeader, Limiter limiter) @@ -145,6 +149,8 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { mFields = new String[] { MediaStore.Audio.Artists.ARTIST }; mFieldKeys = new String[] { MediaStore.Audio.Artists.ARTIST_KEY }; mSongSort = MediaUtils.DEFAULT_SORT; + mSortEntries = new int[] { R.string.name, R.string.number_of_tracks }; + mSortValues = new String[] { "artist_key", "number_of_tracks" }; break; case MediaUtils.TYPE_ALBUM: mStore = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; @@ -152,21 +158,29 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { // Why is there no artist_key column constant in the album MediaStore? The column does seem to exist. mFieldKeys = new String[] { "artist_key", MediaStore.Audio.Albums.ALBUM_KEY }; mSongSort = "album_key,track"; + mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.number_of_tracks }; + mSortValues = new String[] { "album_key", "artist_key,album_key", "minyear", "numsongs" }; break; case MediaUtils.TYPE_SONG: mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; mFields = new String[] { MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE }; mFieldKeys = new String[] { MediaStore.Audio.Media.ARTIST_KEY, MediaStore.Audio.Media.ALBUM_KEY, MediaStore.Audio.Media.TITLE_KEY }; + mSortEntries = new int[] { R.string.name, R.string.artist_album_track, R.string.artist_album_title, R.string.artist_year, R.string.year }; + mSortValues = new String[] { "title_key", "artist_key,album_key,track", "artist_key,album_key,title_key", "artist_key,year", "year" }; break; case MediaUtils.TYPE_PLAYLIST: mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; mFields = new String[] { MediaStore.Audio.Playlists.NAME }; mFieldKeys = null; + mSortEntries = new int[] { R.string.name, R.string.date_added }; + mSortValues = new String[] { "name", "date_added" }; break; case MediaUtils.TYPE_GENRE: mStore = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; mFields = new String[] { MediaStore.Audio.Genres.NAME }; mFieldKeys = null; + mSortEntries = new int[] { R.string.name }; + mSortValues = new String[] { "name" }; break; default: throw new IllegalArgumentException("Invalid value for type: " + type); @@ -292,13 +306,7 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { StringBuilder selection = new StringBuilder(); String[] selectionArgs = null; - String sort; - if (limiter != null && limiter.type == MediaUtils.TYPE_ALBUM) - sort = MediaStore.Audio.Media.TRACK; - else if (mFieldKeys == null) - sort = mFields[mFields.length - 1]; - else - sort = mFieldKeys[mFieldKeys.length - 1]; + String sort = mSortValues[mSortMode]; if (mType == MediaUtils.TYPE_SONG || forceMusicCheck) selection.append("is_music!=0"); @@ -346,7 +354,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. : / - return MediaUtils.buildGenreQuery(limiter.id, projection, selection.toString(), selectionArgs); + return MediaUtils.buildGenreQuery(limiter.id, projection, selection.toString(), selectionArgs, sort); } else { if (limiter != null) { if (selection.length() != 0) @@ -379,6 +387,8 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { QueryTask query = buildQuery(projection, true); if (mType != MediaUtils.TYPE_SONG) { query.setUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); + // Would be better to match the sort order in the adapter. This + // is likely to require significantly more work though. query.setSortOrder(mSongSort); } return query; @@ -471,6 +481,8 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { @Override public Object[] getSections() { + if (mSortMode != 0) + return null; return mIndexer.getSections(); } @@ -511,6 +523,64 @@ public class MediaAdapter extends CursorAdapter implements SectionIndexer { return new MediaView(mContext, null, mExpandable ? MediaView.sExpander : null); } + /** + * Returns the type of the current limiter. + * + * @return One of MediaUtils.TYPE_, or MediaUtils.TYPE_INVALID if there is + * no limiter set. + */ + public int getLimiterType() + { + Limiter limiter = mLimiter; + if (limiter != null) + return limiter.type; + return MediaUtils.TYPE_INVALID; + } + + /** + * Build a menu of sort modes appropriate for this adapter. + * + * @param menu The menu the items will be added to. + */ + public void buildSortMenu(SubMenu menu) + { + int[] entries = mSortEntries; + menu.clear(); + for (int i = 0; i != entries.length; ++i) + menu.add(LibraryActivity.GROUP_SORT, i, 0, entries[i]); + } + + /** + * Set the sorting mode. The adapter should be re-queried after changing + * this. + * + * @param i The index of the sort mode. + */ + public void setSortMode(int i) + { + mSortMode = i; + } + + /** + * Returns the sort mode that should be used if no preference is saved. This + * may very based on the active limiter. + */ + public int getDefaultSortMode() + { + Limiter limiter = mLimiter; + if (limiter != null && limiter.type == MediaUtils.TYPE_SONG) + return 1; // artist,album,track + return 0; + } + + /** + * Return the current sort mode set on this adapter. + */ + public int getSortMode() + { + return mSortMode; + } + /** * Limiter is a constraint for MediaAdapters used when a row is "expanded". */ diff --git a/src/org/kreed/vanilla/MediaUtils.java b/src/org/kreed/vanilla/MediaUtils.java index 1f51eb79..d4cf8155 100644 --- a/src/org/kreed/vanilla/MediaUtils.java +++ b/src/org/kreed/vanilla/MediaUtils.java @@ -33,6 +33,10 @@ import java.util.Random; import junit.framework.Assert; public class MediaUtils { + /** + * A special invalid media type. + */ + public static final int TYPE_INVALID = 0; /** * Type indicating an id represents an artist. */ @@ -159,11 +163,11 @@ public class MediaUtils { * @param projection The columns to query. * @param selection The selection to pass to the query, or null. * @param selectionArgs The arguments to substitute into the selection. + * @param sort The sort order. */ - public static QueryTask buildGenreQuery(long id, String[] projection, String selection, String[] selectionArgs) + public static QueryTask buildGenreQuery(long id, String[] projection, String selection, String[] selectionArgs, String sort) { Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", id); - String sort = MediaStore.Audio.Genres.Members.TITLE_KEY; return new QueryTask(uri, projection, selection, selectionArgs, sort); } @@ -187,7 +191,7 @@ public class MediaUtils { case TYPE_PLAYLIST: return buildPlaylistQuery(id, projection, selection); case TYPE_GENRE: - return buildGenreQuery(id, projection, selection, null); + return buildGenreQuery(id, projection, selection, null, MediaStore.Audio.Genres.Members.TITLE_KEY); default: throw new IllegalArgumentException("Specified type not valid: " + type); } diff --git a/src/org/kreed/vanilla/PlaybackActivity.java b/src/org/kreed/vanilla/PlaybackActivity.java index 09eae20f..10278a6c 100644 --- a/src/org/kreed/vanilla/PlaybackActivity.java +++ b/src/org/kreed/vanilla/PlaybackActivity.java @@ -355,6 +355,7 @@ public class PlaybackActivity extends Activity { } + static final int MENU_SORT = 1; static final int MENU_PREFS = 2; static final int MENU_LIBRARY = 3; static final int MENU_PLAYBACK = 5;