OSDN Git Service

Eleven: Add Section Headers
authorlinus_lee <llee@cyngn.com>
Mon, 18 Aug 2014 23:45:24 +0000 (16:45 -0700)
committerlinus_lee <llee@cyngn.com>
Thu, 20 Nov 2014 19:58:03 +0000 (11:58 -0800)
This implements section headers for the song/album/artist fragments for each of the sort types
including dates, numbers, durations, alphabetical etc.  The ui will need to be tweaked to look
better, but that is pending UX.  There is a bug with localization (like Chinese songs)
where we could create a valid section header but from an alphabetical it doesn't look correct.
I will look at this afer this big check-in

Change-Id: Ibfc09bb12ebf61c668f135358f65f1e7947bac79

30 files changed:
Android.mk
res/layout/list_header.xml
res/values/strings.xml
src/com/cyngn/eleven/adapters/AlbumAdapter.java
src/com/cyngn/eleven/adapters/ArtistAdapter.java
src/com/cyngn/eleven/adapters/SongAdapter.java
src/com/cyngn/eleven/loaders/AlbumLoader.java
src/com/cyngn/eleven/loaders/AlbumSongLoader.java
src/com/cyngn/eleven/loaders/ArtistLoader.java
src/com/cyngn/eleven/loaders/ArtistSongLoader.java
src/com/cyngn/eleven/loaders/FavoritesLoader.java
src/com/cyngn/eleven/loaders/GenreSongLoader.java
src/com/cyngn/eleven/loaders/LastAddedLoader.java
src/com/cyngn/eleven/loaders/NowPlayingCursor.java
src/com/cyngn/eleven/loaders/PlaylistSongLoader.java
src/com/cyngn/eleven/loaders/QueueLoader.java
src/com/cyngn/eleven/loaders/RecentLoader.java
src/com/cyngn/eleven/loaders/SearchLoader.java
src/com/cyngn/eleven/loaders/SongLoader.java
src/com/cyngn/eleven/model/Song.java
src/com/cyngn/eleven/sectionadapter/SectionAdapter.java [new file with mode: 0644]
src/com/cyngn/eleven/sectionadapter/SectionCreator.java [new file with mode: 0644]
src/com/cyngn/eleven/sectionadapter/SectionListContainer.java [new file with mode: 0644]
src/com/cyngn/eleven/ui/fragments/AlbumFragment.java
src/com/cyngn/eleven/ui/fragments/ArtistFragment.java
src/com/cyngn/eleven/ui/fragments/RecentFragment.java
src/com/cyngn/eleven/ui/fragments/SongFragment.java
src/com/cyngn/eleven/utils/LocaleUtils.java [new file with mode: 0644]
src/com/cyngn/eleven/utils/MusicUtils.java
src/com/cyngn/eleven/utils/SectionCreatorUtils.java [new file with mode: 0644]

index c043221..42cc885 100644 (file)
@@ -7,12 +7,12 @@ LOCAL_SRC_FILES := src/com/cyngn/eleven/IElevenService.aidl
 LOCAL_SRC_FILES += $(call all-java-files-under, src)
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-v4
+    android-support-v4 \
+    android-common
 
 LOCAL_PACKAGE_NAME := Eleven
 LOCAL_OVERRIDES_PACKAGES := Music
 
-LOCAL_SDK_VERSION := current
 LOCAL_PROGUARD_ENABLED := disabled
 
 include $(BUILD_PACKAGE)
index 3742e37..c4f2fa7 100644 (file)
   limitations under the License.
 -->
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/header"
     android:layout_width="match_parent"
     android:layout_height="32.0dip"
     android:background="@null"
     android:gravity="center_vertical"
     android:paddingLeft="10.0dip"
-    android:textAllCaps="true"
     android:textColor="@color/white"
     android:textSize="@dimen/text_size_medium" />
index 6af9c37..0635eb3 100644 (file)
     <string name="empty_favorite">Songs you mark as favorites will be shown here.</string>
     <string name="empty_recent">Albums you have listened to will show up here. Try playing some music.</string>
 
+    <!-- Section Headers -->
+    <string name="header_unknown_year">Unknown Year</string>
+    <string name="header_less_than_30s">Less than 30s</string>
+    <string name="header_30_to_60_seconds">30 - 60 seconds</string>
+    <string name="header_1_to_2_minutes">1 - 2 minutes</string>
+    <string name="header_2_to_3_minutes">2 - 3 minutes</string>
+    <string name="header_3_to_4_minutes">3 - 4 minutes</string>
+    <string name="header_4_to_5_minutes">4 - 5 minutes</string>
+    <string name="header_5_to_10_minutes">5 - 10 minutes</string>
+    <string name="header_10_to_30_minutes">10 - 30 minutes</string>
+    <string name="header_30_to_60_minutes">30 - 60 minutes</string>
+    <string name="header_greater_than_60_minutes">More than 60 minutes</string>
+
+    <string name="header_1_song">1 Song</string>
+    <string name="header_2_to_4_songs">2 - 4 Songs</string>
+    <string name="header_5_to_9_songs">5 - 9 Songs</string>
+    <string name="header_10_plus_songs">10+ Songs</string>
+
+    <string name="header_1_album">1 Album</string>
+    <string name="header_n_albums"><xliff:g id="number">%d</xliff:g> Albums</string>
+    <string name="header_5_plus_albums">5+ Albums</string>
 </resources>
index 6ab4033..21da11a 100644 (file)
@@ -20,10 +20,10 @@ import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 
-import com.cyngn.eleven.Config;
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.cache.ImageFetcher;
 import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
 import com.cyngn.eleven.ui.MusicHolder;
 import com.cyngn.eleven.ui.MusicHolder.DataHolder;
 import com.cyngn.eleven.utils.ApolloUtils;
@@ -35,7 +35,7 @@ import com.cyngn.eleven.utils.MusicUtils;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class AlbumAdapter extends ArrayAdapter<Album> {
+public class AlbumAdapter extends ArrayAdapter<Album> implements SectionAdapter.BasicAdapter {
 
     /**
      * Number of views (ImageView and TextView)
index 682861a..11e24d8 100644 (file)
@@ -23,6 +23,7 @@ import android.widget.ImageView;
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.cache.ImageFetcher;
 import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
 import com.cyngn.eleven.ui.MusicHolder;
 import com.cyngn.eleven.ui.MusicHolder.DataHolder;
 import com.cyngn.eleven.utils.ApolloUtils;
@@ -37,7 +38,7 @@ import com.cyngn.eleven.utils.MusicUtils;
 /**
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class ArtistAdapter extends ArrayAdapter<Artist> {
+public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionAdapter.BasicAdapter {
 
     /**
      * Number of views (ImageView and TextView)
index bc2684c..92bbacd 100644 (file)
@@ -18,6 +18,7 @@ import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 
 import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
 import com.cyngn.eleven.ui.MusicHolder;
 import com.cyngn.eleven.ui.MusicHolder.DataHolder;
 import com.cyngn.eleven.ui.fragments.QueueFragment;
@@ -31,7 +32,7 @@ import com.cyngn.eleven.utils.MusicUtils;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class SongAdapter extends ArrayAdapter<Song> {
+public class SongAdapter extends ArrayAdapter<Song> implements SectionAdapter.BasicAdapter {
 
     /**
      * Number of views (TextView)
@@ -137,4 +138,9 @@ public class SongAdapter extends ArrayAdapter<Song> {
         mData = null;
     }
 
+    /**
+     * Do nothing.
+     */
+    public void flush() {
+    }
 }
index f6f80f1..48bc944 100644 (file)
@@ -19,8 +19,12 @@ import android.provider.MediaStore.Audio.AlbumColumns;
 
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
 import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.utils.SectionCreatorUtils;
+import com.cyngn.eleven.utils.SortOrder;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -31,7 +35,7 @@ import java.util.List;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class AlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
+public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
 
     /**
      * The result
@@ -45,7 +49,7 @@ public class AlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
 
     /**
      * Constructor of <code>AlbumLoader</code>
-     * 
+     *
      * @param context The {@link Context} to use
      */
     public AlbumLoader(final Context context) {
@@ -89,6 +93,7 @@ public class AlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
             mCursor.close();
             mCursor = null;
         }
+
         return mAlbumsList;
     }
 
