2 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
16 package com.cyanogenmod.eleven.provider;
18 import android.content.ContentValues;
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.database.sqlite.SQLiteDatabase;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Message;
25 import android.os.SystemClock;
26 import android.provider.MediaStore;
27 import android.provider.MediaStore.Audio.AudioColumns;
28 import android.text.TextUtils;
29 import android.util.Log;
31 import com.cyanogenmod.eleven.loaders.SortedCursor;
32 import com.cyanogenmod.eleven.locale.LocaleSet;
33 import com.cyanogenmod.eleven.locale.LocaleSetManager;
34 import com.cyanogenmod.eleven.locale.LocaleUtils;
35 import com.cyanogenmod.eleven.utils.MusicUtils;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.List;
41 import libcore.icu.ICU;
44 * Because sqlite localized collator isn't sufficient, we need to store more specialized logic
45 * into our own db similar to contacts db. This is most noticeable in languages like Chinese,
48 public class LocalizedStore {
49 private static final String TAG = LocalizedStore.class.getSimpleName();
50 private static final boolean DEBUG = false;
51 private static LocalizedStore sInstance = null;
53 private static final int LOCALE_CHANGED = 0;
55 private final MusicDB mMusicDatabase;
56 private final Context mContext;
57 private final ContentValues mContentValues = new ContentValues(10);
58 private final LocaleSetManager mLocaleSetManager;
60 private final HandlerThread mHandlerThread;
61 private final Handler mHandler;
63 public enum SortParameter {
69 private static class SortData {
71 List<String> bucketLabels;
75 * @param context The {@link android.content.Context} to use
76 * @return A new instance of this class.
78 public static final synchronized LocalizedStore getInstance(final Context context) {
79 if (sInstance == null) {
80 sInstance = new LocalizedStore(context.getApplicationContext());
85 private LocalizedStore(final Context context) {
86 mMusicDatabase = MusicDB.getInstance(context);
88 mLocaleSetManager = new LocaleSetManager(mContext);
90 mHandlerThread = new HandlerThread("LocalizedStoreWorker",
91 android.os.Process.THREAD_PRIORITY_BACKGROUND);
92 mHandlerThread.start();
93 mHandler = new Handler(mHandlerThread.getLooper()) {
95 public void handleMessage(Message msg) {
96 if (msg.what == LOCALE_CHANGED && mLocaleSetManager.localeSetNeedsUpdate()) {
97 rebuildLocaleData(mLocaleSetManager.getSystemLocaleSet());
102 // check to see if locale has changed
106 public void onCreate(final SQLiteDatabase db) {
108 String[] tables = new String[]{
109 "CREATE TABLE IF NOT EXISTS " + SongSortColumns.TABLE_NAME + "(" +
110 SongSortColumns.ID + " INTEGER PRIMARY KEY," +
111 SongSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
112 SongSortColumns.ALBUM_ID + " INTEGER NOT NULL," +
113 SongSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
114 SongSortColumns.NAME_LABEL + " TEXT," +
115 SongSortColumns.NAME_BUCKET + " INTEGER);",
117 "CREATE TABLE IF NOT EXISTS " + AlbumSortColumns.TABLE_NAME + "(" +
118 AlbumSortColumns.ID + " INTEGER PRIMARY KEY," +
119 AlbumSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
120 AlbumSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
121 AlbumSortColumns.NAME_LABEL + " TEXT," +
122 AlbumSortColumns.NAME_BUCKET + " INTEGER);",
124 "CREATE TABLE IF NOT EXISTS " + ArtistSortColumns.TABLE_NAME + "(" +
125 ArtistSortColumns.ID + " INTEGER PRIMARY KEY," +
126 ArtistSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
127 ArtistSortColumns.NAME_LABEL + " TEXT," +
128 ArtistSortColumns.NAME_BUCKET + " INTEGER);",
131 for (String table : tables) {
133 Log.d(TAG, "Creating table: " + table);
139 public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
140 // this table was created in version 3 so call the onCreate method if oldVersion <= 2
141 // in version 4 we need to recreate the SongSortcolumns table so drop the table and call
142 // onCreate if oldVersion <= 3
143 if (oldVersion <= 3) {
144 db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
149 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
150 // If we ever have downgrade, drop the table to be safe
151 db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
152 db.execSQL("DROP TABLE IF EXISTS " + AlbumSortColumns.TABLE_NAME);
153 db.execSQL("DROP TABLE IF EXISTS " + ArtistSortColumns.TABLE_NAME);
157 public void onLocaleChanged() {
158 mHandler.obtainMessage(LOCALE_CHANGED).sendToTarget();
161 private void rebuildLocaleData(LocaleSet locales) {
163 Log.d(TAG, "Locale has changed, rebuilding sorting data");
166 final long start = SystemClock.elapsedRealtime();
167 final SQLiteDatabase db = mMusicDatabase.getWritableDatabase();
168 db.beginTransaction();
170 db.execSQL("DELETE FROM " + SongSortColumns.TABLE_NAME);
171 db.execSQL("DELETE FROM " + AlbumSortColumns.TABLE_NAME);
172 db.execSQL("DELETE FROM " + ArtistSortColumns.TABLE_NAME);
174 // prep the localization classes
175 mLocaleSetManager.updateLocaleSet(locales);
177 updateLocalizedStore(db, null);
179 // Update the ICU version used to generate the locale derived data
180 // so we can tell when we need to rebuild with new ICU versions.
181 PropertiesStore.getInstance(mContext).storeProperty(
182 PropertiesStore.DbProperties.ICU_VERSION, ICU.getIcuVersion());
183 PropertiesStore.getInstance(mContext).storeProperty(PropertiesStore.DbProperties.LOCALE,
186 db.setTransactionSuccessful();
192 Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
197 * This will grab all the songs from the medistore and add the localized data to the db
198 * @param selection if we only want to do this for some songs, this selection will filter it out
200 private void updateLocalizedStore(final SQLiteDatabase db, final String selection) {
201 db.beginTransaction();
203 Cursor cursor = null;
206 final String combinedSelection = MusicUtils.MUSIC_ONLY_SELECTION +
207 (TextUtils.isEmpty(selection) ? "" : " AND " + selection);
209 // order by artist/album/id to minimize artist/album re-inserts
210 final String orderBy = AudioColumns.ARTIST_ID + "," + AudioColumns.ALBUM + ","
214 Log.d(TAG, "Running selection query: " + combinedSelection);
217 cursor = mContext.getContentResolver().query(
218 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
225 AudioColumns.ARTIST_ID,
229 AudioColumns.ALBUM_ID,
232 }, combinedSelection, null, orderBy);
234 long previousArtistId = -1;
235 long previousAlbumId = -1;
239 if (cursor != null && cursor.moveToFirst()) {
241 albumId = cursor.getLong(4);
242 artistId = cursor.getLong(2);
244 if (artistId != previousArtistId) {
245 previousArtistId = artistId;
246 updateArtistData(db, artistId, cursor.getString(3));
249 if (albumId != previousAlbumId) {
250 previousAlbumId = albumId;
252 updateAlbumData(db, albumId, cursor.getString(5), artistId);
255 updateSongData(db, cursor.getLong(0), cursor.getString(1), artistId, albumId);
256 } while (cursor.moveToNext());
259 if (cursor != null) {
265 db.setTransactionSuccessful();
271 private void updateArtistData(SQLiteDatabase db, long id, String name) {
272 mContentValues.clear();
273 name = MusicUtils.getTrimmedName(name);
275 final LocaleUtils localeUtils = LocaleUtils.getInstance();
276 final int bucketIndex = localeUtils.getBucketIndex(name);
278 mContentValues.put(ArtistSortColumns.ID, id);
279 mContentValues.put(ArtistSortColumns.NAME, name);
280 mContentValues.put(ArtistSortColumns.NAME_BUCKET, bucketIndex);
281 mContentValues.put(ArtistSortColumns.NAME_LABEL,
282 localeUtils.getBucketLabel(bucketIndex));
284 db.insertWithOnConflict(ArtistSortColumns.TABLE_NAME, null, mContentValues,
285 SQLiteDatabase.CONFLICT_IGNORE);
288 private void updateAlbumData(SQLiteDatabase db, long id, String name, long artistId) {
289 mContentValues.clear();
290 name = MusicUtils.getTrimmedName(name);
292 final LocaleUtils localeUtils = LocaleUtils.getInstance();
293 final int bucketIndex = localeUtils.getBucketIndex(name);
295 mContentValues.put(AlbumSortColumns.ID, id);
296 mContentValues.put(AlbumSortColumns.NAME, name);
297 mContentValues.put(AlbumSortColumns.NAME_BUCKET, bucketIndex);
298 mContentValues.put(AlbumSortColumns.NAME_LABEL,
299 localeUtils.getBucketLabel(bucketIndex));
300 mContentValues.put(AlbumSortColumns.ARTIST_ID, artistId);
302 db.insertWithOnConflict(AlbumSortColumns.TABLE_NAME, null, mContentValues,
303 SQLiteDatabase.CONFLICT_IGNORE);
306 private void updateSongData(SQLiteDatabase db, long id, String name, long artistId,
308 mContentValues.clear();
309 name = MusicUtils.getTrimmedName(name);
311 final LocaleUtils localeUtils = LocaleUtils.getInstance();
312 final int bucketIndex = localeUtils.getBucketIndex(name);
314 mContentValues.put(SongSortColumns.ID, id);
315 mContentValues.put(SongSortColumns.NAME, name);
316 mContentValues.put(SongSortColumns.NAME_BUCKET, bucketIndex);
317 mContentValues.put(SongSortColumns.NAME_LABEL,
318 localeUtils.getBucketLabel(bucketIndex));
319 mContentValues.put(SongSortColumns.ARTIST_ID, artistId);
320 mContentValues.put(SongSortColumns.ALBUM_ID, albumId);
322 db.insertWithOnConflict(SongSortColumns.TABLE_NAME, null, mContentValues,
323 SQLiteDatabase.CONFLICT_IGNORE);
327 * Gets the list of saved ids and labels for the itemType in localized sorted order
328 * @param itemType the type of item we're querying for (artists, albums, songs)
329 * @param sortType the type we want to sort by (eg songs sorted by artists,
330 * albums sorted by artists). Note some combinations don't make sense and
331 * will fallback to the basic sort, for example Artists sorted by songs
333 * @param descending Whether we want to sort ascending or descending. This will only apply to
334 * the basic searches (ie when sortType == itemType),
335 * otherwise ascending is always assumed
336 * @return sorted list of ids and bucket labels for the itemType
338 public SortData getSortOrder(SortParameter itemType, SortParameter sortType,
339 boolean descending) {
340 SortData sortData = new SortData();
341 String tableName = "";
342 String joinClause = "";
343 String selectParams = "";
344 String postfixOrder = "";
345 String prefixOrder = "";
349 selectParams = SongSortColumns.CONCRETE_ID + ",";
350 postfixOrder = SongSortColumns.getOrderBy(descending);
351 tableName = SongSortColumns.TABLE_NAME;
353 if (sortType == SortParameter.Artist) {
354 selectParams += ArtistSortColumns.NAME_LABEL;
355 prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
356 joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
357 SongSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
358 } else if (sortType == SortParameter.Album) {
359 selectParams += AlbumSortColumns.NAME_LABEL;
360 prefixOrder = AlbumSortColumns.getOrderBy(false) + ",";
361 joinClause = createJoin(AlbumSortColumns.TABLE_NAME,
362 SongSortColumns.ALBUM_ID, AlbumSortColumns.CONCRETE_ID);
364 selectParams += SongSortColumns.NAME_LABEL;
368 selectParams = ArtistSortColumns.CONCRETE_ID + "," + ArtistSortColumns.NAME_LABEL;
369 postfixOrder = ArtistSortColumns.getOrderBy(descending);
370 tableName = ArtistSortColumns.TABLE_NAME;
373 selectParams = AlbumSortColumns.CONCRETE_ID + ",";
374 postfixOrder = AlbumSortColumns.getOrderBy(descending);
375 tableName = AlbumSortColumns.TABLE_NAME;
376 if (sortType == SortParameter.Artist) {
377 selectParams += AlbumSortColumns.NAME_LABEL;
378 prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
379 joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
380 AlbumSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
382 selectParams += AlbumSortColumns.NAME_LABEL;
387 final String selection = "SELECT " + selectParams
388 + " FROM " + tableName
390 + " ORDER BY " + prefixOrder + postfixOrder;
393 Log.d(TAG, "Running selection: " + selection);
398 c = mMusicDatabase.getReadableDatabase().rawQuery(selection, null);
400 if (c != null && c.moveToFirst()) {
401 sortData.ids = new long[c.getCount()];
402 sortData.bucketLabels = new ArrayList<String>(c.getCount());
404 sortData.ids[c.getPosition()] = c.getLong(0);
405 sortData.bucketLabels.add(c.getString(1));
406 } while (c.moveToNext());
418 * Wraps the cursor with a sorted cursor that sorts it in the proper localized order
419 * @param cursor underlying cursor to sort
420 * @param columnName the column name of the id
421 * @param idType the type of item that the cursor contains
422 * @param sortType the type to sort by (for example can be song sorted by albums)
423 * @param descending descending?
424 * @param update do we want to update any discrepencies we find - only should be true if the
425 * cursor contains all songs/artists/albums and not a subset
426 * @return the sorted cursor
428 public Cursor getLocalizedSort(Cursor cursor, String columnName, SortParameter idType,
429 SortParameter sortType, boolean descending, boolean update) {
430 if (cursor != null) {
431 SortedCursor sortedCursor = null;
433 // iterate up to twice if there are discrepancies found
434 for (int i = 0; i < 2; i++) {
435 // get the sort order for the sort parameter
436 SortData sortData = getSortOrder(idType, sortType, descending);
438 // get the sorted cursor based on the sort
439 sortedCursor = new SortedCursor(cursor, sortData.ids, columnName,
440 sortData.bucketLabels);
442 if (!update || !updateDiscrepancies(sortedCursor, idType)) {
454 * Updates the localized store based on the cursor
455 * @param sortedCursor the current sorting cursor based on the LocalizedStore sort
456 * @param type the item type in the cursor
457 * @return true if there are new ids in the cursor that aren't tracked in the store
459 private boolean updateDiscrepancies(SortedCursor sortedCursor, SortParameter type) {
460 boolean hasNewIds = false;
462 final ArrayList<Long> missingIds = sortedCursor.getMissingIds();
463 if (missingIds.size() > 0) {
464 removeIds(missingIds, type);
467 final Collection<Long> extraIds = sortedCursor.getExtraIds();
468 if (extraIds != null && extraIds.size() > 0) {
469 addIds(extraIds, type);
476 private void removeIds(ArrayList<Long> ids, SortParameter idType) {
477 if (ids == null || ids.size() == 0) {
481 final String inParams = "(" + MusicUtils.buildCollectionAsString(ids) + ")";
484 Log.d(TAG, "Deleting from " + idType + " where id is in " + inParams);
489 mMusicDatabase.getWritableDatabase().delete(SongSortColumns.TABLE_NAME,
490 SongSortColumns.ID + " IN " + inParams, null);
493 mMusicDatabase.getWritableDatabase().delete(AlbumSortColumns.TABLE_NAME,
494 AlbumSortColumns.ID + " IN " + inParams, null);
497 mMusicDatabase.getWritableDatabase().delete(ArtistSortColumns.TABLE_NAME,
498 ArtistSortColumns.ID + " IN " + inParams, null);
503 private void addIds(Collection<Long> ids, SortParameter idType) {
504 StringBuilder builder = new StringBuilder();
507 builder.append(AudioColumns._ID);
510 builder.append(AudioColumns.ALBUM_ID);
513 builder.append(AudioColumns.ARTIST_ID);
517 builder.append(" IN (");
518 builder.append(MusicUtils.buildCollectionAsString(ids));
521 updateLocalizedStore(mMusicDatabase.getWritableDatabase(), builder.toString());
524 private static String createJoin(String tableName, String firstParam, String secondParam) {
525 return " JOIN " + tableName + " ON (" + firstParam + "=" + secondParam + ")";
528 private static String createOrderBy(String first, String second, boolean descending) {
529 String desc = descending ? " DESC" : "";
530 return first + desc + "," + second + desc;
533 private static final class SongSortColumns {
535 public static final String TABLE_NAME = "song_sort";
537 /* Song IDs column */
538 public static final String ID = "id";
540 /* Artist IDs column */
541 public static final String ARTIST_ID = "artist_id";
543 /* Album IDs column */
544 public static final String ALBUM_ID = "album_id";
547 public static final String NAME = "song_name";
549 /* The label assigned (categorization buckets - typically the first letter) */
550 public static final String NAME_LABEL = "song_name_label";
552 /* The numerical index of the bucket */
553 public static final String NAME_BUCKET = "song_name_bucket";
556 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
558 public static String getOrderBy(boolean descending) {
559 return createOrderBy(NAME_BUCKET, NAME, descending);
563 private static final class AlbumSortColumns {
566 public static final String TABLE_NAME = "album_sort";
568 /* Album IDs column */
569 public static final String ID = "id";
571 /* Artist IDs column */
572 public static final String ARTIST_ID = "artist_id";
575 public static final String NAME = "album_name";
577 /* The label assigned (categorization buckets - typically the first letter) */
578 public static final String NAME_LABEL = "album_name_label";
580 /* The numerical index of the bucket */
581 public static final String NAME_BUCKET = "album_name_bucket";
584 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
586 public static String getOrderBy(boolean descending) {
587 return createOrderBy(NAME_BUCKET, NAME, descending);
592 private static final class ArtistSortColumns {
595 public static final String TABLE_NAME = "artist_sort";
597 /* Artist IDs column */
598 public static final String ID = "id";
600 /* The Artist name */
601 public static final String NAME = "artist_name";
603 /* The label assigned (categorization buckets - typically the first letter) */
604 public static final String NAME_LABEL = "artist_name_label";
606 /* The numerical index of the bucket */
607 public static final String NAME_BUCKET = "artist_name_bucket";
610 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
612 public static String getOrderBy(boolean descending) {
613 return createOrderBy(NAME_BUCKET, NAME, descending);