OSDN Git Service

Add improved localized sorting (similar to contacts sorting) to Eleven
authorlinus_lee <llee@cyngn.com>
Thu, 4 Dec 2014 20:30:29 +0000 (12:30 -0800)
committerlinus_lee <llee@cyngn.com>
Fri, 12 Dec 2014 00:30:54 +0000 (16:30 -0800)
Change-Id: I6d06f9e6974a2dc178445ca71c7c7f1355e00804

29 files changed:
Android.mk
AndroidManifest.xml
res/layout/fragment_music_browser_phone.xml
src/com/cyanogenmod/eleven/adapters/ArtistDetailAlbumAdapter.java
src/com/cyanogenmod/eleven/adapters/ArtistDetailSongAdapter.java
src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java
src/com/cyanogenmod/eleven/loaders/AlbumLoader.java
src/com/cyanogenmod/eleven/loaders/ArtistAlbumLoader.java [deleted file]
src/com/cyanogenmod/eleven/loaders/ArtistLoader.java
src/com/cyanogenmod/eleven/loaders/ArtistSongLoader.java [deleted file]
src/com/cyanogenmod/eleven/loaders/SongLoader.java
src/com/cyanogenmod/eleven/loaders/SortedCursor.java
src/com/cyanogenmod/eleven/loaders/TopTracksLoader.java
src/com/cyanogenmod/eleven/locale/HanziToPinyin.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/locale/LocaleChangeReceiver.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/locale/LocaleSet.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/locale/LocaleSetManager.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/locale/LocaleUtils.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/model/Album.java
src/com/cyanogenmod/eleven/model/Artist.java
src/com/cyanogenmod/eleven/model/Song.java
src/com/cyanogenmod/eleven/provider/LocalizedStore.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/provider/MusicDB.java
src/com/cyanogenmod/eleven/provider/MusicPlaybackState.java
src/com/cyanogenmod/eleven/provider/PropertiesStore.java [new file with mode: 0644]
src/com/cyanogenmod/eleven/utils/LocaleUtils.java [deleted file]
src/com/cyanogenmod/eleven/utils/MusicUtils.java
src/com/cyanogenmod/eleven/utils/SectionCreatorUtils.java
src/com/cyanogenmod/eleven/utils/SortUtils.java [deleted file]

index eff217a..278613f 100644 (file)
@@ -11,7 +11,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
     android-common \
     android-visualizer \
     eleven_support_v4 \
-    eleven_recyclerview
+    eleven_recyclerview \
+    guava
 
 LOCAL_PACKAGE_NAME := Eleven
 LOCAL_OVERRIDES_PACKAGES := Music
index e198b99..37122a6 100644 (file)
                 <action android:name="android.media.AUDIO_BECOMING_NOISY" />
             </intent-filter>
         </receiver>
+        <!-- Used to recalculate sorting of songs based on the user's locale -->
+        <receiver android:name=".locale.LocaleChangeReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.LOCALE_CHANGED"/>
+            </intent-filter>
+        </receiver>
         <!-- Music service -->
         <service
             android:name="com.cyanogenmod.eleven.MusicPlaybackService"
index 3801078..cb780d3 100644 (file)
@@ -27,7 +27,7 @@
         <com.viewpagerindicator.TabPageIndicator
             android:id="@+id/fragment_home_phone_pager_titles"
             android:background="@color/tpi_background_color"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="@dimen/tpi_height" />
 
         <android.support.v4.view.ViewPager
index 1b7aed6..4670e0d 100644 (file)
@@ -29,7 +29,7 @@ import android.widget.TextView;
 import com.cyanogenmod.eleven.Config;
 import com.cyanogenmod.eleven.R;
 import com.cyanogenmod.eleven.cache.ImageFetcher;
-import com.cyanogenmod.eleven.loaders.ArtistAlbumLoader;
+import com.cyanogenmod.eleven.loaders.AlbumLoader;
 import com.cyanogenmod.eleven.model.Album;
 import com.cyanogenmod.eleven.utils.ApolloUtils;
 import com.cyanogenmod.eleven.utils.NavUtils;
@@ -130,7 +130,7 @@ implements LoaderCallbacks<List<Album>>, IPopupMenuCallback {
 
     @Override // LoaderCallbacks
     public Loader<List<Album>> onCreateLoader(int id, Bundle args) {
-        return new ArtistAlbumLoader(mActivity, args.getLong(Config.ID));
+        return new AlbumLoader(mActivity, args.getLong(Config.ID));
     }
 
     @Override // LoaderCallbacks
index b2ed587..1da8464 100644 (file)
@@ -17,6 +17,7 @@ package com.cyanogenmod.eleven.adapters;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.provider.MediaStore;
 import android.support.v4.content.Loader;
 import android.view.View;
 import android.widget.ImageView;
@@ -25,7 +26,7 @@ import android.widget.TextView;
 import com.cyanogenmod.eleven.Config;
 import com.cyanogenmod.eleven.R;
 import com.cyanogenmod.eleven.cache.ImageFetcher;
-import com.cyanogenmod.eleven.loaders.ArtistSongLoader;
+import com.cyanogenmod.eleven.loaders.SongLoader;
 import com.cyanogenmod.eleven.model.Song;
 
 import java.util.List;
@@ -45,7 +46,8 @@ public abstract class ArtistDetailSongAdapter extends DetailSongAdapter {
     public Loader<List<Song>> onCreateLoader(int id, Bundle args) {
         onLoading();
         setSourceId(args.getLong(Config.ID));
-        return new ArtistSongLoader(mActivity, getSourceId());
+        final String selection = MediaStore.Audio.AudioColumns.ARTIST_ID + "=" + getSourceId();
+        return new SongLoader(mActivity, selection);
     }
 
     protected Holder newHolder(View root, ImageFetcher fetcher) {
index c35ad00..ab87b3f 100644 (file)
@@ -205,7 +205,7 @@ public class PlaylistWorkerTask extends BitmapWorkerTask<Void, Void, TransitionD
 
             // create a new cursor that takes the playlist cursor and the sorted order
             sortedCursor = new SortedCursor(playlistCursor, order,
-                    MediaStore.Audio.Playlists.Members.AUDIO_ID);
+                    MediaStore.Audio.Playlists.Members.AUDIO_ID, null);
 
             // since this cursor is now wrapped by SortedTracksCursor, remove the reference here
             // so we don't accidentally close it in the finally loop
index b66145a..5532a35 100644 (file)
@@ -15,16 +15,19 @@ package com.cyanogenmod.eleven.loaders;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.net.Uri;
 import android.provider.BaseColumns;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio.AlbumColumns;
 
 import com.cyanogenmod.eleven.model.Album;
+import com.cyanogenmod.eleven.provider.LocalizedStore;
+import com.cyanogenmod.eleven.provider.LocalizedStore.SortParameter;
 import com.cyanogenmod.eleven.sectionadapter.SectionCreator;
 import com.cyanogenmod.eleven.utils.Lists;
+import com.cyanogenmod.eleven.utils.MusicUtils;
 import com.cyanogenmod.eleven.utils.PreferenceUtils;
 import com.cyanogenmod.eleven.utils.SortOrder;
-import com.cyanogenmod.eleven.utils.SortUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -48,12 +51,25 @@ public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
     private Cursor mCursor;
 
     /**
-     * Constructor of <code>AlbumLoader</code>
-     *
+     * Additional selection filter
+     */
+    protected Long mArtistId;
+
+    /**
      * @param context The {@link Context} to use
      */
     public AlbumLoader(final Context context) {
+        this(context, null);
+    }
+
+    /**
+     * @param context The {@link Context} to use
+     * @param artistId The artistId to filter against or null if none
+     */
+    public AlbumLoader(final Context context, final Long artistId) {
         super(context);
+
+        mArtistId = artistId;
     }
 
     /**
@@ -62,7 +78,7 @@ public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
     @Override
     public List<Album> loadInBackground() {
         // Create the Cursor
-        mCursor = makeAlbumCursor(getContext());
+        mCursor = makeAlbumCursor(getContext(), mArtistId);
         // Gather the data
         if (mCursor != null && mCursor.moveToFirst()) {
             do {
@@ -89,6 +105,10 @@ public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
                 // Create a new album
                 final Album album = new Album(id, albumName, artist, songCount, year);
 
+                if (mCursor instanceof SortedCursor) {
+                    album.mBucketLabel = (String)((SortedCursor)mCursor).getExtraData();
+                }
+
                 // Add everything up
                 mAlbumsList.add(album);
             } while (mCursor.moveToNext());
@@ -99,37 +119,40 @@ public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
             mCursor = null;
         }
 
-        // requested album ordering
-        String albumSortOrder = PreferenceUtils.getInstance(mContext).getAlbumSortOrder();
-
-        // run a custom localized sort to try to fit items in to header buckets more nicely
-        if (shouldEvokeCustomSortRoutine(albumSortOrder)) {
-            mAlbumsList = SortUtils.localizeSortList(mAlbumsList, albumSortOrder);
-        }
-
         return mAlbumsList;
     }
 
     /**
-     * Evoke custom sorting routine if the sorting attribute is a String. MediaProvider's sort
-     * can be trusted in other instances
-     * @param sortOrder
-     * @return
+     * For string-based sorts, return the localized store sort parameter, otherwise return null
+     * @param sortOrder the song ordering preference selected by the user
      */
-    private boolean shouldEvokeCustomSortRoutine(String sortOrder) {
-        return sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z) ||
-               sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A) ||
-               sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST);
+    private static LocalizedStore.SortParameter getSortParameter(String sortOrder) {
+        if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_A_Z) ||
+                sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_Z_A)) {
+            return LocalizedStore.SortParameter.Album;
+        } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
+            return LocalizedStore.SortParameter.Artist;
+        }
+
+        return null;
     }
 
     /**
      * Creates the {@link Cursor} used to run the query.
      * 
      * @param context The {@link Context} to use.
+     * @param artistId The artistId we want to find albums for or null if we want all albums
      * @return The {@link Cursor} used to run the album query.
      */
-    public static final Cursor makeAlbumCursor(final Context context) {
-        return context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+    public static final Cursor makeAlbumCursor(final Context context, final Long artistId) {
+        // requested album ordering
+        final String albumSortOrder = PreferenceUtils.getInstance(context).getAlbumSortOrder();
+        Uri uri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
+        if (artistId != null) {
+            uri = MediaStore.Audio.Artists.Albums.getContentUri("external", artistId);
+        }
+
+        Cursor cursor = context.getContentResolver().query(uri,
                 new String[] {
                         /* 0 */
                         BaseColumns._ID,
@@ -141,6 +164,16 @@ public class AlbumLoader extends SectionCreator.SimpleListLoader<Album> {
                         AlbumColumns.NUMBER_OF_SONGS,
                         /* 4 */
                         AlbumColumns.FIRST_YEAR
-                }, null, null, PreferenceUtils.getInstance(context).getAlbumSortOrder());
+                }, null, null, albumSortOrder);
+
+        // if our sort is a localized-based sort, grab localized data from the store
+        final SortParameter sortParameter = getSortParameter(albumSortOrder);
+        if (sortParameter != null && cursor != null) {
+            final boolean descending = MusicUtils.isSortOrderDesending(albumSortOrder);
+            return LocalizedStore.getInstance(context).getLocalizedSort(cursor, BaseColumns._ID,
+                    SortParameter.Album, sortParameter, descending, artistId == null);
+        }
+
+        return cursor;
     }
 }
