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 org.lineageos.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.Build;
23 import android.os.Handler;
24 import android.os.HandlerThread;
25 import android.os.Message;
26 import android.os.SystemClock;
27 import android.provider.MediaStore;
28 import android.provider.MediaStore.Audio.AudioColumns;
29 import android.text.TextUtils;
30 import android.util.Log;
32 import org.lineageos.eleven.loaders.SortedCursor;
33 import org.lineageos.eleven.locale.LocaleSet;
34 import org.lineageos.eleven.locale.LocaleSetManager;
35 import org.lineageos.eleven.locale.LocaleUtils;
36 import org.lineageos.eleven.utils.MusicUtils;
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.List;
43 * Because sqlite localized collator isn't sufficient, we need to store more specialized logic
44 * into our own db similar to contacts db. This is most noticeable in languages like Chinese,
47 public class LocalizedStore {
48 private static final String TAG = LocalizedStore.class.getSimpleName();
49 private static final boolean DEBUG = false;
50 private static LocalizedStore sInstance = null;
52 private static final int LOCALE_CHANGED = 0;
54 private final MusicDB mMusicDatabase;
55 private final Context mContext;
56 private final ContentValues mContentValues = new ContentValues(10);
57 private final LocaleSetManager mLocaleSetManager;
59 private final HandlerThread mHandlerThread;
60 private final Handler mHandler;
62 public enum SortParameter {
68 private static class SortData {
70 List<String> bucketLabels;
74 * @param context The {@link android.content.Context} to use
75 * @return A new instance of this class.
77 public static final synchronized LocalizedStore getInstance(final Context context) {
78 if (sInstance == null) {
79 sInstance = new LocalizedStore(context.getApplicationContext());
84 private LocalizedStore(final Context context) {
85 mMusicDatabase = MusicDB.getInstance(context);
87 mLocaleSetManager = new LocaleSetManager(mContext);
89 mHandlerThread = new HandlerThread("LocalizedStoreWorker",
90 android.os.Process.THREAD_PRIORITY_BACKGROUND);
91 mHandlerThread.start();
92 mHandler = new Handler(mHandlerThread.getLooper()) {
94 public void handleMessage(Message msg) {
95 if (msg.what == LOCALE_CHANGED && mLocaleSetManager.localeSetNeedsUpdate()) {
96 rebuildLocaleData(mLocaleSetManager.getSystemLocaleSet());
101 // check to see if locale has changed
105 public void onCreate(final SQLiteDatabase db) {
107 String[] tables = new String[]{
108 "CREATE TABLE IF NOT EXISTS " + SongSortColumns.TABLE_NAME + "(" +
109 SongSortColumns.ID + " INTEGER PRIMARY KEY," +
110 SongSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
111 SongSortColumns.ALBUM_ID + " INTEGER NOT NULL," +
112 SongSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
113 SongSortColumns.NAME_LABEL + " TEXT," +
114 SongSortColumns.NAME_BUCKET + " INTEGER);",
116 "CREATE TABLE IF NOT EXISTS " + AlbumSortColumns.TABLE_NAME + "(" +
117 AlbumSortColumns.ID + " INTEGER PRIMARY KEY," +
118 AlbumSortColumns.ARTIST_ID + " INTEGER NOT NULL," +
119 AlbumSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
120 AlbumSortColumns.NAME_LABEL + " TEXT," +
121 AlbumSortColumns.NAME_BUCKET + " INTEGER);",
123 "CREATE TABLE IF NOT EXISTS " + ArtistSortColumns.TABLE_NAME + "(" +
124 ArtistSortColumns.ID + " INTEGER PRIMARY KEY," +
125 ArtistSortColumns.NAME + " TEXT COLLATE LOCALIZED," +
126 ArtistSortColumns.NAME_LABEL + " TEXT," +
127 ArtistSortColumns.NAME_BUCKET + " INTEGER);",
130 for (String table : tables) {
132 Log.d(TAG, "Creating table: " + table);
138 public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
139 // this table was created in version 3 so call the onCreate method if oldVersion <= 2
140 // in version 4 we need to recreate the SongSortcolumns table so drop the table and call
141 // onCreate if oldVersion <= 3
142 if (oldVersion <= 3) {
143 db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
148 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
149 // If we ever have downgrade, drop the table to be safe
150 db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
151 db.execSQL("DROP TABLE IF EXISTS " + AlbumSortColumns.TABLE_NAME);
152 db.execSQL("DROP TABLE IF EXISTS " + ArtistSortColumns.TABLE_NAME);
156 public void onLocaleChanged() {
157 mHandler.obtainMessage(LOCALE_CHANGED).sendToTarget();
160 private void rebuildLocaleData(LocaleSet locales) {
162 Log.d(TAG, "Locale has changed, rebuilding sorting data");
165 final long start = SystemClock.elapsedRealtime();
166 final SQLiteDatabase db = mMusicDatabase.getWritableDatabase();
167 db.beginTransaction();
169 db.execSQL("DELETE FROM " + SongSortColumns.TABLE_NAME);
170 db.execSQL("DELETE FROM " + AlbumSortColumns.TABLE_NAME);
171 db.execSQL("DELETE FROM " + ArtistSortColumns.TABLE_NAME);
173 // prep the localization classes
174 mLocaleSetManager.updateLocaleSet(locales);
176 updateLocalizedStore(db, null);
178 // Update the ICU version used to generate the locale derived data
179 // so we can tell when we need to rebuild with new ICU versions.
180 // But assume that ICU versions are only able to change on Android version upgrades and
181 // use SDK INT as identifier.
182 PropertiesStore.getInstance(mContext).storeProperty(
183 PropertiesStore.DbProperties.ICU_VERSION, String.valueOf(Build.VERSION.SDK_INT));
184 PropertiesStore.getInstance(mContext).storeProperty(PropertiesStore.DbProperties.LOCALE,
187 db.setTransactionSuccessful();
193 Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
198 * This will grab all the songs from the medistore and add the localized data to the db
199 * @param selection if we only want to do this for some songs, this selection will filter it out
201 private void updateLocalizedStore(final SQLiteDatabase db, final String selection) {
202 db.beginTransaction();
204 Cursor cursor = null;
207 final String combinedSelection = MusicUtils.MUSIC_ONLY_SELECTION +
208 (TextUtils.isEmpty(selection) ? "" : " AND " + selection);
210 // order by artist/album/id to minimize artist/album re-inserts
211 final String orderBy = AudioColumns.ARTIST_ID + "," + AudioColumns.ALBUM + ","
215 Log.d(TAG, "Running selection query: " + combinedSelection);
218 cursor = mContext.getContentResolver().query(
219 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
226 AudioColumns.ARTIST_ID,
230 AudioColumns.ALBUM_ID,
233 }, combinedSelection, null, orderBy);
235 long previousArtistId = -1;
236 long previousAlbumId = -1;
240 if (cursor != null && cursor.moveToFirst()) {
242 albumId = cursor.getLong(4);
243 artistId = cursor.getLong(2);
245 if (artistId != previousArtistId) {
246 previousArtistId = artistId;
247 updateArtistData(db, artistId, cursor.getString(3));
250 if (albumId != previousAlbumId) {
251 previousAlbumId = albumId;
253 updateAlbumData(db, albumId, cursor.getString(5), artistId);
256 updateSongData(db, cursor.getLong(0), cursor.getString(1), artistId, albumId);
257 } while (cursor.moveToNext());
260 if (cursor != null) {
266 db.setTransactionSuccessful();
272 private void updateArtistData(SQLiteDatabase db, long id, String name) {
273 mContentValues.clear();
274 name = MusicUtils.getTrimmedName(name);
276 final LocaleUtils localeUtils = LocaleUtils.getInstance();
277 final int bucketIndex = localeUtils.getBucketIndex(name);
279 mContentValues.put(ArtistSortColumns.ID, id);
280 mContentValues.put(ArtistSortColumns.NAME, name);
281 mContentValues.put(ArtistSortColumns.NAME_BUCKET, bucketIndex);
282 mContentValues.put(ArtistSortColumns.NAME_LABEL,
283 localeUtils.getBucketLabel(bucketIndex));
285 db.insertWithOnConflict(ArtistSortColumns.TABLE_NAME, null, mContentValues,
286 SQLiteDatabase.CONFLICT_IGNORE);
289 private void updateAlbumData(SQLiteDatabase db, long id, String name, long artistId) {
290 mContentValues.clear();
291 name = MusicUtils.getTrimmedName(name);
293 final LocaleUtils localeUtils = LocaleUtils.getInstance();
294 final int bucketIndex = localeUtils.getBucketIndex(name);
296 mContentValues.put(AlbumSortColumns.ID, id);
297 mContentValues.put(AlbumSortColumns.NAME, name);
298 mContentValues.put(AlbumSortColumns.NAME_BUCKET, bucketIndex);
299 mContentValues.put(AlbumSortColumns.NAME_LABEL,
300 localeUtils.getBucketLabel(bucketIndex));
301 mContentValues.put(AlbumSortColumns.ARTIST_ID, artistId);
303 db.insertWithOnConflict(AlbumSortColumns.TABLE_NAME, null, mContentValues,
304 SQLiteDatabase.CONFLICT_IGNORE);
307 private void updateSongData(SQLiteDatabase db, long id, String name, long artistId,
309 mContentValues.clear();
310 name = MusicUtils.getTrimmedName(name);
312 final LocaleUtils localeUtils = LocaleUtils.getInstance();
313 final int bucketIndex = localeUtils.getBucketIndex(name);
315 mContentValues.put(SongSortColumns.ID, id);
316 mContentValues.put(SongSortColumns.NAME, name);
317 mContentValues.put(SongSortColumns.NAME_BUCKET, bucketIndex);
318 mContentValues.put(SongSortColumns.NAME_LABEL,
319 localeUtils.getBucketLabel(bucketIndex));
320 mContentValues.put(SongSortColumns.ARTIST_ID, artistId);
321 mContentValues.put(SongSortColumns.ALBUM_ID, albumId);
323 db.insertWithOnConflict(SongSortColumns.TABLE_NAME, null, mContentValues,
324 SQLiteDatabase.CONFLICT_IGNORE);
328 * Gets the list of saved ids and labels for the itemType in localized sorted order
329 * @param itemType the type of item we're querying for (artists, albums, songs)
330 * @param sortType the type we want to sort by (eg songs sorted by artists,
331 * albums sorted by artists). Note some combinations don't make sense and
332 * will fallback to the basic sort, for example Artists sorted by songs
334 * @param descending Whether we want to sort ascending or descending. This will only apply to
335 * the basic searches (ie when sortType == itemType),
336 * otherwise ascending is always assumed
337 * @return sorted list of ids and bucket labels for the itemType
339 public SortData getSortOrder(SortParameter itemType, SortParameter sortType,
340 boolean descending) {
341 SortData sortData = new SortData();
342 String tableName = "";
343 String joinClause = "";
344 String selectParams = "";
345 String postfixOrder = "";
346 String prefixOrder = "";
350 selectParams = SongSortColumns.CONCRETE_ID + ",";
351 postfixOrder = SongSortColumns.getOrderBy(descending);
352 tableName = SongSortColumns.TABLE_NAME;
354 if (sortType == SortParameter.Artist) {
355 selectParams += ArtistSortColumns.NAME_LABEL;
356 prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
357 joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
358 SongSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
359 } else if (sortType == SortParameter.Album) {
360 selectParams += AlbumSortColumns.NAME_LABEL;
361 prefixOrder = AlbumSortColumns.getOrderBy(false) + ",";
362 joinClause = createJoin(AlbumSortColumns.TABLE_NAME,
363 SongSortColumns.ALBUM_ID, AlbumSortColumns.CONCRETE_ID);
365 selectParams += SongSortColumns.NAME_LABEL;
369 selectParams = ArtistSortColumns.CONCRETE_ID + "," + ArtistSortColumns.NAME_LABEL;
370 postfixOrder = ArtistSortColumns.getOrderBy(descending);
371 tableName = ArtistSortColumns.TABLE_NAME;
374 selectParams = AlbumSortColumns.CONCRETE_ID + ",";
375 postfixOrder = AlbumSortColumns.getOrderBy(descending);
376 tableName = AlbumSortColumns.TABLE_NAME;
377 if (sortType == SortParameter.Artist) {
378 selectParams += AlbumSortColumns.NAME_LABEL;
379 prefixOrder = ArtistSortColumns.getOrderBy(false) + ",";
380 joinClause = createJoin(ArtistSortColumns.TABLE_NAME,
381 AlbumSortColumns.ARTIST_ID, ArtistSortColumns.CONCRETE_ID);
383 selectParams += AlbumSortColumns.NAME_LABEL;
388 final String selection = "SELECT " + selectParams
389 + " FROM " + tableName
391 + " ORDER BY " + prefixOrder + postfixOrder;
394 Log.d(TAG, "Running selection: " + selection);
399 c = mMusicDatabase.getReadableDatabase().rawQuery(selection, null);
401 if (c != null && c.moveToFirst()) {
402 sortData.ids = new long[c.getCount()];
403 sortData.bucketLabels = new ArrayList<String>(c.getCount());
405 sortData.ids[c.getPosition()] = c.getLong(0);
406 sortData.bucketLabels.add(c.getString(1));
407 } while (c.moveToNext());
419 * Wraps the cursor with a sorted cursor that sorts it in the proper localized order
420 * @param cursor underlying cursor to sort
421 * @param columnName the column name of the id
422 * @param idType the type of item that the cursor contains
423 * @param sortType the type to sort by (for example can be song sorted by albums)
424 * @param descending descending?
425 * @param update do we want to update any discrepencies we find - only should be true if the
426 * cursor contains all songs/artists/albums and not a subset
427 * @return the sorted cursor
429 public Cursor getLocalizedSort(Cursor cursor, String columnName, SortParameter idType,
430 SortParameter sortType, boolean descending, boolean update) {
431 if (cursor != null) {
432 SortedCursor sortedCursor = null;
434 // iterate up to twice if there are discrepancies found
435 for (int i = 0; i < 2; i++) {
436 // get the sort order for the sort parameter
437 SortData sortData = getSortOrder(idType, sortType, descending);
439 // get the sorted cursor based on the sort
440 sortedCursor = new SortedCursor(cursor, sortData.ids, columnName,
441 sortData.bucketLabels);
443 if (!update || !updateDiscrepancies(sortedCursor, idType)) {
455 * Updates the localized store based on the cursor
456 * @param sortedCursor the current sorting cursor based on the LocalizedStore sort
457 * @param type the item type in the cursor
458 * @return true if there are new ids in the cursor that aren't tracked in the store
460 private boolean updateDiscrepancies(SortedCursor sortedCursor, SortParameter type) {
461 boolean hasNewIds = false;
463 final ArrayList<Long> missingIds = sortedCursor.getMissingIds();
464 if (missingIds.size() > 0) {
465 removeIds(missingIds, type);
468 final Collection<Long> extraIds = sortedCursor.getExtraIds();
469 if (extraIds != null && extraIds.size() > 0) {
470 addIds(extraIds, type);
477 private void removeIds(ArrayList<Long> ids, SortParameter idType) {
478 if (ids == null || ids.size() == 0) {
482 final String inParams = "(" + MusicUtils.buildCollectionAsString(ids) + ")";
485 Log.d(TAG, "Deleting from " + idType + " where id is in " + inParams);
490 mMusicDatabase.getWritableDatabase().delete(SongSortColumns.TABLE_NAME,
491 SongSortColumns.ID + " IN " + inParams, null);
494 mMusicDatabase.getWritableDatabase().delete(AlbumSortColumns.TABLE_NAME,
495 AlbumSortColumns.ID + " IN " + inParams, null);
498 mMusicDatabase.getWritableDatabase().delete(ArtistSortColumns.TABLE_NAME,
499 ArtistSortColumns.ID + " IN " + inParams, null);
504 private void addIds(Collection<Long> ids, SortParameter idType) {
505 StringBuilder builder = new StringBuilder();
508 builder.append(AudioColumns._ID);
511 builder.append(AudioColumns.ALBUM_ID);
514 builder.append(AudioColumns.ARTIST_ID);
518 builder.append(" IN (");
519 builder.append(MusicUtils.buildCollectionAsString(ids));
522 updateLocalizedStore(mMusicDatabase.getWritableDatabase(), builder.toString());
525 private static String createJoin(String tableName, String firstParam, String secondParam) {
526 return " JOIN " + tableName + " ON (" + firstParam + "=" + secondParam + ")";
529 private static String createOrderBy(String first, String second, boolean descending) {
530 String desc = descending ? " DESC" : "";
531 return first + desc + "," + second + desc;
534 private static final class SongSortColumns {
536 public static final String TABLE_NAME = "song_sort";
538 /* Song IDs column */
539 public static final String ID = "id";
541 /* Artist IDs column */
542 public static final String ARTIST_ID = "artist_id";
544 /* Album IDs column */
545 public static final String ALBUM_ID = "album_id";
548 public static final String NAME = "song_name";
550 /* The label assigned (categorization buckets - typically the first letter) */
551 public static final String NAME_LABEL = "song_name_label";
553 /* The numerical index of the bucket */
554 public static final String NAME_BUCKET = "song_name_bucket";
557 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
559 public static String getOrderBy(boolean descending) {
560 return createOrderBy(NAME_BUCKET, NAME, descending);
564 private static final class AlbumSortColumns {
567 public static final String TABLE_NAME = "album_sort";
569 /* Album IDs column */
570 public static final String ID = "id";
572 /* Artist IDs column */
573 public static final String ARTIST_ID = "artist_id";
576 public static final String NAME = "album_name";
578 /* The label assigned (categorization buckets - typically the first letter) */
579 public static final String NAME_LABEL = "album_name_label";
581 /* The numerical index of the bucket */
582 public static final String NAME_BUCKET = "album_name_bucket";
585 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
587 public static String getOrderBy(boolean descending) {
588 return createOrderBy(NAME_BUCKET, NAME, descending);
593 private static final class ArtistSortColumns {
596 public static final String TABLE_NAME = "artist_sort";
598 /* Artist IDs column */
599 public static final String ID = "id";
601 /* The Artist name */
602 public static final String NAME = "artist_name";
604 /* The label assigned (categorization buckets - typically the first letter) */
605 public static final String NAME_LABEL = "artist_name_label";
607 /* The numerical index of the bucket */
608 public static final String NAME_BUCKET = "artist_name_bucket";
611 public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
613 public static String getOrderBy(boolean descending) {
614 return createOrderBy(NAME_BUCKET, NAME, descending);