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;