diff --git a/src/com/cyanogenmod/eleven/loaders/ArtistAlbumLoader.java b/src/com/cyanogenmod/eleven/loaders/ArtistAlbumLoader.java
deleted file mode 100644 (file)
index 74b3dec..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2012 Andrew Neal
- * Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.eleven.loaders;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio.AlbumColumns;
-import android.util.Log;
-
-import com.cyanogenmod.eleven.model.Album;
-import com.cyanogenmod.eleven.utils.ApolloUtils;
-import com.cyanogenmod.eleven.utils.Lists;
-import com.cyanogenmod.eleven.utils.PreferenceUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Used to query {@link MediaStore.Audio.Artists.Albums} and return the albums
- * for a particular artist.
- * 
- * @author Andrew Neal (andrewdneal@gmail.com)
- */
-public class ArtistAlbumLoader extends WrappedAsyncTaskLoader<List<Album>> {
-    private static final String TAG = ArtistAlbumLoader.class.getSimpleName();
-
-    /**
-     * The result
-     */
-    private final ArrayList<Album> mAlbumsList = Lists.newArrayList();
-
-    /**
-     * The {@link Cursor} used to run the query.
-     */
-    private Cursor mCursor;
-
-    /**
-     * The Id of the artist the albums belong to.
-     */
-    private final Long mArtistID;
-
-    /**
-     * Constructor of <code>ArtistAlbumHandler</code>
-     * 
-     * @param context The {@link Context} to use.
-     * @param artistId The Id of the artist the albums belong to.
-     */
-    public ArtistAlbumLoader(final Context context, final Long artistId) {
-        super(context);
-        mArtistID = artistId;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public List<Album> loadInBackground() {
-        // Create the Cursor
-        mCursor = makeArtistAlbumCursor(getContext(), mArtistID);
-        // Gather the dataS
-        if (mCursor != null && mCursor.moveToFirst()) {
-            do {
-                // Copy the album id
-                final long id = mCursor.getLong(0);
-
-                // Copy the album name
-                final String albumName = mCursor.getString(1);
-
-                // Copy the artist name
-                final String artist = mCursor.getString(2);
-
-                // Copy the number of songs
-                final int songCount = mCursor.getInt(3);
-
-                // Copy the release year
-                final String year = mCursor.getString(4);
-
-                // as per designer's request, don't show unknown albums
-                if (MediaStore.UNKNOWN_STRING.equals(albumName)) {
-                    continue;
-                }
-
-                // Create a new album
-                final Album album = new Album(id, albumName, artist, songCount, year);
-
-                // Add everything up
-                mAlbumsList.add(album);
-            } while (mCursor.moveToNext());
-        }
-        // Close the cursor
-        if (mCursor != null) {
-            mCursor.close();
-            mCursor = null;
-        }
-        return mAlbumsList;
-    }
-
-    /**
-     * @param context The {@link Context} to use.
-     * @param artistId The Id of the artist the albums belong to.
-     */
-    public static final Cursor makeArtistAlbumCursor(final Context context, final Long artistId) {
-        try {
-            return context.getContentResolver().query(
-                    MediaStore.Audio.Artists.Albums.getContentUri("external", artistId), new String[] {
-                            /* 0 */
-                            BaseColumns._ID,
-                            /* 1 */
-                            AlbumColumns.ALBUM,
-                            /* 2 */
-                            AlbumColumns.ARTIST,
-                            /* 3 */
-                            AlbumColumns.NUMBER_OF_SONGS,
-                            /* 4 */
-                            AlbumColumns.FIRST_YEAR
-                    }, null, null, PreferenceUtils.getInstance(context).getArtistAlbumSortOrder());
-        } catch(Exception e) {
-            Log.e(TAG, ApolloUtils.formatException("unable to make ArtistAlbum cursor", e));
-            return null;
-        }
-    }
-}
index b5f57d4..e572c96 100644 (file)
@@ -15,16 +15,17 @@ package com.cyanogenmod.eleven.loaders;
 
 import android.content.Context;
 import android.database.Cursor;
-import android.provider.BaseColumns;
 import android.provider.MediaStore;
-import android.provider.MediaStore.Audio.ArtistColumns;
+import android.provider.MediaStore.Audio.Artists;
 
 import com.cyanogenmod.eleven.model.Artist;
+import com.cyanogenmod.eleven.provider.LocalizedStore;
+import com.cyanogenmod.eleven.provider.LocalizedStore.SortParameter;
 import com.cyanogenmod.eleven.sectionadapter.SectionCreator;
 import com.cyanogenmod.eleven.utils.Lists;
+import com.cyanogenmod.eleven.utils.MusicUtils;
 import com.cyanogenmod.eleven.utils.PreferenceUtils;
 import com.cyanogenmod.eleven.utils.SortOrder;
-import com.cyanogenmod.eleven.utils.SortUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -86,6 +87,10 @@ public class ArtistLoader extends SectionCreator.SimpleListLoader<Artist> {
                 // Create a new artist
                 final Artist artist = new Artist(id, artistName, songCount, albumCount);
 
+                if (mCursor instanceof SortedCursor) {
+                    artist.mBucketLabel = (String)((SortedCursor)mCursor).getExtraData();
+                }
+
                 mArtistsList.add(artist);
             } while (mCursor.moveToNext());
         }
@@ -95,27 +100,21 @@ public class ArtistLoader extends SectionCreator.SimpleListLoader<Artist> {
             mCursor = null;
         }
 
-        // requested artist ordering
-        String artistSortOrder = PreferenceUtils.getInstance(mContext).getArtistSortOrder();
-        // run a custom localized sort to try to fit items in to header buckets more nicely
-        if (shouldEvokeCustomSortRoutine(artistSortOrder)) {
-            mArtistsList = SortUtils.localizeSortList(mArtistsList, artistSortOrder);
-        }
-
         return mArtistsList;
     }
 
     /**
-     * Evoke custom sorting routine if the sorting attribute is a String. MediaProvider's sort
-     * can be trusted in other instances
-     * @param sortOrder
-     * @return
+     * For string-based sorts, return the localized store sort parameter, otherwise return null
+     * @param sortOrder the song ordering preference selected by the user
      */
-    private boolean shouldEvokeCustomSortRoutine(String sortOrder) {
-        return sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z) ||
-               sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A);
-    }
+    private static LocalizedStore.SortParameter getSortParameter(String sortOrder) {
+        if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_A_Z) ||
+                sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_Z_A)) {
+            return LocalizedStore.SortParameter.Artist;
+        }
 
+        return null;
+    }
     /**
      * Creates the {@link Cursor} used to run the query.
      * 
@@ -123,16 +122,29 @@ public class ArtistLoader extends SectionCreator.SimpleListLoader<Artist> {
      * @return The {@link Cursor} used to run the artist query.
      */
     public static final Cursor makeArtistCursor(final Context context) {
-        return context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+        // requested artist ordering
+        final String artistSortOrder = PreferenceUtils.getInstance(context).getArtistSortOrder();
+
+        Cursor cursor = context.getContentResolver().query(Artists.EXTERNAL_CONTENT_URI,
                 new String[] {
                         /* 0 */
-                        BaseColumns._ID,
+                        Artists._ID,
                         /* 1 */
-                        ArtistColumns.ARTIST,
+                        Artists.ARTIST,
                         /* 2 */
-                        ArtistColumns.NUMBER_OF_ALBUMS,
+                        Artists.NUMBER_OF_ALBUMS,
                         /* 3 */
-                        ArtistColumns.NUMBER_OF_TRACKS
-                }, null, null, PreferenceUtils.getInstance(context).getArtistSortOrder());
+                        Artists.NUMBER_OF_TRACKS
+                }, null, null, artistSortOrder);
+
+        // if our sort is a localized-based sort, grab localized data from the store
+        final SortParameter sortParameter = getSortParameter(artistSortOrder);
+        if (sortParameter != null && cursor != null) {
+            final boolean descending = MusicUtils.isSortOrderDesending(artistSortOrder);
+            return LocalizedStore.getInstance(context).getLocalizedSort(cursor, Artists._ID,
+                    SortParameter.Artist, sortParameter, descending, true);
+        }
+
+        return cursor;
     }
 }
diff --git a/src/com/cyanogenmod/eleven/loaders/ArtistSongLoader.java b/src/com/cyanogenmod/eleven/loaders/ArtistSongLoader.java
deleted file mode 100644 (file)
index 9941bfd..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2012 Andrew Neal
- * Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.eleven.loaders;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.BaseColumns;
-import android.provider.MediaStore;
-import android.provider.MediaStore.Audio.AudioColumns;
-
-import com.cyanogenmod.eleven.model.Song;
-import com.cyanogenmod.eleven.utils.Lists;
-import com.cyanogenmod.eleven.utils.PreferenceUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Used to query {@link MediaStore.Audio.Media.EXTERNAL_CONTENT_URI} and return
- * the songs for a particular artist.
- * 
- * @author Andrew Neal (andrewdneal@gmail.com)
- */
-public class ArtistSongLoader extends WrappedAsyncTaskLoader<List<Song>> {
-
-    /**
-     * The result
-     */
-    private final ArrayList<Song> mSongList = Lists.newArrayList();
-
-    /**
-     * The {@link Cursor} used to run the query.
-     */
-    private Cursor mCursor;
-
-    /**
-     * The Id of the artist the songs belong to.
-     */
-    private final Long mArtistID;
-
-    /**
-     * Constructor of <code>ArtistSongLoader</code>
-     * 
-     * @param context The {@link Context} to use.
-     * @param artistId The Id of the artist the songs belong to.
-     */
-    public ArtistSongLoader(final Context context, final Long artistId) {
-        super(context);
-        mArtistID = artistId;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public List<Song> loadInBackground() {
-        // Create the Cursor
-        mCursor = makeArtistSongCursor(getContext(), mArtistID);
-        // Gather the data
-        if (mCursor != null && mCursor.moveToFirst()) {
-            do {
-                // Copy the song Id
-                final long id = mCursor.getLong(0);
-
-                // Copy the song name
-                final String songName = mCursor.getString(1);
-
-                // Copy the artist name
-                final String artist = mCursor.getString(2);
-
-                // Copy the album id
-                final long albumId = mCursor.getLong(3);
-
-                // Copy the album name
-                final String album = mCursor.getString(4);
-
-                // Copy the duration
-                final long duration = mCursor.getLong(5);
-
-                // Convert the duration into seconds
-                final int durationInSecs = (int) duration / 1000;
-
-                // Grab the Song Year
-                final int year = mCursor.getInt(6);
-
-                // Create a new song
-                final Song song = new Song(id, songName, artist, album, albumId, durationInSecs, year);
-
-                // Add everything up
-                mSongList.add(song);
-            } while (mCursor.moveToNext());
-        }
-        // Close the cursor
-        if (mCursor != null) {
-            mCursor.close();
-            mCursor = null;
-        }
-        return mSongList;
-    }
-
-    /**
-     * @param context The {@link Context} to use.
-     * @param artistId The Id of the artist the songs belong to.
-     * @return The {@link Cursor} used to run the query.
-     */
-    public static final Cursor makeArtistSongCursor(final Context context, final Long artistId) {
-        // Match the songs up with the artist
-        final StringBuilder selection = new StringBuilder();
-        selection.append(AudioColumns.IS_MUSIC + "=1");
-        selection.append(" AND " + AudioColumns.TITLE + " != ''");
-        selection.append(" AND " + AudioColumns.ARTIST_ID + "=" + artistId);
-        return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
-                new String[] {
-                        /* 0 */
-                        BaseColumns._ID,
-                        /* 1 */
-                        AudioColumns.TITLE,
-                        /* 2 */
-                        AudioColumns.ARTIST,
-                        /* 3 */
-                        AudioColumns.ALBUM_ID,
-                        /* 4 */
-                        AudioColumns.ALBUM,
-                        /* 5 */
-                        AudioColumns.DURATION,
-                        /* 6 */
-                        AudioColumns.YEAR,
-                }, selection.toString(), null,
-                PreferenceUtils.getInstance(context).getArtistSongSortOrder());
-    }
-
-}
index ca1d828..3037c53 100644 (file)
@@ -15,17 +15,18 @@ package com.cyanogenmod.eleven.loaders;
 
 import android.content.Context;
 import android.database.Cursor;
-import android.provider.BaseColumns;
 import android.provider.MediaStore;
-import android.provider.MediaStore.Audio.AudioColumns;
+import android.provider.MediaStore.Audio;
+import android.text.TextUtils;
 
 import com.cyanogenmod.eleven.model.Song;
+import com.cyanogenmod.eleven.provider.LocalizedStore;
+import com.cyanogenmod.eleven.provider.LocalizedStore.SortParameter;
 import com.cyanogenmod.eleven.sectionadapter.SectionCreator;
 import com.cyanogenmod.eleven.utils.Lists;
 import com.cyanogenmod.eleven.utils.MusicUtils;
 import com.cyanogenmod.eleven.utils.PreferenceUtils;
 import com.cyanogenmod.eleven.utils.SortOrder;
