OSDN Git Service

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