index 81e9c87..de7e3b4 100644 (file)
@@ -86,8 +86,11 @@ public class AlbumSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Make the duration label
                 final int seconds = (int) (duration / 1000);
 
+                // Grab the Song Year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, seconds);
+                final Song song = new Song(id, songName, artist, album, seconds, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -123,7 +126,9 @@ public class AlbumSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 3 */
                         AudioColumns.ALBUM,
                         /* 4 */
-                        AudioColumns.DURATION
+                        AudioColumns.DURATION,
+                        /* 5 */
+                        AudioColumns.YEAR,
                 }, selection.toString(), null,
                 PreferenceUtils.getInstance(context).getAlbumSongSortOrder());
     }
index 4956b90..4894006 100644 (file)
@@ -19,6 +19,7 @@ import android.provider.MediaStore.Audio.ArtistColumns;
 
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
 import com.cyngn.eleven.utils.Lists;
 import com.cyngn.eleven.utils.PreferenceUtils;
 
@@ -31,7 +32,7 @@ import java.util.List;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class ArtistLoader extends WrappedAsyncTaskLoader<List<Artist>> {
+public class ArtistLoader extends SectionCreator.SimpleListLoader<Artist> {
 
     /**
      * The result
index 8d32fbc..18b7082 100644 (file)
@@ -86,8 +86,11 @@ public class ArtistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Grab the Song Year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -123,7 +126,9 @@ public class ArtistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 3 */
                         AudioColumns.ALBUM,
                         /* 4 */
-                        AudioColumns.DURATION
+                        AudioColumns.DURATION,
+                        /* 5 */
+                        AudioColumns.YEAR,
                 }, selection.toString(), null,
                 PreferenceUtils.getInstance(context).getArtistSongSortOrder());
     }
index e517d81..fa96a79 100644 (file)
@@ -76,7 +76,7 @@ public class FavoritesLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         .getColumnIndexOrThrow(FavoriteColumns.ALBUMNAME));
 
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, -1);
+                final Song song = new Song(id, songName, artist, album, -1, -1);
 
                 // Add everything up
                 mSongList.add(song);
index f684f93..699e8bb 100644 (file)
@@ -83,8 +83,11 @@ public class GenreSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Grab the Song Year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -119,7 +122,9 @@ public class GenreSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 3 */
                         MediaStore.Audio.Genres.Members.ARTIST,
                         /* 4 */
-                        MediaStore.Audio.Genres.Members.DURATION
+                        MediaStore.Audio.Genres.Members.DURATION,
+                        /* 5 */
+                        MediaStore.Audio.Genres.Members.YEAR,
                 }, selection.toString(), null, MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER);
     }
 }
index fc3b44d..b9e6185 100644 (file)
@@ -78,8 +78,11 @@ public class LastAddedLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Grab the Song Year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -115,7 +118,9 @@ public class LastAddedLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 3 */
                         AudioColumns.ALBUM,
                         /* 4 */
-                        AudioColumns.DURATION
+                        AudioColumns.DURATION,
+                        /* 5 */
+                        AudioColumns.YEAR,
                 }, selection.toString(), null, MediaStore.Audio.Media.DATE_ADDED + " DESC");
     }
 }
index 2254ee8..00b90e8 100644 (file)
@@ -34,7 +34,9 @@ public class NowPlayingCursor extends AbstractCursor {
             /* 3 */
             AudioColumns.ALBUM,
             /* 4 */
-            AudioColumns.DURATION
+            AudioColumns.DURATION,
+            /* 5 */
+            AudioColumns.YEAR,
     };
 
     private final Context mContext;
index 745d971..19a0cf0 100644 (file)
@@ -89,8 +89,12 @@ public class PlaylistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Grab the Song Year
+                final int year = mCursor.getInt(mCursor
+                        .getColumnIndexOrThrow(AudioColumns.YEAR));
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -129,7 +133,9 @@ public class PlaylistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 4 */
                         AudioColumns.ALBUM,
                         /* 5 */
-                        AudioColumns.DURATION
+                        AudioColumns.DURATION,
+                        /* 6 */
+                        AudioColumns.YEAR,
                 }, mSelection.toString(), null,
                 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
     }
index 3c4491c..7139d05 100644 (file)
@@ -74,8 +74,11 @@ public class QueueLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Copy the year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
index 781cc8f..2f1f921 100644 (file)
@@ -25,7 +25,7 @@ import java.util.List;
 
 /**
  * Used to query {@link RecentStore} and return the last listened to albums.
- * 
+ *
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
 public class RecentLoader extends WrappedAsyncTaskLoader<List<Album>> {
@@ -42,7 +42,7 @@ public class RecentLoader extends WrappedAsyncTaskLoader<List<Album>> {
 
     /**
      * Constructor of <code>RecentLoader</code>
-     * 
+     *
      * @param context The {@link Context} to use
      */
     public RecentLoader(final Context context) {
@@ -96,7 +96,7 @@ public class RecentLoader extends WrappedAsyncTaskLoader<List<Album>> {
 
     /**
      * Creates the {@link Cursor} used to run the query.
-     * 
+     *
      * @param context The {@link Context} to use.
      * @return The {@link Cursor} used to run the album query.
      */
index d86b41a..a6f957f 100644 (file)
@@ -93,7 +93,7 @@ public class SearchLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 }
 
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, -1);
+                final Song song = new Song(id, songName, artist, album, -1, -1);
 
                 // Add everything up
                 mSongList.add(song);
index cb9e77f..8995ece 100644 (file)
@@ -18,6 +18,7 @@ import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.AudioColumns;
 
 import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
 import com.cyngn.eleven.utils.Lists;
 import com.cyngn.eleven.utils.PreferenceUtils;
 
@@ -30,7 +31,7 @@ import java.util.List;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class SongLoader extends WrappedAsyncTaskLoader<List<Song>> {
+public class SongLoader extends SectionCreator.SimpleListLoader<Song> {
 
     /**
      * The result
@@ -79,8 +80,11 @@ public class SongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                 // Convert the duration into seconds
                 final int durationInSecs = (int) duration / 1000;
 
+                // Copy the Year
+                final int year = mCursor.getInt(5);
+
                 // Create a new song
-                final Song song = new Song(id, songName, artist, album, durationInSecs);
+                final Song song = new Song(id, songName, artist, album, durationInSecs, year);
 
                 // Add everything up
                 mSongList.add(song);
@@ -115,7 +119,9 @@ public class SongLoader extends WrappedAsyncTaskLoader<List<Song>> {
                         /* 3 */
                         AudioColumns.ALBUM,
                         /* 4 */
-                        AudioColumns.DURATION
+                        AudioColumns.DURATION,
+                        /* 5 */
+                        AudioColumns.YEAR,
                 }, mSelection.toString(), null,
                 PreferenceUtils.getInstance(context).getSongSortOrder());
     }