-import com.cyanogenmod.eleven.utils.SortUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -49,12 +50,25 @@ public class SongLoader extends SectionCreator.SimpleListLoader<Song> {
     protected Cursor mCursor;
 
     /**
-     * Constructor of <code>SongLoader</code>
-     * 
+     * Additional selection filter
+     */
+    protected String mSelection;
+
+    /**
      * @param context The {@link Context} to use
      */
     public SongLoader(final Context context) {
+        this(context, null);
+    }
+
+    /**
+     * @param context The {@link Context} to use
+     * @param selection Additional selection filter to apply to the loader
+     */
+    public SongLoader(final Context context, final String selection) {
         super(context);
+
+        mSelection = selection;
     }
 
     /**
@@ -96,6 +110,10 @@ public class SongLoader extends SectionCreator.SimpleListLoader<Song> {
                 final Song song = new Song(id, songName, artist, album, albumId,
                                             durationInSecs, year);
 
+                if (mCursor instanceof SortedCursor) {
+                    song.mBucketLabel = (String)((SortedCursor)mCursor).getExtraData();
+                }
+
                 mSongList.add(song);
             } while (mCursor.moveToNext());
         }
@@ -105,36 +123,32 @@ public class SongLoader extends SectionCreator.SimpleListLoader<Song> {
             mCursor = null;
         }
 
-        // requested ordering of songs
-        String songSortOrder = PreferenceUtils.getInstance(mContext).getSongSortOrder();
-
-        // run a custom localized sort to try to fit items in to header buckets more nicely
-        if (shouldEvokeCustomSortRoutine(songSortOrder)) {
-            mSongList = SortUtils.localizeSortList(mSongList, songSortOrder);
-        }
-
         return mSongList;
     }
 
     /**
-     * We are choosing to custom sort the song list for a cleaner look on the UI side for a few
-     * sort options
-     * @param sortOrder the song ordering preference selected by the user
-     * @return
+     * Gets the cursor for the loader - can be overriden
+     * @return cursor to load
      */
-    private boolean shouldEvokeCustomSortRoutine(String sortOrder) {
-        return sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z) ||
-               sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A) ||
-               sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM) ||
-               sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST);
+    protected Cursor getCursor() {
+        return makeSongCursor(mContext, mSelection);
     }
 
     /**
-     * Gets the cursor for the loader - can be overriden
-     * @return cursor to load
+     * For string-based sorts, return the localized store sort parameter, otherwise return null
+     * @param sortOrder the song ordering preference selected by the user
      */
-    protected Cursor getCursor() {
-        return makeSongCursor(mContext, null);
+    private static LocalizedStore.SortParameter getSortParameter(String sortOrder) {
+        if (sortOrder.equals(SortOrder.SongSortOrder.SONG_A_Z) ||
+                sortOrder.equals(SortOrder.SongSortOrder.SONG_Z_A)) {
+            return LocalizedStore.SortParameter.Song;
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
+            return LocalizedStore.SortParameter.Album;
+        } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
+            return LocalizedStore.SortParameter.Artist;
+        }
+
+        return null;
     }
 
     /**
@@ -145,28 +159,54 @@ public class SongLoader extends SectionCreator.SimpleListLoader<Song> {
      * @return The {@link Cursor} used to run the song query.
      */
     public static final Cursor makeSongCursor(final Context context, final String selection) {
+        return makeSongCursor(context, selection, true);
+    }
+
+    /**
+     * Creates the {@link Cursor} used to run the query.
+     *
+     * @param context The {@link Context} to use.
+     * @param selection Additional selection statement to use
+     * @param runSort For localized sorts this can enable/disable the logic for running the
+     *                additional localization sort.  Queries that apply their own sorts can pass
+     *                in false for a boost in perf
+     * @return The {@link Cursor} used to run the song query.
+     */
+    public static final Cursor makeSongCursor(final Context context, final String selection,
+                                              final boolean runSort) {
         String selectionStatement = MusicUtils.MUSIC_ONLY_SELECTION;
-        if (selection != null && !selection.isEmpty()) {
+        if (!TextUtils.isEmpty(selection)) {
             selectionStatement += " AND " + selection;
         }
 
-        return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+        final String songSortOrder = PreferenceUtils.getInstance(context).getSongSortOrder();
+
+        Cursor cursor = context.getContentResolver().query(Audio.Media.EXTERNAL_CONTENT_URI,
                 new String[] {
                         /* 0 */
-                        BaseColumns._ID,
+                        Audio.Media._ID,
                         /* 1 */
-                        AudioColumns.TITLE,
+                        Audio.Media.TITLE,
                         /* 2 */
-                        AudioColumns.ARTIST,
+                        Audio.Media.ARTIST,
                         /* 3 */
-                        AudioColumns.ALBUM_ID,
+                        Audio.Media.ALBUM_ID,
                         /* 4 */
-                        AudioColumns.ALBUM,
+                        Audio.Media.ALBUM,
                         /* 5 */
-                        AudioColumns.DURATION,
+                        Audio.Media.DURATION,
                         /* 6 */
-                        AudioColumns.YEAR,
-                }, selectionStatement, null,
-                PreferenceUtils.getInstance(context).getSongSortOrder());
+                        Audio.Media.YEAR,
+                }, selectionStatement, null, songSortOrder);
+
+        // if our sort is a localized-based sort, grab localized data from the store
+        final SortParameter sortParameter = getSortParameter(songSortOrder);
+        if (runSort && sortParameter != null && cursor != null) {
+            final boolean descending = MusicUtils.isSortOrderDesending(songSortOrder);
+            return LocalizedStore.getInstance(context).getLocalizedSort(cursor, Audio.Media._ID,
+                    SortParameter.Song, sortParameter, descending, TextUtils.isEmpty(selection));
+        }
+
+        return cursor;
     }
 }
index e73b7ce..7c2a5d0 100644 (file)
@@ -19,7 +19,9 @@ import android.database.AbstractCursor;
 import android.database.Cursor;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * This cursor basically wraps a song cursor and is given a list of the order of the ids of the
