diff options
author | linus_lee <llee@cyngn.com> | 2014-12-12 01:06:59 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@cyanogenmod.org> | 2014-12-12 01:06:59 +0000 |
commit | 40030b0a59b793d2e98208471904de9d85b7f01b (patch) | |
tree | 58f5ad03c4f60a2734aee6d204460b923284f2bc /src | |
parent | aa02c18e6390439a662811889823085e2a08d2e9 (diff) | |
parent | 7124d4f4b75906869f3002906bc8dbb0d2fd2d3c (diff) | |
download | android_packages_apps_Eleven-40030b0a59b793d2e98208471904de9d85b7f01b.tar.gz android_packages_apps_Eleven-40030b0a59b793d2e98208471904de9d85b7f01b.tar.bz2 android_packages_apps_Eleven-40030b0a59b793d2e98208471904de9d85b7f01b.zip |
Merge "Add improved localized sorting (similar to contacts sorting) to Eleven" into cm-12.0
Diffstat (limited to 'src')
26 files changed, 2122 insertions, 783 deletions
diff --git a/src/com/cyanogenmod/eleven/adapters/ArtistDetailAlbumAdapter.java b/src/com/cyanogenmod/eleven/adapters/ArtistDetailAlbumAdapter.java index 1b7aed6..4670e0d 100644 --- a/src/com/cyanogenmod/eleven/adapters/ArtistDetailAlbumAdapter.java +++ b/src/com/cyanogenmod/eleven/adapters/ArtistDetailAlbumAdapter.java @@ -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 diff --git a/src/com/cyanogenmod/eleven/adapters/ArtistDetailSongAdapter.java b/src/com/cyanogenmod/eleven/adapters/ArtistDetailSongAdapter.java index b2ed587..1da8464 100644 --- a/src/com/cyanogenmod/eleven/adapters/ArtistDetailSongAdapter.java +++ b/src/com/cyanogenmod/eleven/adapters/ArtistDetailSongAdapter.java @@ -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) { diff --git a/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java b/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java index c35ad00..ab87b3f 100644 --- a/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java +++ b/src/com/cyanogenmod/eleven/cache/PlaylistWorkerTask.java @@ -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 diff --git a/src/com/cyanogenmod/eleven/loaders/AlbumLoader.java b/src/com/cyanogenmod/eleven/loaders/AlbumLoader.java index b66145a..5532a35 100644 --- a/src/com/cyanogenmod/eleven/loaders/AlbumLoader.java +++ b/src/com/cyanogenmod/eleven/loaders/AlbumLoader.java @@ -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 index 74b3dec..0000000 --- a/src/com/cyanogenmod/eleven/loaders/ArtistAlbumLoader.java +++ /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; - } - } -} diff --git a/src/com/cyanogenmod/eleven/loaders/ArtistLoader.java b/src/com/cyanogenmod/eleven/loaders/ArtistLoader.java index b5f57d4..e572c96 100644 --- a/src/com/cyanogenmod/eleven/loaders/ArtistLoader.java +++ b/src/com/cyanogenmod/eleven/loaders/ArtistLoader.java @@ -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 index 9941bfd..0000000 --- a/src/com/cyanogenmod/eleven/loaders/ArtistSongLoader.java +++ /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()); - } - -} diff --git a/src/com/cyanogenmod/eleven/loaders/SongLoader.java b/src/com/cyanogenmod/eleven/loaders/SongLoader.java index ca1d828..3037c53 100644 --- a/src/com/cyanogenmod/eleven/loaders/SongLoader.java +++ b/src/com/cyanogenmod/eleven/loaders/SongLoader.java @@ -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; } } diff --git a/src/com/cyanogenmod/eleven/loaders/SortedCursor.java b/src/com/cyanogenmod/eleven/loaders/SortedCursor.java index e73b7ce..7c2a5d0 100644 --- a/src/com/cyanogenmod/eleven/loaders/SortedCursor.java +++ b/src/com/cyanogenmod/eleven/loaders/SortedCursor.java @@ -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(); diff --git a/src/com/cyanogenmod/eleven/loaders/TopTracksLoader.java b/src/com/cyanogenmod/eleven/loaders/TopTracksLoader.java index a1d79d0..f1ea033 100644 --- a/src/com/cyanogenmod/eleven/loaders/TopTracksLoader.java +++ b/src/com/cyanogenmod/eleven/loaders/TopTracksLoader.java @@ -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 index 0000000..895bbab --- /dev/null +++ b/src/com/cyanogenmod/eleven/locale/HanziToPinyin.java @@ -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 index 0000000..3d2f99f --- /dev/null +++ b/src/com/cyanogenmod/eleven/locale/LocaleChangeReceiver.java @@ -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 index 0000000..25b2ac9 --- /dev/null +++ b/src/com/cyanogenmod/eleven/locale/LocaleSet.java @@ -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 index 0000000..8e49349 --- /dev/null +++ b/src/com/cyanogenmod/eleven/locale/LocaleSetManager.java @@ -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 index 0000000..f16a17a --- /dev/null +++ b/src/com/cyanogenmod/eleven/locale/LocaleUtils.java @@ -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(); + } +} diff --git a/src/com/cyanogenmod/eleven/model/Album.java b/src/com/cyanogenmod/eleven/model/Album.java index b84cb76..f987a6f 100644 --- a/src/com/cyanogenmod/eleven/model/Album.java +++ b/src/com/cyanogenmod/eleven/model/Album.java @@ -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 diff --git a/src/com/cyanogenmod/eleven/model/Artist.java b/src/com/cyanogenmod/eleven/model/Artist.java index ed47553..e54f684 100644 --- a/src/com/cyanogenmod/eleven/model/Artist.java +++ b/src/com/cyanogenmod/eleven/model/Artist.java @@ -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 diff --git a/src/com/cyanogenmod/eleven/model/Song.java b/src/com/cyanogenmod/eleven/model/Song.java index aac99ea..949d0fa 100644 --- a/src/com/cyanogenmod/eleven/model/Song.java +++ b/src/com/cyanogenmod/eleven/model/Song.java @@ -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 index 0000000..43367e6 --- /dev/null +++ b/src/com/cyanogenmod/eleven/provider/LocalizedStore.java @@ -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); + } + } + +} diff --git a/src/com/cyanogenmod/eleven/provider/MusicDB.java b/src/com/cyanogenmod/eleven/provider/MusicDB.java index 1b7d3cb..8090626 100644 --- a/src/com/cyanogenmod/eleven/provider/MusicDB.java +++ b/src/com/cyanogenmod/eleven/provider/MusicDB.java @@ -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); } } diff --git a/src/com/cyanogenmod/eleven/provider/MusicPlaybackState.java b/src/com/cyanogenmod/eleven/provider/MusicPlaybackState.java index ca5e404..fbfca28 100644 --- a/src/com/cyanogenmod/eleven/provider/MusicPlaybackState.java +++ b/src/com/cyanogenmod/eleven/provider/MusicPlaybackState.java @@ -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 index 0000000..b22de68 --- /dev/null +++ b/src/com/cyanogenmod/eleven/provider/PropertiesStore.java @@ -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 index e246cc6..0000000 --- a/src/com/cyanogenmod/eleven/utils/LocaleUtils.java +++ /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(); - } -} diff --git a/src/com/cyanogenmod/eleven/utils/MusicUtils.java b/src/com/cyanogenmod/eleven/utils/MusicUtils.java index 5ab0067..0eb4429 100644 --- a/src/com/cyanogenmod/eleven/utils/MusicUtils.java +++ b/src/com/cyanogenmod/eleven/utils/MusicUtils.java @@ -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(); + } } diff --git a/src/com/cyanogenmod/eleven/utils/SectionCreatorUtils.java b/src/com/cyanogenmod/eleven/utils/SectionCreatorUtils.java index cefc258..cc1756f 100644 --- a/src/com/cyanogenmod/eleven/utils/SectionCreatorUtils.java +++ b/src/com/cyanogenmod/eleven/utils/SectionCreatorUtils.java @@ -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 index a2010e1..0000000 --- a/src/com/cyanogenmod/eleven/utils/SortUtils.java +++ /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; - } - -} |