OSDN Git Service

d3a33d1be6db8ed106b37d03433f7fc9e5c8281f
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / provider / LocalizedStore.java
1 /*
2  * Copyright (C) 2014 The CyanogenMod Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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
15  */
16 package com.cyanogenmod.eleven.provider;
17
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;
30
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;
36
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.List;
40
41 import libcore.icu.ICU;
42
43 /**
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,
46  * Japanese etc
47  */
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;
52
53     private static final int LOCALE_CHANGED = 0;
54
55     private final MusicDB mMusicDatabase;
56     private final Context mContext;
57     private final ContentValues mContentValues = new ContentValues(10);
58     private final LocaleSetManager mLocaleSetManager;
59
60     private final HandlerThread mHandlerThread;
61     private final Handler mHandler;
62
63     public enum SortParameter {
64         Song,
65         Artist,
66         Album,
67     };
68
69     private static class SortData {
70         long[] ids;
71         List<String> bucketLabels;
72     }
73
74     /**
75      * @param context The {@link android.content.Context} to use
76      * @return A new instance of this class.
77      */
78     public static final synchronized LocalizedStore getInstance(final Context context) {
79         if (sInstance == null) {
80             sInstance = new LocalizedStore(context.getApplicationContext());
81         }
82         return sInstance;
83     }
84
85     private LocalizedStore(final Context context) {
86         mMusicDatabase = MusicDB.getInstance(context);
87         mContext = context;
88         mLocaleSetManager = new LocaleSetManager(mContext);
89
90         mHandlerThread = new HandlerThread("LocalizedStoreWorker",
91                 android.os.Process.THREAD_PRIORITY_BACKGROUND);
92         mHandlerThread.start();
93         mHandler = new Handler(mHandlerThread.getLooper()) {
94             @Override
95             public void handleMessage(Message msg) {
96                 if (msg.what == LOCALE_CHANGED && mLocaleSetManager.localeSetNeedsUpdate()) {
97                     rebuildLocaleData(mLocaleSetManager.getSystemLocaleSet());
98                 }
99             }
100         };
101
102         // check to see if locale has changed
103         onLocaleChanged();
104     }
105
106     public void onCreate(final SQLiteDatabase db) {
107
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);",
116
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);",
123
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);",
129         };
130
131         for (String table : tables) {
132             if (DEBUG) {
133                 Log.d(TAG, "Creating table: " + table);
134             }
135             db.execSQL(table);
136         }
137     }
138
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);
145             onCreate(db);
146         }
147     }
148
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);
154         onCreate(db);
155     }
156
157     public void onLocaleChanged() {
158         mHandler.obtainMessage(LOCALE_CHANGED).sendToTarget();
159     }
160
161     private void rebuildLocaleData(LocaleSet locales) {
162         if (DEBUG) {
163             Log.d(TAG, "Locale has changed, rebuilding sorting data");
164         }
165
166         final long start = SystemClock.elapsedRealtime();
167         final SQLiteDatabase db = mMusicDatabase.getWritableDatabase();
168         db.beginTransaction();
169         try {
170             db.execSQL("DELETE FROM " + SongSortColumns.TABLE_NAME);
171             db.execSQL("DELETE FROM " + AlbumSortColumns.TABLE_NAME);
172             db.execSQL("DELETE FROM " + ArtistSortColumns.TABLE_NAME);
173
174             // prep the localization classes
175             mLocaleSetManager.updateLocaleSet(locales);
176
177             updateLocalizedStore(db, null);
178
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,
184                     locales.toString());
185
186             db.setTransactionSuccessful();
187         } finally {
188             db.endTransaction();
189         }
190
191         if (DEBUG) {
192             Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
193         }
194     }
195
196     /**
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
199      */
200     private void updateLocalizedStore(final SQLiteDatabase db, final String selection) {
201         db.beginTransaction();
202         try {
203             Cursor cursor = null;
204
205             try {
206                 final String combinedSelection = MusicUtils.MUSIC_ONLY_SELECTION +
207                         (TextUtils.isEmpty(selection) ? "" : " AND " + selection);
208
209                 // order by artist/album/id to minimize artist/album re-inserts
210                 final String orderBy = AudioColumns.ARTIST_ID + "," + AudioColumns.ALBUM + ","
211                         + AudioColumns._ID;
212
213                 if (DEBUG) {
214                     Log.d(TAG, "Running selection query: " + combinedSelection);
215                 }
216
217                 cursor = mContext.getContentResolver().query(
218                         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
219                         new String[]{
220                                 // 0
221                                 AudioColumns._ID,
222                                 // 1
223                                 AudioColumns.TITLE,
224                                 // 2
225                                 AudioColumns.ARTIST_ID,
226                                 // 3
227                                 AudioColumns.ARTIST,
228                                 // 4
229                                 AudioColumns.ALBUM_ID,
230                                 // 5
231                                 AudioColumns.ALBUM,
232                         }, combinedSelection, null, orderBy);
233
234                 long previousArtistId = -1;
235                 long previousAlbumId = -1;
236                 long artistId;
237                 long albumId;
238
239                 if (cursor != null && cursor.moveToFirst()) {
240                     do {
241                         albumId = cursor.getLong(4);
242                         artistId = cursor.getLong(2);
243
244                         if (artistId != previousArtistId) {
245                             previousArtistId = artistId;
246                             updateArtistData(db, artistId, cursor.getString(3));
247                         }
248
249                         if (albumId != previousAlbumId) {
250                             previousAlbumId = albumId;
251
252                             updateAlbumData(db, albumId, cursor.getString(5), artistId);
253                         }
254
255                         updateSongData(db, cursor.getLong(0), cursor.getString(1), artistId, albumId);
256                     } while (cursor.moveToNext());
257                 }
258             } finally {
259                 if (cursor != null) {
260                     cursor.close();
261                     cursor = null;
262                 }
263             }
264
265             db.setTransactionSuccessful();
266         } finally {
267             db.endTransaction();
268         }
269     }
270
271     private void updateArtistData(SQLiteDatabase db, long id, String name) {
272         mContentValues.clear();
273         name = MusicUtils.getTrimmedName(name);
274
275         final LocaleUtils localeUtils = LocaleUtils.getInstance();
276         final int bucketIndex = localeUtils.getBucketIndex(name);
277
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));
283
284         db.insertWithOnConflict(ArtistSortColumns.TABLE_NAME, null, mContentValues,
285                 SQLiteDatabase.CONFLICT_IGNORE);
286     }
287
288     private void updateAlbumData(SQLiteDatabase db, long id, String name, long artistId) {
289         mContentValues.clear();
290         name = MusicUtils.getTrimmedName(name);
291
292         final LocaleUtils localeUtils = LocaleUtils.getInstance();
293         final int bucketIndex = localeUtils.getBucketIndex(name);
294
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);
301
302         db.insertWithOnConflict(AlbumSortColumns.TABLE_NAME, null, mContentValues,
303                 SQLiteDatabase.CONFLICT_IGNORE);
304     }
305
306     private void updateSongData(SQLiteDatabase db, long id, String name, long artistId,
307                                 long albumId) {
308         mContentValues.clear();
309         name = MusicUtils.getTrimmedName(name);
310
311         final LocaleUtils localeUtils = LocaleUtils.getInstance();
312         final int bucketIndex = localeUtils.getBucketIndex(name);
313
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);
321
322         db.insertWithOnConflict(SongSortColumns.TABLE_NAME, null, mContentValues,
323                 SQLiteDatabase.CONFLICT_IGNORE);
324     }
325
326     /**
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
332      *                 doesn't make sense
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
337      */
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 = "";
346
347         switch (itemType) {
348             case Song:
349                 selectParams = SongSortColumns.CONCRETE_ID + ",";
350                 postfixOrder = SongSortColumns.getOrderBy(descending);
351                 tableName = SongSortColumns.TABLE_NAME;
352
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);
363                 } else {
364                     selectParams += SongSortColumns.NAME_LABEL;
365                 }
366                 break;
367             case Artist:
368                 selectParams = ArtistSortColumns.CONCRETE_ID + "," + ArtistSortColumns.NAME_LABEL;
369                 postfixOrder = ArtistSortColumns.getOrderBy(descending);
370                 tableName = ArtistSortColumns.TABLE_NAME;
371                 break;
372             case Album:
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);
381                 } else {
382                     selectParams += AlbumSortColumns.NAME_LABEL;
383                 }
384                 break;
385         }
386
387         final String selection = "SELECT " + selectParams
388                 + " FROM " + tableName
389                 + joinClause
390                 + " ORDER BY " + prefixOrder + postfixOrder;
391
392         if (DEBUG) {
393             Log.d(TAG, "Running selection: " + selection);
394         }
395
396         Cursor c = null;
397         try {
398             c = mMusicDatabase.getReadableDatabase().rawQuery(selection, null);
399
400             if (c != null && c.moveToFirst()) {
401                 sortData.ids = new long[c.getCount()];
402                 sortData.bucketLabels = new ArrayList<String>(c.getCount());
403                 do {
404                     sortData.ids[c.getPosition()] = c.getLong(0);
405                     sortData.bucketLabels.add(c.getString(1));
406                 } while (c.moveToNext());
407             }
408         } finally {
409             if (c != null) {
410                 c.close();
411             }
412         }
413
414         return sortData;
415     }
416
417     /**
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
427      */
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;
432
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);
437
438                 // get the sorted cursor based on the sort
439                 sortedCursor = new SortedCursor(cursor, sortData.ids, columnName,
440                         sortData.bucketLabels);
441
442                 if (!update || !updateDiscrepancies(sortedCursor, idType)) {
443                     break;
444                 }
445             }
446
447             return sortedCursor;
448         }
449
450         return cursor;
451     }
452
453     /**
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
458      */
459     private boolean updateDiscrepancies(SortedCursor sortedCursor, SortParameter type) {
460         boolean hasNewIds = false;
461
462         final ArrayList<Long> missingIds = sortedCursor.getMissingIds();
463         if (missingIds.size() > 0) {
464             removeIds(missingIds, type);
465         }
466
467         final Collection<Long> extraIds = sortedCursor.getExtraIds();
468         if (extraIds != null && extraIds.size() > 0) {
469             addIds(extraIds, type);
470             hasNewIds = true;
471         }
472
473         return hasNewIds;
474     }
475
476     private void removeIds(ArrayList<Long> ids, SortParameter idType) {
477         if (ids == null || ids.size() == 0) {
478             return;
479         }
480
481         final String inParams = "(" + MusicUtils.buildCollectionAsString(ids) + ")";
482
483         if (DEBUG) {
484             Log.d(TAG, "Deleting from " + idType + " where id is in " + inParams);
485         }
486
487         switch (idType) {
488             case Song:
489                 mMusicDatabase.getWritableDatabase().delete(SongSortColumns.TABLE_NAME,
490                         SongSortColumns.ID + " IN " + inParams, null);
491                 break;
492             case Album:
493                 mMusicDatabase.getWritableDatabase().delete(AlbumSortColumns.TABLE_NAME,
494                         AlbumSortColumns.ID + " IN " + inParams, null);
495                 break;
496             case Artist:
497                 mMusicDatabase.getWritableDatabase().delete(ArtistSortColumns.TABLE_NAME,
498                         ArtistSortColumns.ID + " IN " + inParams, null);
499                 break;
500         }
501     }
502
503     private void addIds(Collection<Long> ids, SortParameter idType) {
504         StringBuilder builder = new StringBuilder();
505         switch (idType) {
506             case Song:
507                 builder.append(AudioColumns._ID);
508                 break;
509             case Album:
510                 builder.append(AudioColumns.ALBUM_ID);
511                 break;
512             case Artist:
513                 builder.append(AudioColumns.ARTIST_ID);
514                 break;
515         }
516
517         builder.append(" IN (");
518         builder.append(MusicUtils.buildCollectionAsString(ids));
519         builder.append(")");
520
521         updateLocalizedStore(mMusicDatabase.getWritableDatabase(), builder.toString());
522     }
523
524     private static String createJoin(String tableName, String firstParam, String secondParam) {
525         return " JOIN " + tableName + " ON (" + firstParam + "=" + secondParam + ")";
526     }
527
528     private static String createOrderBy(String first, String second, boolean descending) {
529         String desc = descending ? " DESC" : "";
530         return first + desc + "," + second + desc;
531     }
532
533     private static final class SongSortColumns {
534         /* Table name */
535         public static final String TABLE_NAME = "song_sort";
536
537         /* Song IDs column */
538         public static final String ID = "id";
539
540         /* Artist IDs column */
541         public static final String ARTIST_ID = "artist_id";
542
543         /* Album IDs column */
544         public static final String ALBUM_ID = "album_id";
545
546         /* The Song name */
547         public static final String NAME = "song_name";
548
549         /* The label assigned (categorization buckets - typically the first letter) */
550         public static final String NAME_LABEL = "song_name_label";
551
552         /* The numerical index of the bucket */
553         public static final String NAME_BUCKET = "song_name_bucket";
554
555         /* Used for joins */
556         public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
557
558         public static String getOrderBy(boolean descending) {
559             return createOrderBy(NAME_BUCKET, NAME, descending);
560         }
561     }
562
563     private static final class AlbumSortColumns {
564
565         /* Table name */
566         public static final String TABLE_NAME = "album_sort";
567
568         /* Album IDs column */
569         public static final String ID = "id";
570
571         /* Artist IDs column */
572         public static final String ARTIST_ID = "artist_id";
573
574         /* The Album name */
575         public static final String NAME = "album_name";
576
577         /* The label assigned (categorization buckets - typically the first letter) */
578         public static final String NAME_LABEL = "album_name_label";
579
580         /* The numerical index of the bucket */
581         public static final String NAME_BUCKET = "album_name_bucket";
582
583         /* Used for joins */
584         public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
585
586         public static String getOrderBy(boolean descending) {
587             return createOrderBy(NAME_BUCKET, NAME, descending);
588         }
589     }
590
591
592     private static final class ArtistSortColumns {
593
594         /* Table name */
595         public static final String TABLE_NAME = "artist_sort";
596
597         /* Artist IDs column */
598         public static final String ID = "id";
599
600         /* The Artist name */
601         public static final String NAME = "artist_name";
602
603         /* The label assigned (categorization buckets - typically the first letter) */
604         public static final String NAME_LABEL = "artist_name_label";
605
606         /* The numerical index of the bucket */
607         public static final String NAME_BUCKET = "artist_name_bucket";
608
609         /* Used for joins */
610         public static final String CONCRETE_ID = TABLE_NAME + "." + ID;
611
612         public static String getOrderBy(boolean descending) {
613             return createOrderBy(NAME_BUCKET, NAME, descending);
614         }
615     }
616
617 }