index e5e916a..e5663bd 100644 (file)
@@ -46,6 +46,11 @@ public class Song {
     public int mDuration;
 
     /**
+     * The year the song was recorded
+     */
+    public int mYear;
+
+    /**
      * Constructor of <code>Song</code>
      * 
      * @param songId The Id of the song
@@ -53,14 +58,16 @@ public class Song {
      * @param artistName The song artist
      * @param albumName The song album
      * @param duration The duration of a song in seconds
+     * @param year The year the song was recorded
      */
     public Song(final long songId, final String songName, final String artistName,
-            final String albumName, final int duration) {
+            final String albumName, final int duration, final int year) {
         mSongId = songId;
         mSongName = songName;
         mArtistName = artistName;
         mAlbumName = albumName;
         mDuration = duration;
+        mYear = year;
     }
 
     /**
@@ -75,6 +82,7 @@ public class Song {
         result = prime * result + mDuration;
         result = prime * result + (int) mSongId;
         result = prime * result + (mSongName == null ? 0 : mSongName.hashCode());
+        result = prime * result + mYear;
         return result;
     }
 
@@ -108,6 +116,11 @@ public class Song {
         if (!TextUtils.equals(mSongName, other.mSongName)) {
             return false;
         }
+
+        if (mYear != other.mYear) {
+            return false;
+        }
+
         return true;
     }
 
diff --git a/src/com/cyngn/eleven/sectionadapter/SectionAdapter.java b/src/com/cyngn/eleven/sectionadapter/SectionAdapter.java
new file mode 100644 (file)
index 0000000..0c47d43
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.sectionadapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.cyngn.eleven.R;
+
+import java.util.TreeMap;
+
+/**
+ * This class wraps an ArrayAdapter that implements BasicAdapter and allows Sections to be inserted
+ * into the list.  This wraps the methods for getting the view/indices and returns the section
+ * heads and if it is an underlying item it flows it through the underlying adapter
+ * @param <TItem> The underlying item that is in the array adapter
+ * @param <TArrayAdapter> the arrayadapter that contains TItem and implements BasicAdapter
+ */
+public class SectionAdapter<TItem,
+        TArrayAdapter extends ArrayAdapter<TItem> & SectionAdapter.BasicAdapter>
+        extends BaseAdapter {
+    /**
+     * Basic interface that the adapters implement
+     */
+    public interface BasicAdapter {
+        public void unload();
+        public void buildCache();
+        public void flush();
+    }
+
+    /**
+     * The underlying adapter to wrap
+     */
+    protected TArrayAdapter mUnderlyingAdapter;
+
+    /**
+     * A map of external position to the String to use as the header
+     */
+    protected TreeMap<Integer, String> mSectionHeaders;
+
+    /**
+     * {@link Context}
+     */
+    protected final Context mContext;
+
+    /**
+     * Creates a SectionAdapter
+     * @param context The {@link Context} to use.
+     * @param underlyingAdapter the underlying adapter to wrap
+     */
+    public SectionAdapter(final Activity context, final TArrayAdapter underlyingAdapter) {
+        mContext = context;
+        mUnderlyingAdapter = underlyingAdapter;
+        mSectionHeaders = new TreeMap<Integer, String>();
+    }
+
+    /**
+     * Gets the underlying array adapter
+     * @return the underlying array adapter
+     */
+    public TArrayAdapter getUnderlyingAdapter() {
+        return mUnderlyingAdapter;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View getView(final int position, View convertView, final ViewGroup parent) {
+        if (isSectionHeader(position)) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(mContext).inflate(R.layout.list_header, parent, false);
+            }
+
+            TextView header = (TextView)convertView.findViewById(R.id.header);
+            header.setText(mSectionHeaders.get(position));
+        } else {
+            convertView = mUnderlyingAdapter.getView(getInternalPosition(position), convertView, parent);
+        }
+
+        return convertView;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getCount() {
+        return mSectionHeaders.size() + mUnderlyingAdapter.getCount();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object getItem(int position) {
+        if (isSectionHeader(position)) {
+            return mSectionHeaders.get(position);
+        }
+
+        return mUnderlyingAdapter.getItem(getInternalPosition(position));
+    }
+
+    /**
+     * Gets the underlying adapter's item
+     * @param position position to query for
+     * @return the underlying item or null if a section header is queried
+     */
+    public TItem getTItem(int position) {
+        if (isSectionHeader(position)) {
+            return null;
+        }
+
+        return mUnderlyingAdapter.getItem(getInternalPosition(position));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getItemViewType(int position) {
+        if (isSectionHeader(position)) {
+            // use the last view type id as the section header
+            return getViewTypeCount() - 1;
+        }
+
+        return mUnderlyingAdapter.getItemViewType(position);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getViewTypeCount() {
+        // increment view type count by 1 for section headers
+        return mUnderlyingAdapter.getViewTypeCount() + 1;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void notifyDataSetChanged() {
+        super.notifyDataSetChanged();
+
+        mUnderlyingAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void notifyDataSetInvalidated() {
+        super.notifyDataSetInvalidated();
+
+        mUnderlyingAdapter.notifyDataSetInvalidated();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEnabled(int position) {
+        // don't enable clicking/long press for section headers
+        return !isSectionHeader(position);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    /**
+     * Determines whether the item at the position is a section header
+     * @param position position in the overall lis
+     * @return true if a section header
+     */
+    private boolean isSectionHeader(int position) {
+        return mSectionHeaders.containsKey(position);
+    }
+
+    /**
+     * Converts the external position to the internal position.  This is needed to determine
+     * the position to pass into the underlying adapter
+     * @param position external position
+     * @return the internal position
+     */
+    public int getInternalPosition(int position) {
+        if (isSectionHeader(position)) {
+            return -1;
+        }
+
+        int countSectionHeaders = 0;
+
+        for (Integer sectionPosition : mSectionHeaders.keySet()) {
+            if (sectionPosition <= position) {
+                countSectionHeaders++;
+            } else {
+                break;
+            }
+        }
+
+        return position - countSectionHeaders;
+    }
+
+    /**
+     * Sets the data on the adapter
+     * @param data data to set
+     */
+    public void setData(SectionListContainer<TItem> data) {
+        mUnderlyingAdapter.unload();
+
+        if (data.mSectionIndices == null) {
+            mSectionHeaders.clear();
+        } else {
+            mSectionHeaders = data.mSectionIndices;
+        }
+
+        mUnderlyingAdapter.addAll(data.mListResults);
+
+        mUnderlyingAdapter.buildCache();
+
+        notifyDataSetChanged();
+    }
+
+    /**
+     * unloads the underlying adapter
+     */
+    public void unload() {
+        mUnderlyingAdapter.unload();
+        notifyDataSetChanged();
+    }
+
+    /**
+     * flushes the underlying adapter
+     */
+    public void flush() {
+        mUnderlyingAdapter.flush();
+        notifyDataSetChanged();
+    }
+}
diff --git a/src/com/cyngn/eleven/sectionadapter/SectionCreator.java b/src/com/cyngn/eleven/sectionadapter/SectionCreator.java
new file mode 100644 (file)
index 0000000..21bfce1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.sectionadapter;
+
+import android.content.Context;
+
+import com.cyngn.eleven.loaders.WrappedAsyncTaskLoader;
+import com.cyngn.eleven.utils.SectionCreatorUtils;
+
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * This class wraps a SimpleListLoader and creates header sections for the sections
+ * @param <T> The type of item that is loaded
+ */
+public class SectionCreator<T> extends WrappedAsyncTaskLoader<SectionListContainer<T>> {
+    /**
+     * Simple list loader class that exposes a load method
+     * @param <T> type of item to load
+     */
+    public static abstract class SimpleListLoader<T> {
+        protected Context mContext;
+
+        public SimpleListLoader(Context context) {
+            mContext = context;
+        }
+
+        public Context getContext() {
+            return mContext;
+        }
+
+        /**
+         * method to load the list in the background
+         * @return
+         */
+        public abstract List<T> loadInBackground();
+    }
+
+    private SimpleListLoader<T> mLoader;
+    private SectionCreatorUtils.IItemCompare<T> mComparator;
+
+    /**
+     * Creates a SectionCreator object which loads @loader
+     * @param context The {@link Context} to use.
+     * @param loader loader to wrap
+     * @param comparator the comparison object to run to create the sections
+     */
+    public SectionCreator(Context context, SimpleListLoader<T> loader,
+                          SectionCreatorUtils.IItemCompare<T> comparator) {
+        super(context);
+        mLoader = loader;
+        mComparator = comparator;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SectionListContainer loadInBackground() {
+        List<T> results = mLoader.loadInBackground();
+        TreeMap<Integer, String> sectionHeaders = null;
+
+        if (mComparator != null) {
+            sectionHeaders = SectionCreatorUtils.createSections(results, mComparator);
+        }
+
+        return new SectionListContainer(sectionHeaders, results);
+    }
+}
diff --git a/src/com/cyngn/eleven/sectionadapter/SectionListContainer.java b/src/com/cyngn/eleven/sectionadapter/SectionListContainer.java
new file mode 100644 (file)
index 0000000..56eb169
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.sectionadapter;
+
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * Simple Container that contains a list of T items as well as the map of section information
+ * @param <T> the type of item that the list contains
+ */
+public class SectionListContainer<T> {
+    public TreeMap<Integer, String> mSectionIndices;
+    public List<T> mListResults;
+
+    public SectionListContainer(TreeMap<Integer, String> sectionIndices, List<T> results) {
+        mSectionIndices = sectionIndices;
+        mListResults = results;
+    }
+}
index ebeb566..f36033a 100644 (file)
@@ -19,7 +19,6 @@ import android.os.SystemClock;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
-import android.text.TextUtils;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.LayoutInflater;
@@ -37,7 +36,6 @@ import android.widget.GridView;
 import android.widget.ListView;
 import android.widget.TextView;
 
-import com.cyngn.eleven.Config;
 import com.cyngn.eleven.MusicStateListener;
 import com.cyngn.eleven.R;
 import com.cyngn.eleven.adapters.AlbumAdapter;
@@ -48,21 +46,23 @@ import com.cyngn.eleven.menu.DeleteDialog;
 import com.cyngn.eleven.menu.FragmentMenuItems;
 import com.cyngn.eleven.model.Album;
 import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
+import com.cyngn.eleven.sectionadapter.SectionListContainer;
 import com.cyngn.eleven.ui.activities.BaseActivity;
 import com.cyngn.eleven.utils.ApolloUtils;
 import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.utils.NavUtils;
 import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.utils.SectionCreatorUtils;
 import com.viewpagerindicator.TitlePageIndicator;
 
-import java.util.List;
-
 /**
  * This class is used to display all of the albums on a user's device.
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Album>>,
+public class AlbumFragment extends Fragment implements LoaderCallbacks<SectionListContainer<Album>>,
         OnScrollListener, OnItemClickListener, MusicStateListener {
 
     /**
@@ -88,7 +88,7 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
     /**
      * The adapter for the grid
      */
-    private AlbumAdapter mAdapter;
+    private SectionAdapter<Album, AlbumAdapter> mAdapter;
 
     /**
      * The grid view
@@ -139,7 +139,9 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
         } else {
             layout = R.layout.grid_items_normal;
         }
-        mAdapter = new AlbumAdapter(getActivity(), layout);
+
+        AlbumAdapter adapter = new AlbumAdapter(getActivity(), layout);
+        mAdapter = new SectionAdapter<Album, AlbumAdapter>(getActivity(), adapter);
     }
 
     /**
@@ -191,7 +193,7 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
         // Get the position of the selected item
         final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
         // Create a new album
-        mAlbum = mAdapter.getItem(info.position);
+        mAlbum = mAdapter.getTItem(info.position);
         // Create a list of the album's songs
         mAlbumList = MusicUtils.getSongListForAlbum(getActivity(), mAlbum.mAlbumId);
 
@@ -264,9 +266,9 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
         // Pause disk cache access to ensure smoother scrolling
         if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
                 || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
-            mAdapter.setPauseDiskCache(true);
+            mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
         } else {
-            mAdapter.setPauseDiskCache(false);
+            mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
             mAdapter.notifyDataSetChanged();
         }
     }
@@ -277,7 +279,7 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
     @Override
     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
             final long id) {
-        mAlbum = mAdapter.getItem(position);
+        mAlbum = mAdapter.getTItem(position);
         NavUtils.openAlbumProfile(getActivity(), mAlbum.mAlbumName, mAlbum.mArtistName, mAlbum.mAlbumId);
     }
 
@@ -285,17 +287,25 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
      * {@inheritDoc}
      */
     @Override
-    public Loader<List<Album>> onCreateLoader(final int id, final Bundle args) {
-        return new AlbumLoader(getActivity());
+    public Loader<SectionListContainer<Album>> onCreateLoader(final int id, final Bundle args) {
+        // only show section headers in the simple and detailed layout
+        SectionCreatorUtils.IItemCompare<Album> comparator = null;
+
+        if (isSimpleLayout() || isDetailedLayout()) {
+            comparator = SectionCreatorUtils.createAlbumComparison(getActivity());
+        }
+
+        return new SectionCreator<Album>(getActivity(), new AlbumLoader(getActivity()), comparator);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoadFinished(final Loader<List<Album>> loader, final List<Album> data) {
+    public void onLoadFinished(final Loader<SectionListContainer<Album>> loader,
+                               final SectionListContainer<Album> data) {
         // Check for any errors
-        if (data.isEmpty()) {
+        if (data.mListResults.isEmpty()) {
             // Set the empty text
             final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
             empty.setText(getString(R.string.empty_music));
@@ -307,21 +317,15 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
             return;
         }
 
-        // Start fresh
-        mAdapter.unload();
-        // Add the data to the adpater
-        for (final Album album : data) {
-            mAdapter.add(album);
-        }
-        // Build the cache
-        mAdapter.buildCache();
+        // Set the data
+        mAdapter.setData(data);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoaderReset(final Loader<List<Album>> loader) {
+    public void onLoaderReset(final Loader<SectionListContainer<Album>> loader) {
         // Clear the data in the adapter
         mAdapter.unload();
     }
@@ -352,7 +356,7 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
             return 0;
         }
         for (int i = 0; i < mAdapter.getCount(); i++) {
-            if (mAdapter.getItem(i).mAlbumId == albumId) {
+            if (mAdapter.getTItem(i).mAlbumId == albumId) {
                 return i;
             }
         }
@@ -423,7 +427,7 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
         mListView.setAdapter(mAdapter);
         // Set up the helpers
         initAbsListView(mListView);
-        mAdapter.setTouchPlay(true);
+        mAdapter.getUnderlyingAdapter().setTouchPlay(true);
     }
 
     /**
@@ -438,14 +442,14 @@ public class AlbumFragment extends Fragment implements LoaderCallbacks<List<Albu
         initAbsListView(mGridView);
         if (ApolloUtils.isLandscape(getActivity())) {
             if (isDetailedLayout()) {
-                mAdapter.setLoadExtraData(true);
+                mAdapter.getUnderlyingAdapter().setLoadExtraData(true);
                 mGridView.setNumColumns(TWO);
             } else {
                 mGridView.setNumColumns(FOUR);
             }
         } else {
             if (isDetailedLayout()) {
-                mAdapter.setLoadExtraData(true);
+                mAdapter.getUnderlyingAdapter().setLoadExtraData(true);
                 mGridView.setNumColumns(ONE);
             } else {
                 mGridView.setNumColumns(TWO);
index c007a03..467b481 100644 (file)
@@ -45,11 +45,15 @@ import com.cyngn.eleven.menu.DeleteDialog;
 import com.cyngn.eleven.menu.FragmentMenuItems;
 import com.cyngn.eleven.model.Artist;
 import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
+import com.cyngn.eleven.sectionadapter.SectionListContainer;
 import com.cyngn.eleven.ui.activities.BaseActivity;
 import com.cyngn.eleven.utils.ApolloUtils;
 import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.utils.NavUtils;
 import com.cyngn.eleven.utils.PreferenceUtils;
+import com.cyngn.eleven.utils.SectionCreatorUtils;
 import com.viewpagerindicator.TitlePageIndicator;
 
 import java.util.List;
@@ -59,7 +63,7 @@ import java.util.List;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Artist>>,
+public class ArtistFragment extends Fragment implements LoaderCallbacks<SectionListContainer<Artist>>,
         OnScrollListener, OnItemClickListener, MusicStateListener {
 
     /**
@@ -85,7 +89,7 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
     /**
      * The adapter for the grid
      */
-    private ArtistAdapter mAdapter;
+    private SectionAdapter<Artist, ArtistAdapter> mAdapter;
 
     /**
      * The grid view
@@ -143,7 +147,9 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
         } else {
             layout = R.layout.grid_items_normal;
         }
-        mAdapter = new ArtistAdapter(getActivity(), layout);
+
+        ArtistAdapter adapter = new ArtistAdapter(getActivity(), layout);
+        mAdapter = new SectionAdapter<Artist, ArtistAdapter>(getActivity(), adapter);
     }
 
     /**
@@ -195,7 +201,7 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
         // Get the position of the selected item
         final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
         // Creat a new model
-        mArtist = mAdapter.getItem(info.position);
+        mArtist = mAdapter.getTItem(info.position);
         // Create a list of the artist's songs
         mArtistList = MusicUtils.getSongListForArtist(getActivity(), mArtist.mArtistId);
 
@@ -260,9 +266,9 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
         // Pause disk cache access to ensure smoother scrolling
         if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
                 || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
-            mAdapter.setPauseDiskCache(true);
+            mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
         } else {
-            mAdapter.setPauseDiskCache(false);
+            mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
             mAdapter.notifyDataSetChanged();
         }
     }
@@ -273,7 +279,7 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
     @Override
     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
             final long id) {
-        mArtist = mAdapter.getItem(position);
+        mArtist = mAdapter.getTItem(position);
         NavUtils.openArtistProfile(getActivity(), mArtist.mArtistName);
     }
 
@@ -281,17 +287,25 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
      * {@inheritDoc}
      */
     @Override
-    public Loader<List<Artist>> onCreateLoader(final int id, final Bundle args) {
-        return new ArtistLoader(getActivity());
+    public Loader<SectionListContainer<Artist>> onCreateLoader(final int id, final Bundle args) {
+        SectionCreatorUtils.IItemCompare<Artist> comparator = null;
+
+        // only show section headers in the simple and detailed layout
+        if (isSimpleLayout() || isDetailedLayout()) {
+            comparator = SectionCreatorUtils.createArtistComparison(getActivity());
+        }
+
+        return new SectionCreator<Artist>(getActivity(), new ArtistLoader(getActivity()), comparator);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoadFinished(final Loader<List<Artist>> loader, final List<Artist> data) {
+    public void onLoadFinished(final Loader<SectionListContainer<Artist>> loader,
+                               final SectionListContainer<Artist> data) {
         // Check for any errors
-        if (data.isEmpty()) {
+        if (data.mListResults.isEmpty()) {
             // Set the empty text
             final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
             empty.setText(getString(R.string.empty_music));
@@ -303,21 +317,15 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
             return;
         }
 
-        // Start fresh
-        mAdapter.unload();
-        // Add the data to the adpater
-        for (final Artist artist : data) {
-            mAdapter.add(artist);
-        }
-        // Build the cache
-        mAdapter.buildCache();
+        // Set the data
+        mAdapter.setData(data);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoaderReset(final Loader<List<Artist>> loader) {
+    public void onLoaderReset(final Loader<SectionListContainer<Artist>> loader) {
         // Clear the data in the adapter
         mAdapter.unload();
     }
@@ -348,7 +356,7 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
             return 0;
         }
         for (int i = 0; i < mAdapter.getCount(); i++) {
-            if (mAdapter.getItem(i).mArtistId == artistId) {
+            if (mAdapter.getTItem(i).mArtistId == artistId) {
                 return i;
             }
         }
@@ -433,14 +441,14 @@ public class ArtistFragment extends Fragment implements LoaderCallbacks<List<Art
         initAbsListView(mGridView);
         if (ApolloUtils.isLandscape(getActivity())) {
             if (isDetailedLayout()) {
-                mAdapter.setLoadExtraData(true);
+                mAdapter.getUnderlyingAdapter().setLoadExtraData(true);
                 mGridView.setNumColumns(TWO);
             } else {
                 mGridView.setNumColumns(FOUR);
             }
         } else {
             if (isDetailedLayout()) {
-                mAdapter.setLoadExtraData(true);
+                mAdapter.getUnderlyingAdapter().setLoadExtraData(true);
                 mGridView.setNumColumns(ONE);
             } else {
                 mGridView.setNumColumns(TWO);
index c4684e1..5ab4384 100644 (file)
@@ -58,7 +58,7 @@ import java.util.List;
 /**
  * This class is used to display all of the recently listened to albums by the
  * user.
- * 
+ *
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
 public class RecentFragment extends Fragment implements LoaderCallbacks<List<Album>>,
@@ -146,7 +146,7 @@ public class RecentFragment extends Fragment implements LoaderCallbacks<List<Alb
      */
     @Override
     public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
-            final Bundle savedInstanceState) {
+                             final Bundle savedInstanceState) {
         // The View for the fragment's UI
         if (isSimpleLayout()) {
             mRootView = (ViewGroup)inflater.inflate(R.layout.list_base, null);
@@ -184,7 +184,7 @@ public class RecentFragment extends Fragment implements LoaderCallbacks<List<Alb
      */
     @Override
     public void onCreateContextMenu(final ContextMenu menu, final View v,
-            final ContextMenuInfo menuInfo) {
+                                    final ContextMenuInfo menuInfo) {
         super.onCreateContextMenu(menu, v, menuInfo);
 
         // Get the position of the selected item
@@ -284,7 +284,7 @@ public class RecentFragment extends Fragment implements LoaderCallbacks<List<Alb
      */
     @Override
     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
-            final long id) {
+                            final long id) {
         mAlbum = mAdapter.getItem(position);
         NavUtils.openAlbumProfile(getActivity(), mAlbum.mAlbumName, mAlbum.mArtistName, mAlbum.mAlbumId);
     }
@@ -339,7 +339,7 @@ public class RecentFragment extends Fragment implements LoaderCallbacks<List<Alb
      */
     @Override
     public void onScroll(final AbsListView view, final int firstVisibleItem,
-            final int visibleItemCount, final int totalItemCount) {
+                         final int visibleItemCount, final int totalItemCount) {
         // Nothing to do
     }
 
@@ -365,7 +365,7 @@ public class RecentFragment extends Fragment implements LoaderCallbacks<List<Alb
 
     /**
      * Sets up various helpers for both the list and grid
-     * 
+     *
      * @param list The list or grid
      */
     private void initAbsListView(final AbsListView list) {
index bb8dece..bd355b3 100644 (file)
@@ -12,6 +12,7 @@
 package com.cyngn.eleven.ui.fragments;
 
 import android.app.Activity;
+import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -42,9 +43,13 @@ import com.cyngn.eleven.menu.FragmentMenuItems;
 import com.cyngn.eleven.model.Song;
 import com.cyngn.eleven.provider.FavoritesStore;
 import com.cyngn.eleven.recycler.RecycleHolder;
+import com.cyngn.eleven.sectionadapter.SectionAdapter;
+import com.cyngn.eleven.sectionadapter.SectionCreator;
+import com.cyngn.eleven.sectionadapter.SectionListContainer;
 import com.cyngn.eleven.ui.activities.BaseActivity;
 import com.cyngn.eleven.utils.MusicUtils;
 import com.cyngn.eleven.utils.NavUtils;
+import com.cyngn.eleven.utils.SectionCreatorUtils;
 import com.viewpagerindicator.TitlePageIndicator;
 
 import java.util.List;
@@ -54,7 +59,7 @@ import java.util.List;
  * 
  * @author Andrew Neal (andrewdneal@gmail.com)
  */
-public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>>,
+public class SongFragment extends Fragment implements LoaderCallbacks<SectionListContainer<Song>>,
         OnItemClickListener, MusicStateListener {
 
     /**
@@ -75,7 +80,7 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
     /**
      * The adapter for the list
      */
-    private SongAdapter mAdapter;
+    private SectionAdapter<Song, SongAdapter> mAdapter;
 
     /**
      * The list view
@@ -130,7 +135,7 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
     public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         // Create the adpater
-        mAdapter = new SongAdapter(getActivity(), R.layout.list_item_simple);
+        mAdapter = new SectionAdapter<Song, SongAdapter>(getActivity(), new SongAdapter(getActivity(), R.layout.list_item_simple));
     }
 
     /**
@@ -177,7 +182,7 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
         final AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
         mSelectedPosition = info.position;
         // Creat a new song
-        mSong = mAdapter.getItem(mSelectedPosition);
+        mSong = mAdapter.getTItem(mSelectedPosition);
         mSelectedId = mSong.mSongId;
         mSongName = mSong.mSongName;
         mAlbumName = mSong.mAlbumName;
@@ -272,9 +277,10 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
     @Override
     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
             final long id) {
+        int internalPosition = mAdapter.getInternalPosition(position);
         Cursor cursor = SongLoader.makeSongCursor(getActivity());
         final long[] list = MusicUtils.getSongListForCursor(cursor);
-        MusicUtils.playAll(getActivity(), list, position, false);
+        MusicUtils.playAll(getActivity(), list, internalPosition, false);
         cursor.close();
         cursor = null;
     }
@@ -283,17 +289,27 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
      * {@inheritDoc}
      */
     @Override
-    public Loader<List<Song>> onCreateLoader(final int id, final Bundle args) {
-        return new SongLoader(getActivity());
+    public Loader<SectionListContainer<Song>> onCreateLoader(final int id, final Bundle args) {
+        // get the context
+        Context context = getActivity();
+
+        // create the underlying song loader
+        SongLoader songLoader = new SongLoader(context);
+
+        // get the song comparison method to create the headers with
+        SectionCreatorUtils.IItemCompare<Song> songComparison = SectionCreatorUtils.createSongComparison(context);
+
+        // return the wrapped section creator
+        return new SectionCreator<Song>(context, songLoader, songComparison);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoadFinished(final Loader<List<Song>> loader, final List<Song> data) {
+    public void onLoadFinished(final Loader<SectionListContainer<Song>> loader, final SectionListContainer<Song> data) {
         // Check for any errors
-        if (data.isEmpty()) {
+        if (data.mListResults.isEmpty()) {
             // Set the empty text
             final TextView empty = (TextView)mRootView.findViewById(R.id.empty);
             empty.setText(getString(R.string.empty_music));
@@ -301,21 +317,15 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
             return;
         }
 
-        // Start fresh
-        mAdapter.unload();
-        // Add the data to the adpater
-        for (final Song song : data) {
-            mAdapter.add(song);
-        }
-        // Build the cache
-        mAdapter.buildCache();
+        // Set the data
+        mAdapter.setData(data);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void onLoaderReset(final Loader<List<Song>> loader) {
+    public void onLoaderReset(final Loader<SectionListContainer<Song>> loader) {
         // Clear the data in the adapter
         mAdapter.unload();
     }
@@ -342,7 +352,7 @@ public class SongFragment extends Fragment implements LoaderCallbacks<List<Song>
             return 0;
         }
         for (int i = 0; i < mAdapter.getCount(); i++) {
-            if (mAdapter.getItem(i).mSongId == trackId) {
+            if (mAdapter.getTItem(i).mSongId == trackId) {
                 return i;
             }
         }
diff --git a/src/com/cyngn/eleven/utils/LocaleUtils.java b/src/com/cyngn/eleven/utils/LocaleUtils.java
new file mode 100644 (file)
index 0000000..5ef06c0
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.cyngn.eleven.utils;
+
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.Character.UnicodeBlock;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Set;
+
+import libcore.icu.AlphabeticIndex;
+import libcore.icu.AlphabeticIndex.ImmutableIndex;
+import libcore.icu.Transliterator;
+
+/**
+ * This utility class provides specialized handling for locale specific
+ * information: labels, name lookup keys.
+ *
+ * This class has been modified from ContactLocaleUtils.java for now to rip out
+ * Chinese/Japanese specific Alphabetic Indexers because the MediaProvider's sort
+ * is using a Collator sort which can result in confusing behavior, so for now we will
+ * simplify and batch up those results until we later support our own internal databases
+ * An example of what This is, if we have songs "Able", "Xylophone" and "上" in
+ * simplified chinese language The media provider would give it to us in that order sorted,
+ * but the ICU lib would return "A", "X", "S".  Unless we write our own db or do our own sort
+ * there is no good easy solution
+ */
+public class LocaleUtils {
+    public static final String TAG = "MusicLocale";
+
+    public static final Locale LOCALE_ARABIC = new Locale("ar");
+    public static final Locale LOCALE_GREEK = new Locale("el");
+    public static final Locale LOCALE_HEBREW = new Locale("he");
+    // Ukrainian labels are superset of Russian
+    public static final Locale LOCALE_UKRAINIAN = new Locale("uk");
+    public static final Locale LOCALE_THAI = new Locale("th");
+
+    /**
+     * This class is the default implementation and should be the base class
+     * for other locales.
+     *
+     * sortKey: same as name
+     * nameLookupKeys: none
+     * labels: uses ICU AlphabeticIndex for labels and extends by labeling
+     *     phone numbers "#".  Eg English labels are: [A-Z], #, " "
+     */
+    private static class LocaleUtilsBase {
+        private static final String EMPTY_STRING = "";
+        private static final String NUMBER_STRING = "#";
+
+        protected final ImmutableIndex mAlphabeticIndex;
+        private final int mAlphabeticIndexBucketCount;
+        private final int mNumberBucketIndex;
+
+        public LocaleUtilsBase(Locale locale) {
+            // AlphabeticIndex.getBucketLabel() uses a binary search across
+            // the entire label set so care should be taken about growing this
+            // set too large. The following set determines for which locales
+            // we will show labels other than your primary locale. General rules
+            // of thumb for adding a locale: should be a supported locale; and
+            // should not be included if from a name it is not deterministic
+            // which way to label it (so eg Chinese cannot be added because
+            // the labeling of a Chinese character varies between Simplified,
+            // Traditional, and Japanese locales). Use English only for all
+            // Latin based alphabets. Ukrainian is chosen for Cyrillic because
+            // its alphabet is a superset of Russian.
+            mAlphabeticIndex = new AlphabeticIndex(locale)
+                    .setMaxLabelCount(300)
+                    .addLabels(Locale.ENGLISH)
+                    .addLabels(Locale.JAPANESE)
+                    .addLabels(Locale.KOREAN)
+                    .addLabels(LOCALE_THAI)
+                    .addLabels(LOCALE_ARABIC)
+                    .addLabels(LOCALE_HEBREW)
+                    .addLabels(LOCALE_GREEK)
+                    .addLabels(LOCALE_UKRAINIAN)
+                    .getImmutableIndex();
+            mAlphabeticIndexBucketCount = mAlphabeticIndex.getBucketCount();
+            mNumberBucketIndex = mAlphabeticIndexBucketCount - 1;
+        }
+
+        public String getSortKey(String name) {
+            return name;
+        }
+
+        /**
+         * Returns the bucket index for the specified string. AlphabeticIndex
+         * sorts strings into buckets numbered in order from 0 to N, where the
+         * exact value of N depends on how many representative index labels are
+         * used in a particular locale. This routine adds one additional bucket
+         * for phone numbers. It attempts to detect phone numbers and shifts
+         * the bucket indexes returned by AlphabeticIndex in order to make room
+         * for the new # bucket, so the returned range becomes 0 to N+1.
+         */
+        public int getBucketIndex(String name) {
+            boolean prefixIsNumeric = false;
+            final int length = name.length();
+            int offset = 0;
+            while (offset < length) {
+                int codePoint = Character.codePointAt(name, offset);
+                // Ignore standard phone number separators and identify any
+                // string that otherwise starts with a number.
+                if (Character.isDigit(codePoint)) {
+                    prefixIsNumeric = true;
+                    break;
+                } else if (!Character.isSpaceChar(codePoint) &&
+                        codePoint != '+' && codePoint != '(' &&
+                        codePoint != ')' && codePoint != '.' &&
+                        codePoint != '-' && codePoint != '#') {
+                    break;
+                }
+                offset += Character.charCount(codePoint);
+            }
+            if (prefixIsNumeric) {
+                return mNumberBucketIndex;
+            }
+
+            final int bucket = mAlphabeticIndex.getBucketIndex(name);
+            if (bucket < 0) {
+                return -1;
+            }
+            if (bucket >= mNumberBucketIndex) {
+                return bucket + 1;
+            }
+            return bucket;
+        }
+
+        /**
+         * Returns the number of buckets in use (one more than AlphabeticIndex
+         * uses, because this class adds a bucket for phone numbers).
+         */
+        public int getBucketCount() {
+            return mAlphabeticIndexBucketCount + 1;
+        }
+
+        /**
+         * Returns the label for the specified bucket index if a valid index,
+         * otherwise returns an empty string. '#' is returned for the phone
+         * number bucket; for all others, the AlphabeticIndex label is returned.
+         */
+        public String getBucketLabel(int bucketIndex) {
+            if (bucketIndex < 0 || bucketIndex >= getBucketCount()) {
+                return EMPTY_STRING;
+            } else if (bucketIndex == mNumberBucketIndex) {
+                return NUMBER_STRING;
+            } else if (bucketIndex > mNumberBucketIndex) {
+                --bucketIndex;
+            }
+            return mAlphabeticIndex.getBucketLabel(bucketIndex);
+        }
+
+        @SuppressWarnings("unused")
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+            return null;
+        }
+
+        public ArrayList<String> getLabels() {
+            final int bucketCount = getBucketCount();
+            final ArrayList<String> labels = new ArrayList<String>(bucketCount);
+            for(int i = 0; i < bucketCount; ++i) {
+                labels.add(getBucketLabel(i));
+            }
+            return labels;
+        }
+    }
+
+    private static LocaleUtils sSingleton;
+
+    private final Locale mLocale;
+    private final LocaleUtilsBase mUtils;
+
+    private LocaleUtils(Locale locale) {
+        if (locale == null) {
+            mLocale = Locale.getDefault();
+        } else {
+            mLocale = locale;
+        }
+        mUtils = new LocaleUtilsBase(mLocale);
+
+        Log.i(TAG, "AddressBook Labels [" + mLocale.toString() + "]: "
+                + getLabels().toString());
+    }
+
+    public boolean isLocale(Locale locale) {
+        return mLocale.equals(locale);
+    }
+
+    public static synchronized LocaleUtils getInstance() {
+        if (sSingleton == null) {
+            sSingleton = new LocaleUtils(null);
+        }
+        return sSingleton;
+    }
+
+    public static synchronized void setLocale(Locale locale) {
+        if (sSingleton == null || !sSingleton.isLocale(locale)) {
+            sSingleton = new LocaleUtils(locale);
+        }
+    }
+
+    public String getSortKey(String name, int nameStyle) {
+        return mUtils.getSortKey(name);
+    }
+
+    public int getBucketIndex(String name) {
+        return mUtils.getBucketIndex(name);
+    }
+
+    public int getBucketCount() {
+        return mUtils.getBucketCount();
+    }
+
+    public String getBucketLabel(int bucketIndex) {
+        return mUtils.getBucketLabel(bucketIndex);
+    }
+
+    public String getLabel(String name) {
+        return getBucketLabel(getBucketIndex(name));
+    }
+
+    public ArrayList<String> getLabels() {
+        return mUtils.getLabels();
+    }
+}
index b466cd4..5080a59 100644 (file)
@@ -73,6 +73,8 @@ public final class MusicUtils {
 
     private static ContentValues[] mContentValuesCache = null;
 
+    private static final int MIN_VALID_YEAR = 1900; // used to remove invalid years from metadata
+
     static {
         mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
         sEmptyList = new long[0];
@@ -1366,4 +1368,48 @@ public final class MusicUtils {
         // Notify the lists to update
         refresh();
     }
+
+    /**
+     * Simple function used to determine if the song/album year is invalid
+     * @param year value to test
+     * @return true if the app considers it valid
+     */
+    public static boolean isInvalidYear(int year) {
+        return year < MIN_VALID_YEAR;
+    }
+
+    /**
+     * A snippet is taken from MediaStore.Audio.keyFor method
+     * This will take a name, removes things like "the", "an", etc
+     * as well as special characters, then find the localized label
+     * @param name Name to get the label of
+     * @param trimName boolean flag to run the trimmer on the name
+     * @return the localized label of the bucket that the name falls into
+     */
+    public static String getLocalizedBucketLetter(String name, boolean trimName) {
+        if (trimName) {
+            name = name.trim().toLowerCase();
+            if (name.startsWith("the ")) {
+                name = name.substring(4);
+            }
+            if (name.startsWith("an ")) {
+                name = name.substring(3);
+            }
+            if (name.startsWith("a ")) {
+                name = name.substring(2);
+            }
+            if (name.endsWith(", the") || name.endsWith(",the") ||
+                    name.endsWith(", an") || name.endsWith(",an") ||
+                    name.endsWith(", a") || name.endsWith(",a")) {
+                name = name.substring(0, name.lastIndexOf(','));
+            }
+            name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
+        }
+
+        if (name.length() > 0) {
+            return LocaleUtils.getInstance().getLabel(name);
+        }
+
+        return null;
+    }
 }
diff --git a/src/com/cyngn/eleven/utils/SectionCreatorUtils.java b/src/com/cyngn/eleven/utils/SectionCreatorUtils.java
new file mode 100644 (file)
index 0000000..8247cb9
--- /dev/null
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2014 Cyanogen, Inc.
+ */
+package com.cyngn.eleven.utils;
+
+import android.content.Context;
+
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.model.Album;
+import com.cyngn.eleven.model.Artist;
+import com.cyngn.eleven.model.Song;
+
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * This Utils class contains code that compares two different items and determines whether
+ * a section should be created
+ */
+public class SectionCreatorUtils {
+    /**
+     * Interface to compare two items and create labels
+     * @param <T> type of item to compare
+     */
+    public static interface IItemCompare<T> {
+        /**
+         * Compares to items and returns a section divider T if there should
+         * be a section divider between first and second
+         * @param first the first element in the list.  If null, it is checking to see
+         *              if we need a divider at the beginning of the list
+         * @param second the second element in the list.
+         * @return String the expected separator label or null if none
+         */
+        public String createSectionSeparator(T first, T second);
+
+        /**
+         * Returns the section label that corresponds to this item
+         * @param item the item
+         * @return the section label that this label falls under
+         */
+        public String createLabel(T item);
+    }
+
+
+    /**
+     * A localized String comparison implementation of IItemCompare
+     * @param <T> the type of item to compare
+     */
+    public static abstract class LocalizedCompare<T> implements IItemCompare<T> {
+        @Override
+        public String createSectionSeparator(T first, T second) {
+            String secondLabel = createLabel(second);
+            if (first == null || !createLabel(first).equals(secondLabel)) {
+                return createLabel(second);
+            }
+
+            return null;
+        }
+
+        @Override
+        public String createLabel(T item) {
+            return MusicUtils.getLocalizedBucketLetter(getString(item), trimName());
+        }
+
+        /**
+         * @return true if we want to trim the name first - apparently artists don't trim
+         * but albums/songs do
+         */
+        public boolean trimName() {
+            return true;
+        }
+
+        public abstract String getString(T item);
+    }
+
+    /**
+     * A simple int comparison implementation of IItemCompare
+     * @param <T> the type of item to compare
+     */
+    public static abstract class IntCompare<T> implements IItemCompare<T> {
+        @Override
+        public String createSectionSeparator(T first, T second) {
+            if (first == null || getInt(first) != getInt(second)) {
+                return createLabel(second);
+            }
+
+            return null;
+        }
+
+        @Override
+        public String createLabel(T item) {
+            return String.valueOf(getInt(item));
+        }
+
+        public abstract int getInt(T item);
+    }
+
+    /**
+     * A Bounded int comparison implementation of IntCompare
+     * Basically this will take ints and determine what bounds it falls into
+     * For example, 1-5 mintes, 5-10 minutes, 10+ minutes
+     * @param <T> the type of item to compare
+     */
+    public static abstract class BoundedIntCompare<T> extends IntCompare<T> {
+        protected Context mContext;
+
+        public BoundedIntCompare(Context context) {
+            mContext = context;
+        }
+
+        protected abstract int getStringId(int value);
+
+        @Override
+        public String createSectionSeparator(T first, T second) {
+            int secondStringId = getStringId(getInt(second));
+            if (first == null || getStringId(getInt(first)) != secondStringId) {
+                return createLabel(secondStringId, second);
+            }
+
+            return null;
+        }
+
+        protected String createLabel(int stringId, T item) {
+            return mContext.getString(stringId);
+        }
+
+        @Override
+        public String createLabel(T item) {
+            return createLabel(getStringId(getInt(item)), item);
+        }
+    }
+
+    /**
+     * This implements BoundedIntCompare and gives duration buckets
+     * @param <T> the type of item to compare
+     */
+    public static abstract class DurationCompare<T> extends BoundedIntCompare<T> {
+        private static final int SECONDS_PER_MINUTE = 60;
+
+        public DurationCompare(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected int getStringId(int value) {
+            if (value < 30) {
+                return R.string.header_less_than_30s;
+            } else if (value < 1 * SECONDS_PER_MINUTE) {
+                return R.string.header_30_to_60_seconds;
+            } else if (value < 2 * SECONDS_PER_MINUTE) {
+                return R.string.header_1_to_2_minutes;
+            } else if (value < 3 * SECONDS_PER_MINUTE) {
+                return R.string.header_2_to_3_minutes;
+            } else if (value < 4 * SECONDS_PER_MINUTE) {
+                return R.string.header_3_to_4_minutes;
+            } else if (value < 5 * SECONDS_PER_MINUTE) {
+                return R.string.header_4_to_5_minutes;
+            } else if (value < 10 * SECONDS_PER_MINUTE) {
+                return R.string.header_5_to_10_minutes;
+            } else if (value < 30 * SECONDS_PER_MINUTE) {
+                return R.string.header_10_to_30_minutes;
+            } else if (value < 60 * SECONDS_PER_MINUTE) {
+                return R.string.header_30_to_60_minutes;
+            }
+
+            return R.string.header_greater_than_60_minutes;
+        }
+    }
+
+    /**
+     * This implements BoundedIntCompare and gives number of songs buckets
+     * @param <T> the type of item to compare
+     */
+    public static abstract class NumberOfSongsCompare<T> extends BoundedIntCompare<T> {
+        public NumberOfSongsCompare(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected int getStringId(int value) {
+            if (value <= 1) {
+                return R.string.header_1_song;
+            } else if (value <= 4) {
+                return R.string.header_2_to_4_songs;
+            } else if (value <= 9) {
+                return R.string.header_5_to_9_songs;
+            }
+
+            return R.string.header_10_plus_songs;
+        }
+    }
+
+    /**
+     * This implements BoundedIntCompare and gives number of albums buckets
+     * @param <T> the type of item to compare
+     */
+    public static abstract class NumberOfAlbumsCompare<T> extends BoundedIntCompare<T> {
+        public NumberOfAlbumsCompare(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected int getStringId(int value) {
+            if (value <= 1) {
+                return R.string.header_1_album;
+            } else if (value <= 4) {
+                return R.string.header_n_albums;
+            }
+
+            return R.string.header_5_plus_albums;
+        }
+
+        @Override
+        public String createSectionSeparator(T first, T second) {
+            boolean returnSeparator = false;
+            if (first == null) {
+                returnSeparator = true;
+            } else {
+                // create a separator if both album counts are different and they are
+                // not greater than 5 albums
+                int firstInt = getInt(first);
+                int secondInt = getInt(second);
+                if (firstInt != secondInt && 
+                        !(firstInt >= 5 && secondInt >= 5)) {
+                    returnSeparator = true;
+                }
+            }
+
+            if (returnSeparator) {
+                return createLabel(second);
+            }
+
+            return null;
+        }
+
+        @Override
+        protected String createLabel(int stringId, T item) {
+            if (stringId == R.string.header_n_albums) {
+                return mContext.getString(stringId, getInt(item));
+            }
+
+            return super.createLabel(stringId, item);
+        }
+    }
+
+    /**
+     * This creates the sections give a list of items and the comparison algorithm
+     * @param list The list of items to analyze
+     * @param comparator The comparison function to use
+     * @param <T> the type of item to compare
+     * @return Creates a TreeMap of indices (if the headers were part of the list) to section labels
+     */
+    public static <T> TreeMap<Integer, String> createSections(final List<T> list,
+                                                              final IItemCompare<T> comparator) {
+        if (list != null && list.size() > 0) {
+            TreeMap<Integer, String> sectionHeaders = new TreeMap<Integer, String>();
+            for (int i = 0; i < list.size(); i++) {
+                T first = (i == 0 ? null : list.get(i - 1));
+                T second = list.get(i);
+
+                String separator = comparator.createSectionSeparator(first, second);
+                if (separator != null) {
+                    // add sectionHeaders.size() to store the indices of the combined list
+                    sectionHeaders.put(sectionHeaders.size() + i, separator);
+                }
+            }
+
+            return sectionHeaders;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns an artist comparison based on the current sort
+     * @param context Context for string generation
+     * @return the artist comparison method
+     */
+    public static IItemCompare<Artist> createArtistComparison(final Context context) {
+        IItemCompare<Artist> sectionCreator = null;
+
+        String sortOrder = PreferenceUtils.getInstance(context).getArtistSortOrder();
+        if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z)
+                || sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A)) {
+            sectionCreator = new SectionCreatorUtils.LocalizedCompare<Artist>() {
+                @Override
+                public String getString(Artist item) {
+                    return item.mArtistName;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS)) {
+            sectionCreator = new SectionCreatorUtils.NumberOfAlbumsCompare<Artist>(context) {
+                @Override
+                public int getInt(Artist item) {
+                    return item.mAlbumNumber;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_SONGS)) {
+            sectionCreator = new NumberOfSongsCompare<Artist>(context) {
+                @Override
+                public int getInt(Artist item) {
+                    return item.mSongNumber;
+                }
+            };
+        }
+
+        return sectionCreator;
+    }
+
+    /**
+     * Returns an album comparison based on the current sort
+     * @param context Context for string generation
+     * @return the album comparison method
+     */
+    public static IItemCompare<Album> createAlbumComparison(final Context context) {
+        IItemCompare<Album> sectionCreator = null;
+
+        String sortOrder = PreferenceUtils.getInstance(context).getAlbumSortOrder();
+        if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z)
+                || sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A)) {
+            sectionCreator = new LocalizedCompare<Album>() {
+                @Override
+                public String getString(Album item) {
+                    return item.mAlbumName;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
+            sectionCreator = new LocalizedCompare<Album>() {
+                @Override
+                public String getString(Album item) {
+                    return item.mArtistName;
+                }
+
+                @Override
+                public boolean trimName() {
+                    return false;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS)) {
+            sectionCreator = new NumberOfSongsCompare<Album>(context) {
+                @Override
+                public int getInt(Album item) {
+                    return item.mSongNumber;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_YEAR)) {
+            sectionCreator = new IntCompare<Album>() {
+                private static final int INVALID_YEAR = -1;
+
+                @Override
+                public int getInt(Album item) {
+                    // if we don't have a year, treat it as invalid
+                    if (item.mYear == null) {
+                        return INVALID_YEAR;
+                    }
+
+                    int year = Integer.valueOf(item.mYear);
+
+                    // if the year is extremely low, treat it as invalid too
+                    if (MusicUtils.isInvalidYear(year)) {
+                        return INVALID_YEAR;
+                    }
+
+                    return year;
+                }
+
+                @Override
+                public String createLabel(Album item) {
+                    if (MusicUtils.isInvalidYear(getInt(item))) {
+                        return context.getString(R.string.header_unknown_year);
+                    }
+
+                    return item.mYear;
+                }
+            };
+        }
+
+        return sectionCreator;
+    }
+
+    /**
+     * Returns an song comparison based on the current sort
+     * @param context Context for string generation
+     * @return the song comparison method
+     */
+    public static IItemCompare<Song> createSongComparison(final Context context) {
+        IItemCompare<Song> sectionCreator = null;
+
+        String sortOrder = PreferenceUtils.getInstance(context).getSongSortOrder();
+
+        // doesn't make sense to have headers for SONG_FILENAME
+        // so we will not return a sectionCreator for that one
+        if (sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z)
+                || sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A)) {
+            sectionCreator = new LocalizedCompare<Song>() {
+                @Override
+                public String getString(Song item) {
+                    return item.mSongName;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
+            sectionCreator = new LocalizedCompare<Song>() {
+                @Override
+                public String getString(Song item) {
+                    return item.mAlbumName;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
+            sectionCreator = new LocalizedCompare<Song>() {
+                @Override
+                public String getString(Song item) {
+                    return item.mArtistName;
+                }
+
+                @Override
+                public boolean trimName() {
+                    return false;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_DURATION)) {
+            sectionCreator = new DurationCompare<Song>(context) {
+                @Override
+                public int getInt(Song item) {
+                    return item.mDuration;
+                }
+            };
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_YEAR)) {
+            sectionCreator = new SectionCreatorUtils.IntCompare<Song>() {
+                @Override
+                public int getInt(Song item) {
+                    return item.mYear;
+                }
+
+                @Override
+                public String createLabel(Song item) {
+                    // I have seen tracks in my library where it would return 0 or 2
+                    // so have this check to return a more friendly label in that case
+                    if (MusicUtils.isInvalidYear(item.mYear)) {
+                        return context.getString(R.string.header_unknown_year);
+                    }
+
+                    return super.createLabel(item);
+                }
+            };
+        }
+
+        return sectionCreator;
+    }
+}