@@ -33,50 +35,65 @@ public class SortedCursor extends AbstractCursor {
     private ArrayList<Integer> mOrderedPositions;
     // this contains the ids that weren't found in the underlying cursor
     private ArrayList<Long> mMissingIds;
+    // this contains the mapped cursor positions and afterwards the extra ids that weren't found
+    private HashMap<Long, Integer> mMapCursorPositions;
+    // extra we want to store with the cursor
+    private ArrayList<Object> mExtraData;
 
     /**
      * @param cursor to wrap
-     * @param order the list of ids in sorted order to display
+     * @param order the list of unique ids in sorted order to display
      * @param columnName the column name of the id to look up in the internal cursor
      */
-    public SortedCursor(final Cursor cursor, final long[] order, final String columnName) {
+    public SortedCursor(final Cursor cursor, final long[] order, final String columnName,
+            final List<? extends Object> extraData) {
         if (cursor == null) {
             throw new IllegalArgumentException("Non-null cursor is needed");
         }
 
         mCursor = cursor;
-        mMissingIds = buildCursorPositionMapping(order, columnName);
+        mMissingIds = buildCursorPositionMapping(order, columnName, extraData);
     }
 
     /**
      * This function populates mOrderedPositions with the cursor positions in the order based
      * on the order passed in
      * @param order the target order of the internal cursor
+     * @param extraData Extra data we want to add to the cursor
      * @return returns the ids that aren't found in the underlying cursor
      */
-    private ArrayList<Long> buildCursorPositionMapping(final long[] order, final String columnName) {
+    private ArrayList<Long> buildCursorPositionMapping(final long[] order,
+            final String columnName, final List<? extends Object> extraData) {
         ArrayList<Long> missingIds = new ArrayList<Long>();
 
         mOrderedPositions = new ArrayList<Integer>(mCursor.getCount());
+        mExtraData = new ArrayList<Object>();
 
-        HashMap<Long, Integer> mapCursorPositions = new HashMap<Long, Integer>(mCursor.getCount());
+        mMapCursorPositions = new HashMap<Long, Integer>(mCursor.getCount());
         final int idPosition = mCursor.getColumnIndex(columnName);
 
         if (mCursor.moveToFirst()) {
             // first figure out where each of the ids are in the cursor
             do {
-                mapCursorPositions.put(mCursor.getLong(idPosition), mCursor.getPosition());
+                mMapCursorPositions.put(mCursor.getLong(idPosition), mCursor.getPosition());
             } while (mCursor.moveToNext());
 
             // now create the ordered positions to map to the internal cursor given the
             // external sort order
-            for (long id : order) {
-                if (mapCursorPositions.containsKey(id)) {
-                    mOrderedPositions.add(mapCursorPositions.get(id));
+            for (int i = 0; order != null && i < order.length; i++) {
+                final long id = order[i];
+                if (mMapCursorPositions.containsKey(id)) {
+                    mOrderedPositions.add(mMapCursorPositions.get(id));
+                    mMapCursorPositions.remove(id);
+                    if (extraData != null) {
+                        mExtraData.add(extraData.get(i));
+                    }
                 } else {
                     missingIds.add(id);
                 }
             }
+
+            mCursor.moveToFirst();
         }
 
         return missingIds;
@@ -89,6 +106,20 @@ public class SortedCursor extends AbstractCursor {
         return mMissingIds;
     }
 
+    /**
+     * @return the list of ids that were in the underlying cursor but not part of the ordered list
+     */
+    public Collection<Long> getExtraIds() {
+        return mMapCursorPositions.keySet();
+    }
+
+    /**
+     * @return the extra object data that was passed in to be attached to the current row
+     */
+    public Object getExtraData() {
+        return mExtraData.get(getPosition());
+    }
+
     @Override
     public void close() {
         mCursor.close();
index a1d79d0..f1ea033 100644 (file)
@@ -149,10 +149,10 @@ public class TopTracksLoader extends SongLoader {
             selection.append(")");
 
             // get a list of songs with the data given the selection statement
-            Cursor songCursor = makeSongCursor(context, selection.toString());
+            Cursor songCursor = makeSongCursor(context, selection.toString(), false);
             if (songCursor != null) {
                 // now return the wrapped TopTracksCursor to handle sorting given order
-                return new SortedCursor(songCursor, order, BaseColumns._ID);
+                return new SortedCursor(songCursor, order, BaseColumns._ID, null);
             }
         }
 
diff --git a/src/com/cyanogenmod/eleven/locale/HanziToPinyin.java b/src/com/cyanogenmod/eleven/locale/HanziToPinyin.java
new file mode 100644 (file)
index 0000000..895bbab
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2011 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.cyanogenmod.eleven.locale;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+import libcore.icu.Transliterator;
+
+/**
+ * An object to convert Chinese character to its corresponding pinyin string.
+ * For characters with multiple possible pinyin string, only one is selected
+ * according to ICU Transliterator class. Polyphone is not supported in this
+ * implementation.
+ */
+public class HanziToPinyin {
+    private static final String TAG = "HanziToPinyin";
+
+    private static HanziToPinyin sInstance;
+    private Transliterator mPinyinTransliterator;
+    private Transliterator mAsciiTransliterator;
+
+    public static class Token {
+        /**
+         * Separator between target string for each source char
+         */
+        public static final String SEPARATOR = " ";
+
+        public static final int LATIN = 1;
+        public static final int PINYIN = 2;
+        public static final int UNKNOWN = 3;
+
+        public Token() {
+        }
+
+        public Token(int type, String source, String target) {
+            this.type = type;
+            this.source = source;
+            this.target = target;
+        }
+
+        /**
+         * Type of this token, ASCII, PINYIN or UNKNOWN.
+         */
+        public int type;
+        /**
+         * Original string before translation.
+         */
+        public String source;
+        /**
+         * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is
+         * original string in source.
+         */
+        public String target;
+    }
+
+    private HanziToPinyin() {
+        try {
+            mPinyinTransliterator = new Transliterator("Han-Latin/Names; Latin-Ascii; Any-Upper");
+            mAsciiTransliterator = new Transliterator("Latin-Ascii");
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Han-Latin/Names transliterator data is missing,"
+                    + " HanziToPinyin is disabled");
+        }
+    }
+
+    public boolean hasChineseTransliterator() {
+        return mPinyinTransliterator != null;
+    }
+
+    public static HanziToPinyin getInstance() {
+        synchronized (HanziToPinyin.class) {
+            if (sInstance == null) {
+                sInstance = new HanziToPinyin();
+            }
+            return sInstance;
+        }
+    }
+
+    private void tokenize(char character, Token token) {
+        token.source = Character.toString(character);
+
+        // ASCII
+        if (character < 128) {
+            token.type = Token.LATIN;
+            token.target = token.source;
+            return;
+        }
+
+        // Extended Latin. Transcode these to ASCII equivalents
+        if (character < 0x250 || (0x1e00 <= character && character < 0x1eff)) {
+            token.type = Token.LATIN;
+            token.target = mAsciiTransliterator == null ? token.source :
+                    mAsciiTransliterator.transliterate(token.source);
+            return;
+        }
+
+        token.type = Token.PINYIN;
+        token.target = mPinyinTransliterator.transliterate(token.source);
+        if (TextUtils.isEmpty(token.target) ||
+                TextUtils.equals(token.source, token.target)) {
+            token.type = Token.UNKNOWN;
+            token.target = token.source;
+        }
+    }
+
+    public String transliterate(final String input) {
+        if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) {
+            return null;
+        }
+        return mPinyinTransliterator.transliterate(input);
+    }
+
+    /**
+     * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without
+     * space will be put into a Token, One Hanzi character which has pinyin will be treated as a
+     * Token. If there is no Chinese transliterator, the empty token array is returned.
+     */
+    public ArrayList<Token> getTokens(final String input) {
+        ArrayList<Token> tokens = new ArrayList<Token>();
+        if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) {
+            // return empty tokens.
+            return tokens;
+        }
+
+        final int inputLength = input.length();
+        final StringBuilder sb = new StringBuilder();
+        int tokenType = Token.LATIN;
+        Token token = new Token();
+
+        // Go through the input, create a new token when
+        // a. Token type changed
+        // b. Get the Pinyin of current charater.
+        // c. current character is space.
+        for (int i = 0; i < inputLength; i++) {
+            final char character = input.charAt(i);
+            if (Character.isSpaceChar(character)) {
+                if (sb.length() > 0) {
+                    addToken(sb, tokens, tokenType);
+                }
+            } else {
+                tokenize(character, token);
+                if (token.type == Token.PINYIN) {
+                    if (sb.length() > 0) {
+                        addToken(sb, tokens, tokenType);
+                    }
+                    tokens.add(token);
+                    token = new Token();
+                } else {
+                    if (tokenType != token.type && sb.length() > 0) {
+                        addToken(sb, tokens, tokenType);
+                    }
+                    sb.append(token.target);
+                }
+                tokenType = token.type;
+            }
+        }
+        if (sb.length() > 0) {
+            addToken(sb, tokens, tokenType);
+        }
+        return tokens;
+    }
+
+    private void addToken(
+            final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) {
+        String str = sb.toString();
+        tokens.add(new Token(tokenType, str, str));
+        sb.setLength(0);
+    }
+}
diff --git a/src/com/cyanogenmod/eleven/locale/LocaleChangeReceiver.java b/src/com/cyanogenmod/eleven/locale/LocaleChangeReceiver.java
new file mode 100644 (file)
index 0000000..3d2f99f
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.cyanogenmod.eleven.locale;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.cyanogenmod.eleven.provider.LocalizedStore;
+
+/**
+ * Locale change intent receiver that invokes {@link LocalizedStore} to update
+ * the database for the new locale.
+ */
+public class LocaleChangeReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        LocalizedStore.getInstance(context).onLocaleChanged();
+    }
+}
diff --git a/src/com/cyanogenmod/eleven/locale/LocaleSet.java b/src/com/cyanogenmod/eleven/locale/LocaleSet.java
new file mode 100644 (file)
index 0000000..25b2ac9
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2014 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.cyanogenmod.eleven.locale;
+
+import android.text.TextUtils;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Locale;
+
+public class LocaleSet {
+    private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase();
+    private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+    private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
+
+    private static class LocaleWrapper {
+        private final Locale mLocale;
+        private final String mLanguage;
+        private final boolean mLocaleIsCJK;
+
+        private static boolean isLanguageCJK(String language) {
+            return CHINESE_LANGUAGE.equals(language) ||
+                    JAPANESE_LANGUAGE.equals(language) ||
+                    KOREAN_LANGUAGE.equals(language);
+        }
+
+        public LocaleWrapper(Locale locale) {
+            mLocale = locale;
+            if (mLocale != null) {
+                mLanguage = mLocale.getLanguage().toLowerCase();
+                mLocaleIsCJK = isLanguageCJK(mLanguage);
+            } else {
+                mLanguage = null;
+                mLocaleIsCJK = false;
+            }
+        }
+
+        public boolean hasLocale() {
+            return mLocale != null;
+        }
+
+        public Locale getLocale() {
+            return mLocale;
+        }
+
+        public boolean isLocale(Locale locale) {
+            return mLocale == null ? (locale == null) : mLocale.equals(locale);
+        }
+
+        public boolean isLocaleCJK() {
+            return mLocaleIsCJK;
+        }
+
+        public boolean isLanguage(String language) {
+            return mLanguage == null ? (language == null)
+                    : mLanguage.equalsIgnoreCase(language);
+        }
+
+        public String toString() {
+            return mLocale != null ? mLocale.toLanguageTag() : "(null)";
+        }
+    }
+
+    public static LocaleSet getDefault() {
+        return new LocaleSet(Locale.getDefault());
+    }
+
+    public LocaleSet(Locale locale) {
+        this(locale, null);
+    }
+
+    /**
+     * Returns locale set for a given set of IETF BCP-47 tags separated by ';'.
+     * BCP-47 tags are what is used by ICU 52's toLanguageTag/forLanguageTag
+     * methods to represent individual Locales: "en-US" for Locale.US,
+     * "zh-CN" for Locale.CHINA, etc. So eg "en-US;zh-CN" specifies the locale
+     * set LocaleSet(Locale.US, Locale.CHINA).
+     *
+     * @param localeString One or more BCP-47 tags separated by ';'.
+     * @return LocaleSet for specified locale string, or default set if null
+     * or unable to parse.
+     */
+    public static LocaleSet getLocaleSet(String localeString) {
+        // Locale.toString() generates strings like "en_US" and "zh_CN_#Hans".
+        // Locale.toLanguageTag() generates strings like "en-US" and "zh-Hans-CN".
+        // We can only parse language tags.
+        if (localeString != null && localeString.indexOf('_') == -1) {
+            final String[] locales = localeString.split(";");
+            final Locale primaryLocale = Locale.forLanguageTag(locales[0]);
+            // ICU tags undefined/unparseable locales "und"
+            if (primaryLocale != null &&
+                    !TextUtils.equals(primaryLocale.toLanguageTag(), "und")) {
+                if (locales.length > 1 && locales[1] != null) {
+                    final Locale secondaryLocale = Locale.forLanguageTag(locales[1]);
+                    if (secondaryLocale != null &&
+                            !TextUtils.equals(secondaryLocale.toLanguageTag(), "und")) {
+                        return new LocaleSet(primaryLocale, secondaryLocale);
+                    }
+                }
+                return new LocaleSet(primaryLocale);
+            }
+        }
+        return getDefault();
+    }
+
+    private final LocaleWrapper mPrimaryLocale;
+    private final LocaleWrapper mSecondaryLocale;
+
+    public LocaleSet(Locale primaryLocale, Locale secondaryLocale) {
+        mPrimaryLocale = new LocaleWrapper(primaryLocale);
+        mSecondaryLocale = new LocaleWrapper(
+                mPrimaryLocale.equals(secondaryLocale) ? null : secondaryLocale);
+    }
+
+    public LocaleSet normalize() {
+        final Locale primaryLocale = getPrimaryLocale();
+        if (primaryLocale == null) {
+            return getDefault();
+        }
+        Locale secondaryLocale = getSecondaryLocale();
+        // disallow both locales with same language (redundant and/or conflicting)
+        // disallow both locales CJK (conflicting rules)
+        if (secondaryLocale == null ||
+                isPrimaryLanguage(secondaryLocale.getLanguage()) ||
+                (isPrimaryLocaleCJK() && isSecondaryLocaleCJK())) {
+            return new LocaleSet(primaryLocale);
+        }
+        // unnecessary to specify English as secondary locale (redundant)
+        if (isSecondaryLanguage(Locale.ENGLISH.getLanguage())) {
+            return new LocaleSet(primaryLocale);
+        }
+        return this;
+    }
+
+    public boolean hasSecondaryLocale() {
+        return mSecondaryLocale.hasLocale();
+    }
+
+    public Locale getPrimaryLocale() {
+        return mPrimaryLocale.getLocale();
+    }
+
+    public Locale getSecondaryLocale() {
+        return mSecondaryLocale.getLocale();
+    }
+
+    public boolean isPrimaryLocale(Locale locale) {
+        return mPrimaryLocale.isLocale(locale);
+    }
+
+    public boolean isSecondaryLocale(Locale locale) {
+        return mSecondaryLocale.isLocale(locale);
+    }
+
+    private static final String SCRIPT_SIMPLIFIED_CHINESE = "Hans";
+    private static final String SCRIPT_TRADITIONAL_CHINESE = "Hant";
+
+    @VisibleForTesting
+    public static boolean isLocaleSimplifiedChinese(Locale locale) {
+        // language must match
+        if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) {
+            return false;
+        }
+        // script is optional but if present must match
+        if (!TextUtils.isEmpty(locale.getScript())) {
+            return locale.getScript().equals(SCRIPT_SIMPLIFIED_CHINESE);
+        }
+        // if no script, must match known country
+        return locale.equals(Locale.SIMPLIFIED_CHINESE);
+    }
+
+    public boolean isPrimaryLocaleSimplifiedChinese() {
+        return isLocaleSimplifiedChinese(getPrimaryLocale());
+    }
+
+    public boolean isSecondaryLocaleSimplifiedChinese() {
+        return isLocaleSimplifiedChinese(getSecondaryLocale());
+    }
+
+    @VisibleForTesting
+    public static boolean isLocaleTraditionalChinese(Locale locale) {
+        // language must match
+        if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) {
+            return false;
+        }
+        // script is optional but if present must match
+        if (!TextUtils.isEmpty(locale.getScript())) {
+            return locale.getScript().equals(SCRIPT_TRADITIONAL_CHINESE);
+        }
+        // if no script, must match known country
+        return locale.equals(Locale.TRADITIONAL_CHINESE);
+    }
+
+    public boolean isPrimaryLocaleTraditionalChinese() {
+        return isLocaleTraditionalChinese(getPrimaryLocale());
+    }
+
+    public boolean isSecondaryLocaleTraditionalChinese() {
+        return isLocaleTraditionalChinese(getSecondaryLocale());
+    }
+
+    public boolean isPrimaryLocaleCJK() {
+        return mPrimaryLocale.isLocaleCJK();
+    }
+
+    public boolean isSecondaryLocaleCJK() {
+        return mSecondaryLocale.isLocaleCJK();
+    }
+
+    public boolean isPrimaryLanguage(String language) {
+        return mPrimaryLocale.isLanguage(language);
+    }
+
+    public boolean isSecondaryLanguage(String language) {
+        return mSecondaryLocale.isLanguage(language);
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object == this) {
+            return true;
+        }
+        if (object instanceof LocaleSet) {
+            final LocaleSet other = (LocaleSet) object;
+            return other.isPrimaryLocale(mPrimaryLocale.getLocale())
+                    && other.isSecondaryLocale(mSecondaryLocale.getLocale());
+        }
+        return false;
+    }
+
+    @Override
+    public final String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append(mPrimaryLocale.toString());
+        if (hasSecondaryLocale()) {
+            builder.append(";");
+            builder.append(mSecondaryLocale.toString());
+        }
+        return builder.toString();
+    }
+}
diff --git a/src/com/cyanogenmod/eleven/locale/LocaleSetManager.java b/src/com/cyanogenmod/eleven/locale/LocaleSetManager.java
new file mode 100644 (file)
index 0000000..8e49349
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Copyright (C) 2009 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.cyanogenmod.eleven.locale;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.cyanogenmod.eleven.provider.PropertiesStore;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+import libcore.icu.ICU;
+
+public class LocaleSetManager {
+    private static final String TAG = LocaleSetManager.class.getSimpleName();
+
+    private LocaleSet mCurrentLocales;
+    private final Context mContext;
+
+    public LocaleSetManager(final Context context) {
+        mContext = context;
+    }
+
+    /**
+     * @return true if the currently saved locale set needs to be updated
+     */
+    public boolean localeSetNeedsUpdate() {
+        // if we haven't loaded our current locale, try to retrieve it from the db
+        if (mCurrentLocales == null) {
+            updateLocaleSet(getStoredLocaleSet());
+        }
+
+        LocaleSet systemLocaleSet = getSystemLocaleSet();
+
+        // if we don't have a stored locale or it is different, return true
+        if (mCurrentLocales == null ||
+                !mCurrentLocales.toString().equals(systemLocaleSet.toString())) {
+            return true;
+        }
+
+        // if our icu version has changed, return true
+        final String storedICUversion = PropertiesStore.getInstance(mContext)
+                .getProperty(PropertiesStore.DbProperties.ICU_VERSION);
+        if (!ICU.getIcuVersion().equals(storedICUversion)) {
+            Log.d(TAG, "ICU version has changed from: " + storedICUversion + " to "
+                    + ICU.getIcuVersion());
+            return true;
+        }
+
+
+        return false;
+    }
+
+    /**
+     * Sets up the locale set
+     * @param localeSet value to set it to
+     */
+    public void updateLocaleSet(LocaleSet localeSet) {
+        Log.d(TAG, "Locale Changed from: " + mCurrentLocales + " to " + localeSet);
+        mCurrentLocales = localeSet;
+        LocaleUtils.getInstance().setLocales(mCurrentLocales);
+    }
+
+    /**
+     * This takes an old and new locale set and creates a combined locale set.  If they share a
+     * primary then the old one is returned
+     * @return the combined locale set
+     */
+    private static LocaleSet getCombinedLocaleSet(LocaleSet oldLocales, Locale newLocale) {
+        Locale prevLocale = null;
+
+        if (oldLocales != null) {
+            prevLocale = oldLocales.getPrimaryLocale();
+            // If primary locale is unchanged then no change to locale set.
+            if (newLocale.equals(prevLocale)) {
+                return oldLocales;
+            }
+        }
+
+        // Otherwise, construct a new locale set based on the new locale
+        // and the previous primary locale.
+        return new LocaleSet(newLocale, prevLocale).normalize();
+    }
+
+    /**
+     * @return the system locale set
+     */
+    public LocaleSet getSystemLocaleSet() {
+        final Locale curLocale = getLocale();
+        return getCombinedLocaleSet(mCurrentLocales, curLocale);
+    }
+
+    /**
+     * @return the stored locale set
+     */
+    public LocaleSet getStoredLocaleSet() {
+        final String providerLocaleString = PropertiesStore.getInstance(mContext)
+                .getProperty(PropertiesStore.DbProperties.LOCALE);
+
+        if (TextUtils.isEmpty(providerLocaleString)) {
+            return null;
+        }
+
+        return LocaleSet.getLocaleSet(providerLocaleString);
+    }
+
+    @VisibleForTesting
+    protected Locale getLocale() {
+        return Locale.getDefault();
+    }
+}
diff --git a/src/com/cyanogenmod/eleven/locale/LocaleUtils.java b/src/com/cyanogenmod/eleven/locale/LocaleUtils.java
new file mode 100644 (file)
index 0000000..f16a17a
--- /dev/null
@@ -0,0 +1,484 @@
+/*
+ * 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.cyanogenmod.eleven.locale;
+
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.cyanogenmod.eleven.locale.HanziToPinyin.Token;
+
+import com.google.common.annotations.VisibleForTesting;
+
+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");
+    // Serbian and Ukrainian labels are complementary supersets of Russian
+    public static final Locale LOCALE_SERBIAN = new Locale("sr");
+    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;
+        private final boolean mEnableSecondaryLocalePinyin;
+
+        public LocaleUtilsBase(LocaleSet locales) {
+            // 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 and Serbian are chosen for
+            // Cyrillic because their alphabets are complementary supersets
+            // of Russian.
+            final Locale secondaryLocale = locales.getSecondaryLocale();
+            mEnableSecondaryLocalePinyin = locales.isSecondaryLocaleSimplifiedChinese();
+            AlphabeticIndex ai = new AlphabeticIndex(locales.getPrimaryLocale())
+                .setMaxLabelCount(300);
+            if (secondaryLocale != null) {
+                ai.addLabels(secondaryLocale);
+            }
+            mAlphabeticIndex = ai.addLabels(Locale.ENGLISH)
+                .addLabels(Locale.JAPANESE)
+                .addLabels(Locale.KOREAN)
+                .addLabels(LOCALE_THAI)
+                .addLabels(LOCALE_ARABIC)
+                .addLabels(LOCALE_HEBREW)
+                .addLabels(LOCALE_GREEK)
+                .addLabels(LOCALE_UKRAINIAN)
+                .addLabels(LOCALE_SERBIAN)
+                .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;
+            }
+
+            /**
+             * TODO: ICU 52 AlphabeticIndex doesn't support Simplified Chinese
+             * as a secondary locale. Remove the following if that is added.
+             */
+            if (mEnableSecondaryLocalePinyin) {
+                name = HanziToPinyin.getInstance().transliterate(name);
+            }
+            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;
+        }
+    }
+
+    /**
+     * Japanese specific locale overrides.
+     *
+     * sortKey: unchanged (same as name)
+     * nameLookupKeys: unchanged (none)
+     * labels: extends default labels by labeling unlabeled CJ characters
+     *     with the Japanese character 他 ("misc"). Japanese labels are:
+     *     あ, か, さ, た, な, は, ま, や, ら, わ, 他, [A-Z], #, " "
+     */
+    private static class JapaneseContactUtils extends LocaleUtilsBase {
+        // \u4ed6 is Japanese character 他 ("misc")
+        private static final String JAPANESE_MISC_LABEL = "\u4ed6";
+        private final int mMiscBucketIndex;
+
+        public JapaneseContactUtils(LocaleSet locales) {
+            super(locales);
+            // Determine which bucket AlphabeticIndex is lumping unclassified
+            // Japanese characters into by looking up the bucket index for
+            // a representative Kanji/CJK unified ideograph (\u65e5 is the
+            // character '日').
+            mMiscBucketIndex = super.getBucketIndex("\u65e5");
+        }
+
+        // Set of UnicodeBlocks for unified CJK (Chinese) characters and
+        // Japanese characters. This includes all code blocks that might
+        // contain a character used in Japanese (which is why unified CJK
+        // blocks are included but Korean Hangul and jamo are not).
+        private static final Set<Character.UnicodeBlock> CJ_BLOCKS;
+        static {
+            Set<UnicodeBlock> set = new HashSet<UnicodeBlock>();
+            set.add(UnicodeBlock.HIRAGANA);
+            set.add(UnicodeBlock.KATAKANA);
+            set.add(UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS);
+            set.add(UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B);
+            set.add(UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION);
+            set.add(UnicodeBlock.CJK_RADICALS_SUPPLEMENT);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_FORMS);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT);
+            CJ_BLOCKS = Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Helper routine to identify unlabeled Chinese or Japanese characters
+         * to put in a 'misc' bucket.
+         *
+         * @return true if the specified Unicode code point is Chinese or
+         *              Japanese
+         */
+        private static boolean isChineseOrJapanese(int codePoint) {
+            return CJ_BLOCKS.contains(UnicodeBlock.of(codePoint));
+        }
+
+        /**
+         * Returns the bucket index for the specified string. Adds an
+         * additional 'misc' bucket for Kanji characters to the base class set.
+         */
+        @Override
+        public int getBucketIndex(String name) {
+            final int bucketIndex = super.getBucketIndex(name);
+            if ((bucketIndex == mMiscBucketIndex &&
+                 !isChineseOrJapanese(Character.codePointAt(name, 0))) ||
+                bucketIndex > mMiscBucketIndex) {
+                return bucketIndex + 1;
+            }
+            return bucketIndex;
+        }
+
+        /**
+         * Returns the number of buckets in use (one more than the base class
+         * uses, because this class adds a bucket for Kanji).
+         */
+        @Override
+        public int getBucketCount() {
+            return super.getBucketCount() + 1;
+        }
+
+        /**
+         * Returns the label for the specified bucket index if a valid index,
+         * otherwise returns an empty string. '他' is returned for unclassified
+         * Kanji; for all others, the label determined by the base class is
+         * returned.
+         */
+        @Override
+        public String getBucketLabel(int bucketIndex) {
+            if (bucketIndex == mMiscBucketIndex) {
+                return JAPANESE_MISC_LABEL;
+            } else if (bucketIndex > mMiscBucketIndex) {
+                --bucketIndex;
+            }
+            return super.getBucketLabel(bucketIndex);
+        }
+
+        @Override
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+            // Hiragana and Katakana will be positively identified as Japanese.
+            if (nameStyle == PhoneticNameStyle.JAPANESE) {
+                return getRomajiNameLookupKeys(name);
+            }
+            return null;
+        }
+
+        private static boolean mInitializedTransliterator;
+        private static Transliterator mJapaneseTransliterator;
+
+        private static Transliterator getJapaneseTransliterator() {
+            synchronized(JapaneseContactUtils.class) {
+                if (!mInitializedTransliterator) {
+                    mInitializedTransliterator = true;
+                    Transliterator t = null;
+                    try {
+                        t = new Transliterator("Hiragana-Latin; Katakana-Latin;"
+                                + " Latin-Ascii");
+                    } catch (RuntimeException e) {
+                        Log.w(TAG, "Hiragana/Katakana-Latin transliterator data"
+                                + " is missing");
+                    }
+                    mJapaneseTransliterator = t;
+                }
+                return mJapaneseTransliterator;
+            }
+        }
+
+        public static Iterator<String> getRomajiNameLookupKeys(String name) {
+            final Transliterator t = getJapaneseTransliterator();
+            if (t == null) {
+                return null;
+            }
+            final String romajiName = t.transliterate(name);
+            if (TextUtils.isEmpty(romajiName) ||
+                    TextUtils.equals(name, romajiName)) {
+                return null;
+            }
+            final HashSet<String> keys = new HashSet<String>();
+            keys.add(romajiName);
+            return keys.iterator();
+        }
+    }
+
+    /**
+     * Simplified Chinese specific locale overrides. Uses ICU Transliterator
+     * for generating pinyin transliteration.
+     *
+     * sortKey: unchanged (same as name)
+     * nameLookupKeys: adds additional name lookup keys
+     *     - Chinese character's pinyin and pinyin's initial character.
+     *     - Latin word and initial character.
+     * labels: unchanged
+     *     Simplified Chinese labels are the same as English: [A-Z], #, " "
+     */
+    private static class SimplifiedChineseContactUtils
+        extends LocaleUtilsBase {
+        public SimplifiedChineseContactUtils(LocaleSet locales) {
+            super(locales);
+        }
+
+        @Override
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+            if (nameStyle != FullNameStyle.JAPANESE &&
+                    nameStyle != FullNameStyle.KOREAN) {
+                return getPinyinNameLookupKeys(name);
+            }
+            return null;
+        }
+
+        public static Iterator<String> getPinyinNameLookupKeys(String name) {
+            // TODO : Reduce the object allocation.
+            HashSet<String> keys = new HashSet<String>();
+            ArrayList<Token> tokens = HanziToPinyin.getInstance().getTokens(name);
+            final int tokenCount = tokens.size();
+            final StringBuilder keyPinyin = new StringBuilder();
+            final StringBuilder keyInitial = new StringBuilder();
+            // There is no space among the Chinese Characters, the variant name
+            // lookup key wouldn't work for Chinese. The keyOriginal is used to
+            // build the lookup keys for itself.
+            final StringBuilder keyOriginal = new StringBuilder();
+            for (int i = tokenCount - 1; i >= 0; i--) {
+                final Token token = tokens.get(i);
+                if (Token.UNKNOWN == token.type) {
+                    continue;
+                }
+                if (Token.PINYIN == token.type) {
+                    keyPinyin.insert(0, token.target);
+                    keyInitial.insert(0, token.target.charAt(0));
+                } else if (Token.LATIN == token.type) {
+                    // Avoid adding space at the end of String.
+                    if (keyPinyin.length() > 0) {
+                        keyPinyin.insert(0, ' ');
+                    }
+                    if (keyOriginal.length() > 0) {
+                        keyOriginal.insert(0, ' ');
+                    }
+                    keyPinyin.insert(0, token.source);
+                    keyInitial.insert(0, token.source.charAt(0));
+                }
+                keyOriginal.insert(0, token.source);
+                keys.add(keyOriginal.toString());
+                keys.add(keyPinyin.toString());
+                keys.add(keyInitial.toString());
+            }
+            return keys.iterator();
+        }
+    }
+
+    private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+    private static LocaleUtils sSingleton;
+
+    private final LocaleSet mLocales;
+    private final LocaleUtilsBase mUtils;
+
+    private LocaleUtils(LocaleSet locales) {
+        if (locales == null) {
+            mLocales = LocaleSet.getDefault();
+        } else {
+            mLocales = locales;
+        }
+        if (mLocales.isPrimaryLanguage(JAPANESE_LANGUAGE)) {
+            mUtils = new JapaneseContactUtils(mLocales);
+        } else if (mLocales.isPrimaryLocaleSimplifiedChinese()) {
+            mUtils = new SimplifiedChineseContactUtils(mLocales);
+        } else {
+            mUtils = new LocaleUtilsBase(mLocales);
+        }
+        Log.i(TAG, "AddressBook Labels [" + mLocales.toString() + "]: "
+                + getLabels().toString());
+    }
+
+    public boolean isLocale(LocaleSet locales) {
+        return mLocales.equals(locales);
+    }
+
+    public static synchronized LocaleUtils getInstance() {
+        if (sSingleton == null) {
+            sSingleton = new LocaleUtils(LocaleSet.getDefault());
+        }
+        return sSingleton;
+    }
+
+    @VisibleForTesting
+    public static synchronized void setLocale(Locale locale) {
+        setLocales(new LocaleSet(locale));
+    }
+
+    public static synchronized void setLocales(LocaleSet locales) {
+        if (sSingleton == null || !sSingleton.isLocale(locales)) {
+            sSingleton = new LocaleUtils(locales);
+        }
+    }
+
+    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 b84cb76..f987a6f 100644 (file)
@@ -48,6 +48,12 @@ public class Album {
     public String mYear;
 
     /**
+     * Bucket label for the name - may not necessarily be the name - for example albums sorted by
+     * artists would be the artist bucket label and not the album name bucket label
+     */
+    public String mBucketLabel;
+
+    /**
      * Constructor of <code>Album</code>
      * 
      * @param albumId The Id of the album
index ed47553..e54f684 100644 (file)
@@ -43,6 +43,11 @@ public class Artist {
     public int mSongNumber;
 
     /**
+     * Bucket label for the artist name if it exists
+     */
+    public String mBucketLabel;
+
+    /**
      * Constructor of <code>Artist</code>
      * 
      * @param artistId The Id of the artist
index aac99ea..949d0fa 100644 (file)
@@ -58,6 +58,12 @@ public class Song {
     public int mYear;
 
     /**
+     * Bucket label for the name - may not necessarily be the name - for example songs sorted by
+     * artists would be the artist bucket label and not the song name bucket label
+     */
+    public String mBucketLabel;
+
+    /**
      * Constructor of <code>Song</code>
      * 
      * @param songId The Id of the song
diff --git a/src/com/cyanogenmod/eleven/provider/LocalizedStore.java b/src/com/cyanogenmod/eleven/provider/LocalizedStore.java
new file mode 100644 (file)
index 0000000..43367e6
--- /dev/null
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.eleven.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.AudioColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.cyanogenmod.eleven.loaders.SortedCursor;
+import com.cyanogenmod.eleven.locale.LocaleSet;
+import com.cyanogenmod.eleven.locale.LocaleSetManager;
+import com.cyanogenmod.eleven.locale.LocaleUtils;
+import com.cyanogenmod.eleven.utils.MusicUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import libcore.icu.ICU;
+
+/**
+ * Because sqlite localized collator isn't sufficient, we need to store more specialized logic
+ * into our own db similar to contacts db.  This is most noticeable in languages like Chinese,
+ * Japanese etc
+ */
+public class LocalizedStore {
+    private static final String TAG = LocalizedStore.class.getSimpleName();
+    private static final boolean DEBUG = false;
+    private static LocalizedStore sInstance = null;
+
+    private static final int LOCALE_CHANGED = 0;
+
+    private final MusicDB mMusicDatabase;
+    private final Context mContext;
+    private final ContentValues mContentValues = new ContentValues(10);
+    private final LocaleSetManager mLocaleSetManager;
+
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+
+    public enum SortParameter {
+        Song,
+        Artist,
+        Album,
+    };
+
+    private static class SortData {
+        long[] ids;
+        List<String> bucketLabels;
+    }
+
+    /**
+     * @param context The {@link android.content.Context} to use
+     * @return A new instance of this class.
+     */
+    public static final synchronized LocalizedStore getInstance(final Context context) {
+        if (sInstance == null) {
+            sInstance = new LocalizedStore(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    private LocalizedStore(final Context context) {
+        mMusicDatabase = MusicDB.getInstance(context);
+        mContext = context;
+        mLocaleSetManager = new LocaleSetManager(mContext);
+
+        mHandlerThread = new HandlerThread("LocalizedStoreWorker",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == LOCALE_CHANGED && mLocaleSetManager.localeSetNeedsUpdate()) {
+                    rebuildLocaleData(mLocaleSetManager.getSystemLocaleSet());
+                }
+            }
+        };
+
+        // check to see if locale has changed
+        onLocaleChanged();
+    }
+
+    public void onCreate(final SQLiteDatabase db) {
+
+        String[] tables = new String[]{
+            "CREATE TABLE IF NOT EXISTS " + SongSortColumns.TABLE_NAME + "(" +
+                    SongSortColumns.ID + " INTEGER PRIMARY KEY," +
+                    SongSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
+                    SongSortColumns.ALBUM_ID + " INTEGER NOT NULL," +
+                    SongSortColumns.NAME + " TEXT," +
+                    SongSortColumns.NAME_LABEL + " TEXT," +
+                    SongSortColumns.NAME_BUCKET + " INTEGER);",
+
+            "CREATE TABLE IF NOT EXISTS " + AlbumSortColumns.TABLE_NAME + "(" +
+                    AlbumSortColumns.ID + " INTEGER PRIMARY KEY," +
+                    AlbumSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
+                    AlbumSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
+                    AlbumSortColumns.NAME_LABEL + " TEXT," +
+                    AlbumSortColumns.NAME_BUCKET + " INTEGER);",
+
+            "CREATE TABLE IF NOT EXISTS " + ArtistSortColumns.TABLE_NAME + "(" +
+                    ArtistSortColumns.ID + " INTEGER PRIMARY KEY," +
+                    ArtistSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
+                    ArtistSortColumns.NAME_LABEL + " TEXT," +
+                    ArtistSortColumns.NAME_BUCKET + " INTEGER);",
+        };
+
+        for (String table : tables) {
+            if (DEBUG) {
+                Log.d(TAG, "Creating table: " + table);
+            }
+            db.execSQL(table);
+        }
+    }
+
+    public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+        // this table was created in version 3 so call the onCreate method if we hit that scenario
+        if (oldVersion < 3 && newVersion >= 3) {
+            onCreate(db);
+        }
+    }
+
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // If we ever have downgrade, drop the table to be safe
+        db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
+        db.execSQL("DROP TABLE IF EXISTS " + AlbumSortColumns.TABLE_NAME);
+        db.execSQL("DROP TABLE IF EXISTS " + ArtistSortColumns.TABLE_NAME);
+        onCreate(db);
+    }
+
+    public void onLocaleChanged() {
+        mHandler.obtainMessage(LOCALE_CHANGED).sendToTarget();
+    }
+
+    private void rebuildLocaleData(LocaleSet locales) {
+        if (DEBUG) {
+            Log.d(TAG, "Locale has changed, rebuilding sorting data");
+        }
+
+        final long start = SystemClock.elapsedRealtime();
+        final SQLiteDatabase db = mMusicDatabase.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            db.execSQL("DELETE FROM " + SongSortColumns.TABLE_NAME);
+            db.execSQL("DELETE FROM " + AlbumSortColumns.TABLE_NAME);
+            db.execSQL("DELETE FROM " + ArtistSortColumns.TABLE_NAME);
+
+            // prep the localization classes
+            mLocaleSetManager.updateLocaleSet(locales);
+
+            updateLocalizedStore(db, null);
+
+            // Update the ICU version used to generate the locale derived data
+            // so we can tell when we need to rebuild with new ICU versions.
+            PropertiesStore.getInstance(mContext).storeProperty(
+                    PropertiesStore.DbProperties.ICU_VERSION, ICU.getIcuVersion());
+            PropertiesStore.getInstance(mContext).storeProperty(PropertiesStore.DbProperties.LOCALE,
+                    locales.toString());
+
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+
+        if (DEBUG) {
+            Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
+        }
+    }
+
+    /**
+     * This will grab all the songs from the medistore and add the localized data to the db
+     * @param selection if we only want to do this for some songs, this selection will filter it out
+     */
+    private void updateLocalizedStore(final SQLiteDatabase db, final String selection) {
+        db.beginTransaction();
+        try {
+            Cursor cursor = null;
+
+            try {
+                final String combinedSelection = MusicUtils.MUSIC_ONLY_SELECTION +
+                        (TextUtils.isEmpty(selection) ? "" : " AND " + selection);
+
+                // order by artist/album/id to minimize artist/album re-inserts
+                final String orderBy = AudioColumns.ARTIST_ID + "," + AudioColumns.ALBUM + ","
+                        + AudioColumns._ID;
+
+                if (DEBUG) {
+                    Log.d(TAG, "Running selection query: " + combinedSelection);
+                }
+
+                cursor = mContext.getContentResolver().query(
+                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        new String[]{
+                                // 0
+                                AudioColumns._ID,
+                                // 1
+                                AudioColumns.TITLE,
+                                // 2
+                                AudioColumns.ARTIST_ID,
+                                // 3
+                                AudioColumns.ARTIST,
+                                // 4
+                                AudioColumns.ALBUM_ID,
+                                // 5
+                                AudioColumns.ALBUM,
+                        }, combinedSelection, null, orderBy);
+
+                long previousArtistId = -1;
+                long previousAlbumId = -1;
+                long artistId;
+                long albumId;
+
+                if (cursor != null && cursor.moveToFirst()) {
+                    do {
+                        albumId = cursor.getLong(4);
+                        artistId = cursor.getLong(2);
+
+                        if (artistId != previousArtistId) {
+                            previousArtistId = artistId;
+                            updateArtistData(db, artistId, cursor.getString(3));
+                        }
+
+                        if (albumId != previousAlbumId) {
+                            previousAlbumId = albumId;
+
+                            updateAlbumData(db, albumId, cursor.getString(5), artistId);
+                        }
+
+                        updateSongData(db, cursor.getLong(0), cursor.getString(1), artistId, albumId);
+                    } while (cursor.moveToNext());
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                    cursor = null;
+                }
+            }
+
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private void updateArtistData(SQLiteDatabase db, long id, String name) {
+        mContentValues.clear();
+        name = MusicUtils.getTrimmedName(name);
+
+        final LocaleUtils localeUtils = LocaleUtils.getInstance();
+        final int bucketIndex = localeUtils.getBucketIndex(name);
+
+        mContentValues.put(ArtistSortColumns.ID, id);
+        mContentValues.put(ArtistSortColumns.NAME, name);
+        mContentValues.put(ArtistSortColumns.NAME_BUCKET, bucketIndex);
+        mContentValues.put(ArtistSortColumns.NAME_LABEL,
+                localeUtils.getBucketLabel(bucketIndex));
+
+        db.insertWithOnConflict(ArtistSortColumns.TABLE_NAME, null, mContentValues,
+                SQLiteDatabase.CONFLICT_IGNORE);
+    }
+
+    private void updateAlbumData(SQLiteDatabase db, long id, String name, long artistId) {
+        mContentValues.clear();
+        name = MusicUtils.getTrimmedName(name);
+
+        final LocaleUtils localeUtils = LocaleUtils.getInstance();
+        final int bucketIndex = localeUtils.getBucketIndex(name);
+
+        mContentValues.put(AlbumSortColumns.ID, id);
+        mContentValues.put(AlbumSortColumns.NAME, name);
+        mContentValues.put(AlbumSortColumns.NAME_BUCKET, bucketIndex);
+        mContentValues.put(AlbumSortColumns.NAME_LABEL,
+                localeUtils.getBucketLabel(bucketIndex));
+        mContentValues.put(AlbumSortColumns.ARTIST_ID, artistId);
+
+        db.insertWithOnConflict(AlbumSortColumns.TABLE_NAME, null, mContentValues,
+                SQLiteDatabase.CONFLICT_IGNORE);
+    }
+
+    private void updateSongData(SQLiteDatabase db, long id, String name, long artistId,
+                                long albumId) {
+        mContentValues.clear();
+        name = MusicUtils.getTrimmedName(name);
+
+        final LocaleUtils localeUtils = LocaleUtils.getInstance();
+        final int bucketIndex = localeUtils.getBucketIndex(name);
+
+        mContentValues.put(SongSortColumns.ID, id);
+        mContentValues.put(SongSortColumns.NAME, name);
+        mContentValues.put(SongSortColumns.NAME_BUCKET, bucketIndex);
+        mContentValues.put(SongSortColumns.NAME_LABEL,
+                localeUtils.getBucketLabel(bucketIndex));
+        mContentValues.put(SongSortColumns.ARTIST_ID, artistId);
+        mContentValues.put(SongSortColumns.ALBUM_ID, albumId);
+
+        db.insertWithOnConflict(SongSortColumns.TABLE_NAME, null, mContentValues,
+                SQLiteDatabase.CONFLICT_IGNORE);
+    }
+
+    /**
+     * Gets the list of saved ids and labels for the itemType in localized sorted order
+     * @param itemType the type of item we're querying for (artists, albums, songs)
+     * @param sortType the type we want to sort by (eg songs sorted by artists,
+     *                 albums sorted by artists).  Note some combinations don't make sense and
+     *                 will fallback to the basic sort, for example Artists sorted by songs
+     *                 doesn't make sense
+     * @param descending Whether we want to sort ascending or descending.  This will only apply to
+     *                  the basic searches (ie when sortType == itemType),
+     *                  otherwise ascending is always assumed
+     * @return sorted list of ids and bucket labels for the itemType
+     */
+    public SortData getSortOrder(SortParameter itemType, SortParameter sortType,
+                                boolean descending) {
+        SortData sortData = new SortData();
+        String tableName = "";
+        String joinClause = "";
+        String selectParams = "";
+        String postfixOrder = "";
+        String prefixOrder = "";
+
+        switch (itemType) {
+            case Song:
+                selectParams = SongSortColumns.CONCRETE_ID + ",";
+                postfixOrder = SongSortColumns.getOrderBy(descending);
+                tableName = SongSortColumns.TABLE_NAME;
+
+                if (sortType == SortParameter.Artist) {
+                    selectParams += ArtistSortColumns.NAME_LABEL;
+                    prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
+                    joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
+                            SongSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
+                } else if (sortType == SortParameter.Album) {
+                    selectParams += AlbumSortColumns.NAME_LABEL;
+                    prefixOrder = AlbumSortColumns.getOrderBy(false) + ",";
+                    joinClause = createJoin(AlbumSortColumns.TABLE_NAME,
+                            SongSortColumns.ALBUM_ID, AlbumSortColumns.CONCRETE_ID);
+                } else {
+                    selectParams += SongSortColumns.NAME_LABEL;
+                }
+                break;
+            case Artist:
+                selectParams = ArtistSortColumns.CONCRETE_ID + "," + ArtistSortColumns.NAME_LABEL;
+                postfixOrder = ArtistSortColumns.getOrderBy(descending);
+                tableName = ArtistSortColumns.TABLE_NAME;
+                break;
+            case Album:
+                selectParams = AlbumSortColumns.CONCRETE_ID + ",";
+                postfixOrder = AlbumSortColumns.getOrderBy(descending);
+                tableName = AlbumSortColumns.TABLE_NAME;
+                if (sortType == SortParameter.Artist) {
+                    selectParams += AlbumSortColumns.NAME_LABEL;
+                    prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
+                    joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
+                            AlbumSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
+                } else {
+                    selectParams += AlbumSortColumns.NAME_LABEL;
+                }
+                break;
+        }
+
+        final String selection = "SELECT " + selectParams
+                + " FROM " + tableName
+                + joinClause
+                + " ORDER BY " + prefixOrder + postfixOrder;
+
+        if (DEBUG) {
+            Log.d(TAG, "Running selection: " + selection);
+        }
+
+        Cursor c = null;
+        try {
+            c = mMusicDatabase.getReadableDatabase().rawQuery(selection, null);
+
+            if (c != null && c.moveToFirst()) {
+                sortData.ids = new long[c.getCount()];
+                sortData.bucketLabels = new ArrayList<String>(c.getCount());
+                do {
+                    sortData.ids[c.getPosition()] = c.getLong(0);
+                    sortData.bucketLabels.add(c.getString(1));
+                } while (c.moveToNext());
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+
+        return sortData;
+    }
+
+    /**
+     * Wraps the cursor with a sorted cursor that sorts it in the proper localized order
+     * @param cursor underlying cursor to sort
+     * @param columnName the column name of the id
+     * @param idType the type of item that the cursor contains
+     * @param sortType the type to sort by (for example can be song sorted by albums)
+     * @param descending descending?
+     * @param update do we want to update any discrepencies we find - only should be true if the
+     *               cursor contains all songs/artists/albums and not a subset
+     * @return the sorted cursor
+     */
+    public Cursor getLocalizedSort(Cursor cursor, String columnName, SortParameter idType,
+                                   SortParameter sortType, boolean descending, boolean update) {
+        if (cursor != null) {
+            SortedCursor sortedCursor = null;
+
+            // iterate up to twice if there are discrepancies found
+            for (int i = 0; i < 2; i++) {
+                // get the sort order for the sort parameter
+                SortData sortData = getSortOrder(idType, sortType, descending);
+
+                // get the sorted cursor based on the sort
+                sortedCursor = new SortedCursor(cursor, sortData.ids, columnName,
+                        sortData.bucketLabels);
+
+                if (!update || !updateDiscrepancies(sortedCursor, idType)) {
+                    break;
+                }
+            }
+
+            return sortedCursor;
+        }
+
+        return cursor;
+    }
+
+    /**
+     * Updates the localized store based on the cursor
+     * @param sortedCursor the current sorting cursor based on the LocalizedStore sort
+     * @param type the item type in the cursor
+     * @return true if there are new ids in the cursor that aren't tracked in the store
+     */
+    private boolean updateDiscrepancies(SortedCursor sortedCursor, SortParameter type) {
+        boolean hasNewIds = false;
+
+        final ArrayList<Long> missingIds = sortedCursor.getMissingIds();
+        if (missingIds.size() > 0) {
+            removeIds(missingIds, type);
+        }
+
+        final Collection<Long> extraIds = sortedCursor.getExtraIds();
+        if (extraIds != null && extraIds.size() > 0) {
+            addIds(extraIds, type);
+            hasNewIds = true;
+        }
+
+        return hasNewIds;
+    }
+
+    private void removeIds(ArrayList<Long> ids, SortParameter idType) {
+        if (ids == null || ids.size() == 0) {
+            return;
+        }
+
+        final String inParams = "(" + MusicUtils.buildCollectionAsString(ids) + ")";
+
+        if (DEBUG) {
+            Log.d(TAG, "Deleting from " + idType + " where id is in " + inParams);
+        }
+
+        switch (idType) {
+            case Song:
+                mMusicDatabase.getWritableDatabase().delete(SongSortColumns.TABLE_NAME,
+                        SongSortColumns.ID + " IN " + inParams, null);
+                break;
+            case Album:
+                mMusicDatabase.getWritableDatabase().delete(AlbumSortColumns.TABLE_NAME,
+                        AlbumSortColumns.ID + " IN " + inParams, null);
+                break;
+            case Artist:
+                mMusicDatabase.getWritableDatabase().delete(ArtistSortColumns.TABLE_NAME,
+                        ArtistSortColumns.ID + " IN " + inParams, null);
+                break;
+        }
+    }
+
+    private void addIds(Collection<Long> ids, SortParameter idType) {
+        StringBuilder builder = new StringBuilder();
+        switch (idType) {
+            case Song:
+                builder.append(AudioColumns._ID);
+                break;
+            case Album:
+                builder.append(AudioColumns.ALBUM_ID);
+                break;
+            case Artist:
+                builder.append(AudioColumns.ARTIST_ID);
+                break;
+        }
+
+        builder.append(" IN (");
+        builder.append(MusicUtils.buildCollectionAsString(ids));
+        builder.append(")");
+
+        updateLocalizedStore(mMusicDatabase.getWritableDatabase(), builder.toString());
+    }
+
+    private static String createJoin(String tableName, String firstParam, String secondParam) {
+        return " JOIN " + tableName + " ON (" + firstParam + "=" + secondParam + ")";
+    }
+
+    private static String createOrderBy(String first, String second, boolean descending) {
+        String desc = descending ? " DESC" : "";
+        return first + desc + "," + second + desc;
+    }
+
+    private static final class SongSortColumns {
+        /* Table name */
+        public static final String TABLE_NAME = "song_sort";
+
+        /* Song IDs column */
+        public static final String ID = "id";
+
+        /* Artist IDs column */
+        public static final String ARTIST_ID = "artist_id";
+
+        /* Album IDs column */
+        public static final String ALBUM_ID = "album_id";
+
+        /* The Song name */
+        public static final String NAME = "song_name";
+
+        /* The label assigned (categorization buckets - typically the first letter) */
+        public static final String NAME_LABEL = "song_name_label";
+
+        /* The numerical index of the bucket */
+        public static final String NAME_BUCKET = "song_name_bucket";
+
+        /* Used for joins */
+        public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
+
+        public static String getOrderBy(boolean descending) {
+            return createOrderBy(NAME_BUCKET, NAME, descending);
+        }
+    }
+
+    private static final class AlbumSortColumns {
+
+        /* Table name */
+        public static final String TABLE_NAME = "album_sort";
+
+        /* Album IDs column */
+        public static final String ID = "id";
+
+        /* Artist IDs column */
+        public static final String ARTIST_ID = "artist_id";
+
+        /* The Album name */
+        public static final String NAME = "album_name";
+
+        /* The label assigned (categorization buckets - typically the first letter) */
+        public static final String NAME_LABEL = "album_name_label";
+
+        /* The numerical index of the bucket */
+        public static final String NAME_BUCKET = "album_name_bucket";
+
+        /* Used for joins */
+        public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
+
+        public static String getOrderBy(boolean descending) {
+            return createOrderBy(NAME_BUCKET, NAME, descending);
+        }
+    }
+
+
+    private static final class ArtistSortColumns {
+
+        /* Table name */
+        public static final String TABLE_NAME = "artist_sort";
+
+        /* Artist IDs column */
+        public static final String ID = "id";
+
+        /* The Artist name */
+        public static final String NAME = "artist_name";
+
+        /* The label assigned (categorization buckets - typically the first letter) */
+        public static final String NAME_LABEL = "artist_name_label";
+
+        /* The numerical index of the bucket */
+        public static final String NAME_BUCKET = "artist_name_bucket";
+
+        /* Used for joins */
+        public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
+
+        public static String getOrderBy(boolean descending) {
+            return createOrderBy(NAME_BUCKET, NAME, descending);
+        }
+    }
+
+}
index 1b7d3cb..8090626 100644 (file)
@@ -28,12 +28,13 @@ public class MusicDB extends SQLiteOpenHelper {
      * v2 Oct 7 2014    Added a new class MusicPlaybackState - need to bump version so the new
      *                  tables are created, but need to remove all drops from other classes to
      *                  maintain data
-     *
+     * v3 Dec 4 2014    Add Sorting tables similar to Contacts to enable other languages like
+     *                  Chinese to properly sort as they would expect
      */
 
 
     /* Version constant to increment when the database should be rebuilt */
-    private static final int VERSION = 2;
+    private static final int VERSION = 3;
 
     /* Name of database file */
     public static final String DATABASENAME = "musicdb.db";
@@ -42,7 +43,6 @@ public class MusicDB extends SQLiteOpenHelper {
 
     private final Context mContext;
 
-
     /**
      * @param context The {@link android.content.Context} to use
      * @return A new instance of this class.
@@ -62,30 +62,36 @@ public class MusicDB extends SQLiteOpenHelper {
 
     @Override
     public void onCreate(SQLiteDatabase db) {
+        PropertiesStore.getInstance(mContext).onCreate(db);
         PlaylistArtworkStore.getInstance(mContext).onCreate(db);
         RecentStore.getInstance(mContext).onCreate(db);
         SongPlayCount.getInstance(mContext).onCreate(db);
         SearchHistory.getInstance(mContext).onCreate(db);
         MusicPlaybackState.getInstance(mContext).onCreate(db);
+        LocalizedStore.getInstance(mContext).onCreate(db);
     }
 
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        PropertiesStore.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
         PlaylistArtworkStore.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
         RecentStore.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
         SongPlayCount.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
         SearchHistory.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
         MusicPlaybackState.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
+        LocalizedStore.getInstance(mContext).onUpgrade(db, oldVersion, newVersion);
     }
 
     @Override
     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         Log.w(MusicDB.class.getSimpleName(),
                 "Downgrading from: " + oldVersion + " to " + newVersion + ". Dropping tables");
+        PropertiesStore.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
         PlaylistArtworkStore.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
         RecentStore.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
         SongPlayCount.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
         SearchHistory.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
         MusicPlaybackState.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
+        LocalizedStore.getInstance(mContext).onDowngrade(db, oldVersion, newVersion);
     }
 }
index ca5e404..fbfca28 100644 (file)
@@ -78,7 +78,7 @@ public class MusicPlaybackState {
 
     public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
         // this table was created in version 2 so call the onCreate method if we hit that scenario
-        if (oldVersion == 1 && newVersion > 1) {
+        if (oldVersion < 2 && newVersion >= 2) {
             onCreate(db);
         }
     }
diff --git a/src/com/cyanogenmod/eleven/provider/PropertiesStore.java b/src/com/cyanogenmod/eleven/provider/PropertiesStore.java
new file mode 100644 (file)
index 0000000..b22de68
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Copyright (C) 2009 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.cyanogenmod.eleven.provider;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+public class PropertiesStore {
+    private final MusicDB mMusicDatabase;
+    private static PropertiesStore sInstance = null;
+
+    public static final synchronized PropertiesStore getInstance(final Context context) {
+        if (sInstance == null) {
+            sInstance = new PropertiesStore(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    private PropertiesStore(final Context context) {
+        mMusicDatabase = MusicDB.getInstance(context);
+    }
+
+    public void onCreate(final SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE IF NOT EXISTS " + PropertiesColumns.TABLE_NAME + "(" +
+                PropertiesColumns.PROPERTY_KEY + " STRING PRIMARY KEY," +
+                PropertiesColumns.PROPERTY_VALUE + " TEXT);");
+    }
+
+    public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+        // this table was created in version 3 so call the onCreate method if we hit that scenario
+        if (oldVersion < 3 && newVersion >= 3) {
+            onCreate(db);
+        }
+    }
+
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // If we ever have downgrade, drop the table to be safe
+        db.execSQL("DROP TABLE IF EXISTS " + PropertiesColumns.TABLE_NAME);
+        onCreate(db);
+    }
+
+    public String getProperty(String key) {
+        return getProperty(key, null);
+    }
+
+    public String getProperty(String key, String defaultValue) {
+        Cursor cursor = mMusicDatabase.getReadableDatabase().query(PropertiesColumns.TABLE_NAME,
+                new String[] { PropertiesColumns.PROPERTY_VALUE },
+                PropertiesColumns.PROPERTY_KEY + "=?",
+                new String[] { key }, null, null, null);
+        try {
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getString(0);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+                cursor = null;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    public void storeProperty(String key, String value) {
+        ContentValues values = new ContentValues(2);
+        values.put(PropertiesColumns.PROPERTY_KEY, key);
+        values.put(PropertiesColumns.PROPERTY_VALUE, value);
+        mMusicDatabase.getWritableDatabase().replace(PropertiesColumns.TABLE_NAME,
+                null, values);
+    }
+
+    public interface DbProperties {
+        String ICU_VERSION = "icu_version";
+        String LOCALE = "locale";
+    }
+
+    private static final class PropertiesColumns {
+        public static final String TABLE_NAME = "properties";
+        public static final String PROPERTY_KEY = "property_key";
+        public static final String PROPERTY_VALUE = "property_value";
+    }
+}
diff --git a/src/com/cyanogenmod/eleven/utils/LocaleUtils.java b/src/com/cyanogenmod/eleven/utils/LocaleUtils.java
deleted file mode 100644 (file)
index e246cc6..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * 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.cyanogenmod.eleven.utils;
-
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Locale;
-
-import libcore.icu.AlphabeticIndex;
-import libcore.icu.AlphabeticIndex.ImmutableIndex;
-
-/**
- * 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 5ab0067..0eb4429 100644 (file)
@@ -24,6 +24,7 @@ import android.content.Intent;
 import android.content.ServiceConnection;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -50,6 +51,7 @@ import com.cyanogenmod.eleven.loaders.PlaylistLoader;
 import com.cyanogenmod.eleven.loaders.PlaylistSongLoader;
 import com.cyanogenmod.eleven.loaders.SongLoader;
 import com.cyanogenmod.eleven.loaders.TopTracksLoader;
+import com.cyanogenmod.eleven.locale.LocaleUtils;
 import com.cyanogenmod.eleven.menu.FragmentMenuItems;
 import com.cyanogenmod.eleven.model.Album;
 import com.cyanogenmod.eleven.model.AlbumArtistDetails;
@@ -61,6 +63,8 @@ import com.cyanogenmod.eleven.service.MusicPlaybackTrack;
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.WeakHashMap;
 
 /**
@@ -1684,34 +1688,17 @@ public final class MusicUtils {
      * 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) {
+    public static String getLocalizedBucketLetter(String name) {
         if (name == null || name.length() == 0) {
             return null;
         }
 
-        if (trimName) {
-            name = getTrimmedName(name);
-        }
+        name = getTrimmedName(name);
 
         if (name.length() > 0) {
-            String lbl = LocaleUtils.getInstance().getLabel(name);
-            // For now let's cap it to latin alphabet and the # sign
-            // since chinese characters are resulting in " " and other random
-            // characters but the sort doesn't match the sql sort so it is
-            // not quite sorted
-            if (lbl != null && lbl.length() > 0) {
-                char ch = lbl.charAt(0);
-                if ((ch < 'A' || ch > 'Z') && ch != '#') {
-                    return null;
-                }
-            }
-
-            if (lbl != null && lbl.length() > 0) {
-                return lbl;
-            }
+            return LocaleUtils.getInstance().getLabel(name);
         }
 
         return null;
@@ -1748,47 +1735,6 @@ public final class MusicUtils {
     }
 
     /**
-     * Determines the correct item attribute to use for a given sort request and generates the
-     * localized bucket for that attribute
-     * @param item
-     * @param sortOrder
-     * @param <T>
-     * @return
-     */
-    public static <T> String getLocalizedBucketLetterByAttribute(T item, String sortOrder) {
-        if (item instanceof Song) {
-            // we aren't 'trimming' certain attributes - a flag for such attributes
-            boolean trimName = true;
-            String attributeToLocalize = ((Song)item).mSongName;
-
-            // select Song attribute based on the sort order
-            if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST) ) {
-                attributeToLocalize = ((Song)item).mArtistName;
-                trimName = false;
-            } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM) ) {
-                attributeToLocalize = ((Song)item).mAlbumName;
-            }
-
-            return getLocalizedBucketLetter(attributeToLocalize, trimName);
-        } else if (item instanceof Artist) {
-            return getLocalizedBucketLetter(((Artist)item).mArtistName, true);
-        } else if (item instanceof Album) {
-            // we aren't 'trimming' certain attributes - a flag for such attributes
-            boolean trimName = true;
-            String attributeToLocalize = ((Album)item).mAlbumName;
-
-            if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST) ) {
-                attributeToLocalize = ((Album)item).mArtistName;
-                trimName = false;
-            }
-
-            return getLocalizedBucketLetter(attributeToLocalize, trimName);
-        }
-
-        return null;
-    }
-
-    /**
      *
      * @param sortOrder values are mostly derived from SortOrder.class or could also be any sql
      *                  order clause
@@ -1797,4 +1743,23 @@ public final class MusicUtils {
     public static boolean isSortOrderDesending(String sortOrder) {
         return sortOrder.endsWith(" DESC");
     }
+
+    /**
+     * Takes a collection of items and builds a comma-separated list of them
+     * @param items collection of items
+     * @return comma-separted list of items
+     */
+    public static final <E> String buildCollectionAsString(Collection<E> items) {
+        Iterator<E> iterator = items.iterator();
+        StringBuilder str = new StringBuilder();
+        if (iterator.hasNext()) {
+            str.append(iterator.next());
+            while (iterator.hasNext()) {
+                str.append(",");
+                str.append(iterator.next());
+            }
+        }
+
+        return str.toString();
+    }
 }
