com.cyanogenmod.eleven.provider.LocalizedStore.java Source code

Java tutorial

Introduction

Here is the source code for com.cyanogenmod.eleven.provider.LocalizedStore.java

Source

/*
 * 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.support.v4.text.ICUCompat;
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 COLLATE LOCALIZED," + 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 oldVersion <= 2
        // in version 4 we need to recreate the SongSortcolumns table so drop the table and call
        // onCreate if oldVersion <= 3
        if (oldVersion <= 3) {
            db.execSQL("DROP TABLE IF EXISTS " + SongSortColumns.TABLE_NAME);
            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);
        }
    }

}