index cefc258..cc1756f 100644 (file)
@@ -146,21 +146,17 @@ public class SectionCreatorUtils {
 
         @Override
         public String createHeaderLabel(T item) {
-            final String label = MusicUtils.getLocalizedBucketLetter(getString(item), trimName());
+            final String label = MusicUtils.getLocalizedBucketLetter(getString(item));
+            return createHeaderLabel(label);
+        }
+
+        protected String createHeaderLabel(final String label) {
             if (TextUtils.isEmpty(label)) {
                 return mContext.getString(R.string.header_other);
             }
             return label;
         }
 
-        /**
-         * @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);
 
         @Override
@@ -398,6 +394,15 @@ public class SectionCreatorUtils {
                 public String getString(Artist item) {
                     return item.mArtistName;
                 }
+
+                @Override
+                public String createHeaderLabel(Artist item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
+                }
             };
         } else if (sortOrder.equals(SortOrder.ArtistSortOrder.ARTIST_NUMBER_OF_ALBUMS)) {
             sectionCreator = new SectionCreatorUtils.NumberOfAlbumsCompare<Artist>(context) {
@@ -434,6 +439,15 @@ public class SectionCreatorUtils {
                 public String getString(Album item) {
                     return item.mAlbumName;
                 }
+
+                @Override
+                public String createHeaderLabel(Album item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
+                }
             };
         } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_ARTIST)) {
             sectionCreator = new LocalizedCompare<Album>(context) {
@@ -443,8 +457,12 @@ public class SectionCreatorUtils {
                 }
 
                 @Override
-                public boolean trimName() {
-                    return false;
+                public String createHeaderLabel(Album item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
                 }
             };
         } else if (sortOrder.equals(SortOrder.AlbumSortOrder.ALBUM_NUMBER_OF_SONGS)) {
@@ -508,6 +526,15 @@ public class SectionCreatorUtils {
                 public String getString(Song item) {
                     return item.mSongName;
                 }
+
+                @Override
+                public String createHeaderLabel(Song item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
+                }
             };
         } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ALBUM)) {
             sectionCreator = new LocalizedCompare<Song>(context) {
@@ -515,6 +542,15 @@ public class SectionCreatorUtils {
                 public String getString(Song item) {
                     return item.mAlbumName;
                 }
+
+                @Override
+                public String createHeaderLabel(Song item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
+                }
             };
         } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_ARTIST)) {
             sectionCreator = new LocalizedCompare<Song>(context) {
@@ -524,8 +560,12 @@ public class SectionCreatorUtils {
                 }
 
                 @Override
-                public boolean trimName() {
-                    return false;
+                public String createHeaderLabel(Song item) {
+                    if (item.mBucketLabel != null) {
+                        return super.createHeaderLabel(item.mBucketLabel);
+                    }
+
+                    return super.createHeaderLabel(item);
                 }
             };
         } else if (sortOrder.equals(SortOrder.SongSortOrder.SONG_DURATION)) {
diff --git a/src/com/cyanogenmod/eleven/utils/SortUtils.java b/src/com/cyanogenmod/eleven/utils/SortUtils.java
deleted file mode 100644 (file)
index a2010e1..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-* Copyright (C) 2014 The CyanogenMod 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.cyanogenmod.eleven.utils;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.TreeMap;
-
-/**
- * Implementation of custom sorting routines of song list
- */
-public class SortUtils {
-
-    /**
-     * Sorts items based on the localized bucket letter they belong to and the sort order specified
-     * @param items the original list of items
-     * @param sortOrder values derived from SortOrder.class
-     * @return the new sorted list
-     */
-    public static <T> ArrayList<T> localizeSortList(ArrayList<T> items, String sortOrder) {
-        ArrayList<T> finalList = Lists.newArrayList();
-        // map of items grouped by their localized label
-        TreeMap<String, LinkedList<T>> mappedList = new TreeMap<String, LinkedList<T>>();
-
-        // list holding items that don't have a localized label
-        ArrayList<T> nonLocalizableItems = Lists.newArrayList();
-
-        for (T item : items) {
-            // get the bucket letter based on the attribute to sort by
-            String label = MusicUtils.getLocalizedBucketLetterByAttribute(item, sortOrder);
-            //divvy items based on their localized bucket letter
-            if (label != null) {
-                if (mappedList.get(label) == null) {
-                    // create new label slot to assign items
-                    mappedList.put(label, Lists.<T>newLinkedList());
-                }
-                // add item to the label's list
-                mappedList.get(label).add(item);
-            } else {
-                nonLocalizableItems.add(item);
-            }
-        }
-
-        // generate a sorted item list out of localizable items
-        boolean isDescendingSort = MusicUtils.isSortOrderDesending(sortOrder);
-        finalList.addAll(getSortedList(mappedList, isDescendingSort));
-        finalList.addAll(nonLocalizableItems);
-
-        return finalList;
-    }
-
-    /**
-     * Traverses a tree map of a divvied up list to generate a sorted list
-     * @param mappedList the bucketized list of items based on the header
-     * @param reverseOrder dictates the order in which the TreeMap is traversed (descending order
-     *                     if true)
-     * @return the combined sorted list
-     */
-    private static <T> ArrayList<T> getSortedList(TreeMap<String, LinkedList<T>> mappedList,
-                                                        boolean reverseOrder) {
-        ArrayList<T> sortedList = Lists.newArrayList();
-
-        Iterator<String> iterator = mappedList.navigableKeySet().iterator();
-        if (reverseOrder) {
-            iterator = mappedList.navigableKeySet().descendingIterator();
-        }
-
-        while (iterator.hasNext()) {
-            LinkedList<T> list = mappedList.get(iterator.next());
-            sortedList.addAll(list);
-        }
-
-        return sortedList;
-    }
-
-}