/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.db;

import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.ProfileMigrator;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.Control;
import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.ThreadUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BrowserProvider extends ContentProvider {
    private static final String LOGTAG = "GeckoBrowserProvider";
    private Context mContext;

    static final String DATABASE_NAME = "browser.db";

    static final int DATABASE_VERSION = 17;

    // Maximum age of deleted records to be cleaned up (20 days in ms)
    static final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;

    // Number of records marked as deleted to be removed
    static final long DELETED_RECORDS_PURGE_LIMIT = 5;

    // How many records to reposition in a single query.
    // This should be less than the SQLite maximum number of query variables
    // (currently 999) divided by the number of variables used per positioning
    // query (currently 3).
    static final int MAX_POSITION_UPDATES_PER_QUERY = 100;

    // Minimum number of records to keep when expiring history.
    static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000;
    static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500;

    // Minimum duration to keep when expiring.
    static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L;     // Four weeks.
    // Minimum number of thumbnails to keep around.
    static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15;

    static final String TABLE_BOOKMARKS = "bookmarks";
    static final String TABLE_HISTORY = "history";
    static final String TABLE_FAVICONS = "favicons";
    static final String TABLE_THUMBNAILS = "thumbnails";

    static final String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp";
    static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp";
    static final String TABLE_IMAGES_TMP = Obsolete.TABLE_IMAGES + "_tmp";

    static final String VIEW_COMBINED = "combined";

    static final String VIEW_BOOKMARKS_WITH_FAVICONS = "bookmarks_with_favicons";
    static final String VIEW_HISTORY_WITH_FAVICONS = "history_with_favicons";
    static final String VIEW_COMBINED_WITH_FAVICONS = "combined_with_favicons";

    // Bookmark matches
    static final int BOOKMARKS = 100;
    static final int BOOKMARKS_ID = 101;
    static final int BOOKMARKS_FOLDER_ID = 102;
    static final int BOOKMARKS_PARENT = 103;
    static final int BOOKMARKS_POSITIONS = 104;

    // History matches
    static final int HISTORY = 200;
    static final int HISTORY_ID = 201;
    static final int HISTORY_OLD = 202;

    // Favicon matches
    static final int FAVICONS = 300;
    static final int FAVICON_ID = 301;

    // Schema matches
    static final int SCHEMA = 400;

    // Combined bookmarks and history matches
    static final int COMBINED = 500;

    // Control matches
    static final int CONTROL = 600;

    // Search Suggest matches
    static final int SEARCH_SUGGEST = 700;

    // Thumbnail matches
    static final int THUMBNAILS = 800;
    static final int THUMBNAIL_ID = 801;

    static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
            + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
            + " ASC";

    static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";

    static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
            TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " +
            qualifyColumn(TABLE_FAVICONS, Favicons._ID);

    static final String TABLE_HISTORY_JOIN_FAVICONS = TABLE_HISTORY + " LEFT OUTER JOIN " +
            TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " = " +
            qualifyColumn(TABLE_FAVICONS, Favicons._ID);

    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

    static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
    static final Map<String, String> HISTORY_PROJECTION_MAP;
    static final Map<String, String> COMBINED_PROJECTION_MAP;
    static final Map<String, String> SCHEMA_PROJECTION_MAP;
    static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
    static final Map<String, String> FAVICONS_PROJECTION_MAP;
    static final Map<String, String> THUMBNAILS_PROJECTION_MAP;

    static final class Obsolete {
        public static final String TABLE_IMAGES = "images";
        public static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
        public static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
        public static final String VIEW_COMBINED_WITH_IMAGES = "combined_with_images";

        public static final class Images implements CommonColumns, SyncColumns {
            private Images() {}

            public static final String URL = "url_key";
            public static final String FAVICON_URL = "favicon_url";
            public static final String FAVICON = "favicon";
            public static final String THUMBNAIL = "thumbnail";
            public static final String _ID = "_id";
            public static final String GUID = "guid";
            public static final String DATE_CREATED = "created";
            public static final String DATE_MODIFIED = "modified";
            public static final String IS_DELETED = "deleted";
        }

        public static final class Combined {
            private Combined() {}

            public static final String THUMBNAIL = "thumbnail";
        }

        static final String TABLE_BOOKMARKS_JOIN_IMAGES = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
                Obsolete.TABLE_IMAGES + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " +
                qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);

        static final String TABLE_HISTORY_JOIN_IMAGES = TABLE_HISTORY + " LEFT OUTER JOIN " +
                Obsolete.TABLE_IMAGES + " ON " + qualifyColumn(TABLE_HISTORY, History.URL) + " = " +
                qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);

        static final String FAVICON_DB = "favicon_urls.db";
    }

    static {
        // We will reuse this.
        HashMap<String, String> map;

        // Bookmarks
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);

        map = new HashMap<String, String>();
        map.put(Bookmarks._ID, Bookmarks._ID);
        map.put(Bookmarks.TITLE, Bookmarks.TITLE);
        map.put(Bookmarks.URL, Bookmarks.URL);
        map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
        map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID);
        map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL);
        map.put(Bookmarks.TYPE, Bookmarks.TYPE);
        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
        map.put(Bookmarks.TAGS, Bookmarks.TAGS);
        map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION);
        map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD);
        map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
        map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
        map.put(Bookmarks.GUID, Bookmarks.GUID);
        map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
        BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map);

        // History
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD);

        map = new HashMap<String, String>();
        map.put(History._ID, History._ID);
        map.put(History.TITLE, History.TITLE);
        map.put(History.URL, History.URL);
        map.put(History.FAVICON, History.FAVICON);
        map.put(History.FAVICON_ID, History.FAVICON_ID);
        map.put(History.FAVICON_URL, History.FAVICON_URL);
        map.put(History.VISITS, History.VISITS);
        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
        map.put(History.DATE_CREATED, History.DATE_CREATED);
        map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
        map.put(History.GUID, History.GUID);
        map.put(History.IS_DELETED, History.IS_DELETED);
        HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map);

        // Favicons
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID);

        map = new HashMap<String, String>();
        map.put(Favicons._ID, Favicons._ID);
        map.put(Favicons.URL, Favicons.URL);
        map.put(Favicons.DATA, Favicons.DATA);
        map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED);
        map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED);
        FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map);

        // Thumbnails
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS);
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID);

        map = new HashMap<String, String>();
        map.put(Thumbnails._ID, Thumbnails._ID);
        map.put(Thumbnails.URL, Thumbnails.URL);
        map.put(Thumbnails.DATA, Thumbnails.DATA);
        THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map);

        // Combined bookmarks and history
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED);

        map = new HashMap<String, String>();
        map.put(Combined._ID, Combined._ID);
        map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID);
        map.put(Combined.HISTORY_ID, Combined.HISTORY_ID);
        map.put(Combined.DISPLAY, "MAX(" + Combined.DISPLAY + ") AS " + Combined.DISPLAY);
        map.put(Combined.URL, Combined.URL);
        map.put(Combined.TITLE, Combined.TITLE);
        map.put(Combined.VISITS, Combined.VISITS);
        map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
        map.put(Combined.FAVICON, Combined.FAVICON);
        map.put(Combined.FAVICON_ID, Combined.FAVICON_ID);
        map.put(Combined.FAVICON_URL, Combined.FAVICON_URL);
        COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map);

        // Schema
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);

        map = new HashMap<String, String>();
        map.put(Schema.VERSION, Schema.VERSION);
        SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);


        // Control
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL);

        // Search Suggest
        URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);

        map = new HashMap<String, String>();
        map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
                Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
        map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
                Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
        map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
                Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA);
        SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map);
    }

    private HashMap<String, DatabaseHelper> mDatabasePerProfile;

    private interface BookmarkMigrator {
        public void updateForNewTable(ContentValues bookmark);
    }

    private class BookmarkMigrator3to4 implements BookmarkMigrator {
        @Override
        public void updateForNewTable(ContentValues bookmark) {
            Integer isFolder = bookmark.getAsInteger("folder");
            if (isFolder == null || isFolder != 1) {
                bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
            } else {
                bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
            }

            bookmark.remove("folder");
        }
    }

    static final String qualifyColumn(String table, String column) {
        return table + "." + column;
    }

    private static boolean hasFaviconsInProjection(String[] projection) {
        if (projection == null) return true;
        for (int i = 0; i < projection.length; ++i) {
            if (projection[i].equals(FaviconColumns.FAVICON) ||
                projection[i].equals(FaviconColumns.FAVICON_URL))
                return true;
        }

        return false;
    }

    // Calculate these once, at initialization. isLoggable is too expensive to
    // have in-line in each log call.
    private static boolean logDebug   = Log.isLoggable(LOGTAG, Log.DEBUG);
    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
    protected static void trace(String message) {
        if (logVerbose) {
            Log.v(LOGTAG, message);
        }
    }

    protected static void debug(String message) {
        if (logDebug) {
            Log.d(LOGTAG, message);
        }
    }

    final class DatabaseHelper extends SQLiteOpenHelper {
        public DatabaseHelper(Context context, String databasePath) {
            super(context, databasePath, null, DATABASE_VERSION);
        }

        private void createBookmarksTable(SQLiteDatabase db) {
            debug("Creating " + TABLE_BOOKMARKS + " table");

            // Android versions older than Froyo ship with an sqlite
            // that doesn't support foreign keys.
            String foreignKeyOnParent = null;
            if (Build.VERSION.SDK_INT >= 8) {
                foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
                    ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
            }

            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    Bookmarks.TITLE + " TEXT," +
                    Bookmarks.URL + " TEXT," +
                    Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
                    Bookmarks.PARENT + " INTEGER," +
                    Bookmarks.POSITION + " INTEGER NOT NULL," +
                    Bookmarks.KEYWORD + " TEXT," +
                    Bookmarks.DESCRIPTION + " TEXT," +
                    Bookmarks.TAGS + " TEXT," +
                    Bookmarks.DATE_CREATED + " INTEGER," +
                    Bookmarks.DATE_MODIFIED + " INTEGER," +
                    Bookmarks.GUID + " TEXT NOT NULL," +
                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
                    (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
                    ");");

            db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.URL + ")");
            db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
            db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.GUID + ")");
            db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.DATE_MODIFIED + ")");
        }

        private void createBookmarksTableOn13(SQLiteDatabase db) {
            debug("Creating " + TABLE_BOOKMARKS + " table");

            // Android versions older than Froyo ship with an sqlite
            // that doesn't support foreign keys.
            String foreignKeyOnParent = null;
            if (Build.VERSION.SDK_INT >= 8) {
                foreignKeyOnParent = ", FOREIGN KEY (" + Bookmarks.PARENT +
                    ") REFERENCES " + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")";
            }

            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    Bookmarks.TITLE + " TEXT," +
                    Bookmarks.URL + " TEXT," +
                    Bookmarks.TYPE + " INTEGER NOT NULL DEFAULT " + Bookmarks.TYPE_BOOKMARK + "," +
                    Bookmarks.PARENT + " INTEGER," +
                    Bookmarks.POSITION + " INTEGER NOT NULL," +
                    Bookmarks.KEYWORD + " TEXT," +
                    Bookmarks.DESCRIPTION + " TEXT," +
                    Bookmarks.TAGS + " TEXT," +
                    Bookmarks.FAVICON_ID + " INTEGER," +
                    Bookmarks.DATE_CREATED + " INTEGER," +
                    Bookmarks.DATE_MODIFIED + " INTEGER," +
                    Bookmarks.GUID + " TEXT NOT NULL," +
                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
                    (foreignKeyOnParent != null ? foreignKeyOnParent : "") +
                    ");");

            db.execSQL("CREATE INDEX bookmarks_url_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.URL + ")");
            db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");
            db.execSQL("CREATE UNIQUE INDEX bookmarks_guid_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.GUID + ")");
            db.execSQL("CREATE INDEX bookmarks_modified_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.DATE_MODIFIED + ")");
        }

        private void createHistoryTable(SQLiteDatabase db) {
            debug("Creating " + TABLE_HISTORY + " table");
            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    History.TITLE + " TEXT," +
                    History.URL + " TEXT NOT NULL," +
                    History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
                    History.DATE_LAST_VISITED + " INTEGER," +
                    History.DATE_CREATED + " INTEGER," +
                    History.DATE_MODIFIED + " INTEGER," +
                    History.GUID + " TEXT NOT NULL," +
                    History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
                    ");");

            db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
                    + History.URL + ")");
            db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
                    + History.GUID + ")");
            db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
                    + History.DATE_MODIFIED + ")");
            db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
                    + History.DATE_LAST_VISITED + ")");
        }

        private void createHistoryTableOn13(SQLiteDatabase db) {
            debug("Creating " + TABLE_HISTORY + " table");
            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    History.TITLE + " TEXT," +
                    History.URL + " TEXT NOT NULL," +
                    History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
                    History.FAVICON_ID + " INTEGER," +
                    History.DATE_LAST_VISITED + " INTEGER," +
                    History.DATE_CREATED + " INTEGER," +
                    History.DATE_MODIFIED + " INTEGER," +
                    History.GUID + " TEXT NOT NULL," +
                    History.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
                    ");");

            db.execSQL("CREATE INDEX history_url_index ON " + TABLE_HISTORY + "("
                    + History.URL + ")");
            db.execSQL("CREATE UNIQUE INDEX history_guid_index ON " + TABLE_HISTORY + "("
                    + History.GUID + ")");
            db.execSQL("CREATE INDEX history_modified_index ON " + TABLE_HISTORY + "("
                    + History.DATE_MODIFIED + ")");
            db.execSQL("CREATE INDEX history_visited_index ON " + TABLE_HISTORY + "("
                    + History.DATE_LAST_VISITED + ")");
        }

        private void createImagesTable(SQLiteDatabase db) {
            debug("Creating " + Obsolete.TABLE_IMAGES + " table");
            db.execSQL("CREATE TABLE " + Obsolete.TABLE_IMAGES + " (" +
                    Obsolete.Images._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    Obsolete.Images.URL + " TEXT UNIQUE NOT NULL," +
                    Obsolete.Images.FAVICON + " BLOB," +
                    Obsolete.Images.FAVICON_URL + " TEXT," +
                    Obsolete.Images.THUMBNAIL + " BLOB," +
                    Obsolete.Images.DATE_CREATED + " INTEGER," +
                    Obsolete.Images.DATE_MODIFIED + " INTEGER," +
                    Obsolete.Images.GUID + " TEXT NOT NULL," +
                    Obsolete.Images.IS_DELETED + " INTEGER NOT NULL DEFAULT 0" +
                    ");");

            db.execSQL("CREATE INDEX images_url_index ON " + Obsolete.TABLE_IMAGES + "("
                    + Obsolete.Images.URL + ")");
            db.execSQL("CREATE UNIQUE INDEX images_guid_index ON " + Obsolete.TABLE_IMAGES + "("
                    + Obsolete.Images.GUID + ")");
            db.execSQL("CREATE INDEX images_modified_index ON " + Obsolete.TABLE_IMAGES + "("
                    + Obsolete.Images.DATE_MODIFIED + ")");
        }

        private void createFaviconsTable(SQLiteDatabase db) {
            debug("Creating " + TABLE_FAVICONS + " table");
            db.execSQL("CREATE TABLE " + TABLE_FAVICONS + " (" +
                    Favicons._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    Favicons.URL + " TEXT UNIQUE," +
                    Favicons.DATA + " BLOB," +
                    Favicons.DATE_CREATED + " INTEGER," +
                    Favicons.DATE_MODIFIED + " INTEGER" +
                    ");");

            db.execSQL("CREATE INDEX favicons_url_index ON " + TABLE_FAVICONS + "("
                    + Favicons.URL + ")");
            db.execSQL("CREATE INDEX favicons_modified_index ON " + TABLE_FAVICONS + "("
                    + Favicons.DATE_MODIFIED + ")");
        }

        private void createThumbnailsTable(SQLiteDatabase db) {
            debug("Creating " + TABLE_THUMBNAILS + " table");
            db.execSQL("CREATE TABLE " + TABLE_THUMBNAILS + " (" +
                    Thumbnails._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    Thumbnails.URL + " TEXT UNIQUE," +
                    Thumbnails.DATA + " BLOB" +
                    ");");

            db.execSQL("CREATE INDEX thumbnails_url_index ON " + TABLE_THUMBNAILS + "("
                    + Thumbnails.URL + ")");
        }

        private void createBookmarksWithImagesView(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES + " AS " +
                    "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
                    ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
                    Obsolete.TABLE_BOOKMARKS_JOIN_IMAGES);
        }

        private void createBookmarksWithFaviconsView(SQLiteDatabase db) {
            debug("Creating " + VIEW_BOOKMARKS_WITH_FAVICONS + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_FAVICONS + " AS " +
                    "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") +
                    ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Bookmarks.FAVICON +
                    ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Bookmarks.FAVICON_URL +
                    " FROM " + TABLE_BOOKMARKS_JOIN_FAVICONS);
        }

        private void createHistoryWithImagesView(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES + " AS " +
                    "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
                    ", " + Obsolete.Images.FAVICON + ", " + Obsolete.Images.THUMBNAIL + " FROM " +
                    Obsolete.TABLE_HISTORY_JOIN_IMAGES);
        }

        private void createHistoryWithFaviconsView(SQLiteDatabase db) {
            debug("Creating " + VIEW_HISTORY_WITH_FAVICONS + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " +
                    "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
                    ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON +
                    ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL +
                    " FROM " + TABLE_HISTORY_JOIN_FAVICONS);
        }

        private void createCombinedWithImagesView(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritze bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
                    ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
                        " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
        }

        private void createCombinedWithImagesViewOn9(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritze bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
                    ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
                        " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
        }

        private void createCombinedWithImagesViewOn10(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +  " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritze bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ")" +
                    ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
                        " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
        }

        private void createCombinedWithImagesViewOn11(SQLiteDatabase db) {
            debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
                                 qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +  " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritze bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     // Only use DISPLAY_READER if the matching bookmark entry inside reading
                                     // list folder is not marked as deleted.
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
                                        " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
                    ") LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
                        " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
        }

        private void createCombinedViewOn12(SQLiteDatabase db) {
            debug("Creating " + VIEW_COMBINED + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +  " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritze bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     // Only use DISPLAY_READER if the matching bookmark entry inside reading
                                     // list folder is not marked as deleted.
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
                                        " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
                    ")");

            debug("Creating " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES + " AS" +
                    " SELECT *, " +
                        qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.FAVICON) + " AS " + Combined.FAVICON + ", " +
                        qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.THUMBNAIL) + " AS " + Obsolete.Combined.THUMBNAIL +
                    " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + Obsolete.TABLE_IMAGES +
                        " ON " + Combined.URL + " = " + qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL));
        }

        private void createCombinedViewOn13(SQLiteDatabase db) {
            debug("Creating " + VIEW_COMBINED + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 Combined.FAVICON_ID +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) +  " ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritize bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     // Only use DISPLAY_READER if the matching bookmark entry inside reading
                                     // list folder is not marked as deleted.
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
                                        " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
                    ")");

            debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
                    " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
                        qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
                        qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
                    " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
                        " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
        }

        private void createCombinedViewOn16(SQLiteDatabase db) {
            debug("Creating " + VIEW_COMBINED + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" +
                    " SELECT " + Combined.BOOKMARK_ID + ", " +
                                 Combined.HISTORY_ID + ", " +
                                 // We need to return an _id column because CursorAdapter requires it for its
                                 // default implementation for the getItemId() method. However, since
                                 // we're not using this feature in the parts of the UI using this view,
                                 // we can just use 0 for all rows.
                                 "0 AS " + Combined._ID + ", " +
                                 Combined.URL + ", " +
                                 Combined.TITLE + ", " +
                                 Combined.VISITS + ", " +
                                 Combined.DISPLAY + ", " +
                                 Combined.DATE_LAST_VISITED + ", " +
                                 Combined.FAVICON_ID +
                    " FROM (" +
                        // Bookmarks without history.
                        " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " +
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_READING_LIST_ID + " THEN " + Combined.DISPLAY_READER + " ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     "-1 AS " + Combined.HISTORY_ID + ", " +
                                     "-1 AS " + Combined.VISITS + ", " +
                                     "-1 AS " + Combined.DATE_LAST_VISITED + ", " +
                                     qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + " AND " +
                                    // Ignore pinned bookmarks.
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT)  + " <> " + Bookmarks.FIXED_PINNED_LIST_ID + " AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED)  + " = 0 AND " +
                                    qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) +
                                        " NOT IN (SELECT " + History.URL + " FROM " + TABLE_HISTORY + ")" +
                        " UNION ALL" +
                        // History with and without bookmark.
                        " SELECT " + "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN " +
                                        // Give pinned bookmarks a NULL ID so that they're not treated as bookmarks. We can't
                                        // completely ignore them here because they're joined with history entries we care about.
                                        "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " +
                                        Bookmarks.FIXED_PINNED_LIST_ID + " THEN NULL ELSE " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " END " +
                                     "ELSE NULL END AS " + Combined.BOOKMARK_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.URL) + " AS " + Combined.URL + ", " +
                                     // Prioritize bookmark titles over history titles, since the user may have
                                     // customized the title for a bookmark.
                                     "COALESCE(" + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + ", " +
                                                   qualifyColumn(TABLE_HISTORY, History.TITLE) +")" + " AS " + Combined.TITLE + ", " +
                                     // Only use DISPLAY_READER if the matching bookmark entry inside reading
                                     // list folder is not marked as deleted.
                                     "CASE " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " WHEN 0 THEN CASE " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " WHEN " + Bookmarks.FIXED_READING_LIST_ID +
                                        " THEN " + Combined.DISPLAY_READER + " ELSE " + Combined.DISPLAY_NORMAL + " END ELSE " +
                                        Combined.DISPLAY_NORMAL + " END AS " + Combined.DISPLAY + ", " +
                                     qualifyColumn(TABLE_HISTORY, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.VISITS) + " AS " + Combined.VISITS + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED) + " AS " + Combined.DATE_LAST_VISITED + ", " +
                                     qualifyColumn(TABLE_HISTORY, History.FAVICON_ID) + " AS " + Combined.FAVICON_ID +
                        " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS +
                            " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) +
                        " WHERE " + qualifyColumn(TABLE_HISTORY, History.URL) + " IS NOT NULL AND " +
                                    qualifyColumn(TABLE_HISTORY, History.IS_DELETED)  + " = 0 AND (" +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " +
                                        qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE)  + " = " + Bookmarks.TYPE_BOOKMARK + ") " +
                    ")");

            debug("Creating " + VIEW_COMBINED_WITH_FAVICONS + " view");

            db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED_WITH_FAVICONS + " AS" +
                    " SELECT " + qualifyColumn(VIEW_COMBINED, "*") + ", " +
                        qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + Combined.FAVICON_URL + ", " +
                        qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + Combined.FAVICON +
                    " FROM " + VIEW_COMBINED + " LEFT OUTER JOIN " + TABLE_FAVICONS +
                        " ON " + Combined.FAVICON_ID + " = " + qualifyColumn(TABLE_FAVICONS, Favicons._ID));
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            debug("Creating browser.db: " + db.getPath());

            createBookmarksTableOn13(db);
            createHistoryTableOn13(db);
            createFaviconsTable(db);
            createThumbnailsTable(db);

            createBookmarksWithFaviconsView(db);
            createHistoryWithFaviconsView(db);
            createCombinedViewOn16(db);

            createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
                R.string.bookmarks_folder_places, 0);

            createOrUpdateAllSpecialFolders(db);

            // Create distribution bookmarks before our own default bookmarks
            int pos = createDistributionBookmarks(db);
            createDefaultBookmarks(db, pos);
        }

        private String getLocalizedProperty(JSONObject bookmark, String property, Locale locale) throws JSONException {
            // Try the full locale
            String fullLocale = property + "." + locale.toString();
            if (bookmark.has(fullLocale)) {
                return bookmark.getString(fullLocale);
            }
            // Try without a variant
            if (!TextUtils.isEmpty(locale.getVariant())) {
                String noVariant = fullLocale.substring(0, fullLocale.lastIndexOf("_"));
                if (bookmark.has(noVariant)) {
                    return bookmark.getString(noVariant);
                }
            }
            // Try just the language
            String lang = property + "." + locale.getLanguage();
            if (bookmark.has(lang)) {
                return bookmark.getString(lang);
            }
            // Default to the non-localized property name
            return bookmark.getString(property);
        }

        // Returns the number of bookmarks inserted in the db
        private int createDistributionBookmarks(SQLiteDatabase db) {
            JSONArray bookmarks = Distribution.getBookmarks(mContext);
            if (bookmarks == null) {
                return 0;
            }

            Locale locale = Locale.getDefault();
            int pos = 0;
            Integer mobileFolderId = getMobileFolderId(db);
            if (mobileFolderId == null) {
                Log.e(LOGTAG, "Error creating distribution bookmarks: mobileFolderId is null");
                return 0;
            }

            for (int i = 0; i < bookmarks.length(); i++) {
                try {
                    final JSONObject bookmark = bookmarks.getJSONObject(i);

                    String title = getLocalizedProperty(bookmark, "title", locale);
                    final String url = getLocalizedProperty(bookmark, "url", locale);
                    createBookmark(db, title, url, pos, mobileFolderId);

                    if (bookmark.has("pinned")) {
                        try {
                            // Create a fake bookmark in the hidden pinned folder to pin bookmark
                            // to about:home top sites. Pass pos as the pinned position to pin
                            // sites in the order that bookmarks are specified in bookmarks.json.
                            if (bookmark.getBoolean("pinned")) {
                                createBookmark(db, title, url, pos, Bookmarks.FIXED_PINNED_LIST_ID);
                            }
                        } catch (JSONException e) {
                            Log.e(LOGTAG, "Error pinning bookmark to top sites", e);
                        }
                    }

                    pos++;

                    // return early if there is no icon for this bookmark
                    if (!bookmark.has("icon")) {
                        continue;
                    }

                    // create icons in a separate thread to avoid blocking about:home on startup
                    ThreadUtils.postToBackgroundThread(new Runnable() {
                        @Override
                        public void run() {
                            SQLiteDatabase db = getWritableDatabase();
                            try {
                                String iconData = bookmark.getString("icon");
                                Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconData);
                                if (icon != null) {
                                    createFavicon(db, url, icon);
                                }
                            } catch (JSONException e) {
                                Log.e(LOGTAG, "Error creating distribution bookmark icon", e);
                            }
                        }
                    });
                } catch (JSONException e) {
                    Log.e(LOGTAG, "Error creating distribution bookmark", e);
                }
            }
            return pos;
        }

        // Inserts default bookmarks, starting at a specified position
        private void createDefaultBookmarks(SQLiteDatabase db, int pos) {
            Class<?> stringsClass = R.string.class;
            Field[] fields = stringsClass.getFields();
            Pattern p = Pattern.compile("^bookmarkdefaults_title_");

            Integer mobileFolderId = getMobileFolderId(db);
            if (mobileFolderId == null) {
                Log.e(LOGTAG, "Error creating default bookmarks: mobileFolderId is null");
                return;
            }

            for (int i = 0; i < fields.length; i++) {
                final String name = fields[i].getName();
                Matcher m = p.matcher(name);
                if (!m.find()) {
                    continue;
                }
                try {
                    int titleid = fields[i].getInt(null);
                    String title = mContext.getString(titleid);

                    Field urlField = stringsClass.getField(name.replace("_title_", "_url_"));
                    int urlId = urlField.getInt(null);
                    final String url = mContext.getString(urlId);
                    createBookmark(db, title, url, pos, mobileFolderId);

                    // create icons in a separate thread to avoid blocking about:home on startup
                    ThreadUtils.postToBackgroundThread(new Runnable() {
                        @Override
                        public void run() {
                            SQLiteDatabase db = getWritableDatabase();
                            Bitmap icon = getDefaultFaviconFromPath(name);
                            if (icon == null) {
                                icon = getDefaultFaviconFromDrawable(name);
                            }
                            if (icon != null) {
                                createFavicon(db, url, icon);
                            }
                        }
                    });
                    pos++;
                } catch (java.lang.IllegalAccessException ex) {
                    Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                } catch (java.lang.NoSuchFieldException ex) {
                    Log.e(LOGTAG, "Can't create bookmark " + name, ex);
                }
            }
        }

        private void createBookmark(SQLiteDatabase db, String title, String url, int pos, int parent) {
            ContentValues bookmarkValues = new ContentValues();
            bookmarkValues.put(Bookmarks.PARENT, parent);

            long now = System.currentTimeMillis();
            bookmarkValues.put(Bookmarks.DATE_CREATED, now);
            bookmarkValues.put(Bookmarks.DATE_MODIFIED, now);

            bookmarkValues.put(Bookmarks.TITLE, title);
            bookmarkValues.put(Bookmarks.URL, url);
            bookmarkValues.put(Bookmarks.GUID, Utils.generateGuid());
            bookmarkValues.put(Bookmarks.POSITION, pos);
            db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, bookmarkValues);
        }

        private void createFavicon(SQLiteDatabase db, String url, Bitmap icon) {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();

            ContentValues iconValues = new ContentValues();
            iconValues.put(Favicons.PAGE_URL, url);

            byte[] data = null;
            if (icon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
                data = stream.toByteArray();
            } else {
                Log.w(LOGTAG, "Favicon compression failed.");
            }
            iconValues.put(Favicons.DATA, data);

            insertFavicon(db, iconValues);
        }

        private Bitmap getDefaultFaviconFromPath(String name) {
            Class<?> stringClass = R.string.class;
            try {
                // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
                Field faviconField = stringClass.getField(name.replace("_title_", "_favicon_"));
                if (faviconField == null) {
                    return null;
                }
                int faviconId = faviconField.getInt(null);
                String path = mContext.getString(faviconId);

                String apkPath = mContext.getPackageResourcePath();
                File apkFile = new File(apkPath);
                String bitmapPath = "jar:jar:" + apkFile.toURI() + "!/" + AppConstants.OMNIJAR_NAME + "!/" + path;
                return GeckoJarReader.getBitmap(mContext.getResources(), bitmapPath);
            } catch (java.lang.IllegalAccessException ex) {
                Log.e(LOGTAG, "[Path] Can't create favicon " + name, ex);
            } catch (java.lang.NoSuchFieldException ex) {
                // If the field does not exist, that means we intend to load via a drawable
            }
            return null;
        }

        private Bitmap getDefaultFaviconFromDrawable(String name) {
            Class<?> drawablesClass = R.drawable.class;
            try {
                // Look for a drawable with the id R.drawable.bookmarkdefaults_favicon_*
                Field faviconField = drawablesClass.getField(name.replace("_title_", "_favicon_"));
                if (faviconField == null) {
                    return null;
                }
                int faviconId = faviconField.getInt(null);
                return BitmapUtils.decodeResource(mContext, faviconId);
            } catch (java.lang.IllegalAccessException ex) {
                Log.e(LOGTAG, "[Drawable] Can't create favicon " + name, ex);
            } catch (java.lang.NoSuchFieldException ex) {
                // If the field does not exist, that means we intend to load via a file path
            }
            return null;
        }

        private void createOrUpdateAllSpecialFolders(SQLiteDatabase db) {
            createOrUpdateSpecialFolder(db, Bookmarks.MOBILE_FOLDER_GUID,
                R.string.bookmarks_folder_mobile, 0);
            createOrUpdateSpecialFolder(db, Bookmarks.TOOLBAR_FOLDER_GUID,
                R.string.bookmarks_folder_toolbar, 1);
            createOrUpdateSpecialFolder(db, Bookmarks.MENU_FOLDER_GUID,
                R.string.bookmarks_folder_menu, 2);
            createOrUpdateSpecialFolder(db, Bookmarks.TAGS_FOLDER_GUID,
                R.string.bookmarks_folder_tags, 3);
            createOrUpdateSpecialFolder(db, Bookmarks.UNFILED_FOLDER_GUID,
                R.string.bookmarks_folder_unfiled, 4);
            createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
                R.string.bookmarks_folder_reading_list, 5);
            createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
                R.string.bookmarks_folder_pinned, 6);
        }

        private void createOrUpdateSpecialFolder(SQLiteDatabase db,
                String guid, int titleId, int position) {
            ContentValues values = new ContentValues();
            values.put(Bookmarks.GUID, guid);
            values.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
            values.put(Bookmarks.POSITION, position);

            if (guid.equals(Bookmarks.PLACES_FOLDER_GUID))
                values.put(Bookmarks._ID, Bookmarks.FIXED_ROOT_ID);
            else if (guid.equals(Bookmarks.READING_LIST_FOLDER_GUID))
                values.put(Bookmarks._ID, Bookmarks.FIXED_READING_LIST_ID);
            else if (guid.equals(Bookmarks.PINNED_FOLDER_GUID))
                values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);

            // Set the parent to 0, which sync assumes is the root
            values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);

            String title = mContext.getResources().getString(titleId);
            values.put(Bookmarks.TITLE, title);

            long now = System.currentTimeMillis();
            values.put(Bookmarks.DATE_CREATED, now);
            values.put(Bookmarks.DATE_MODIFIED, now);

            int updated = db.update(TABLE_BOOKMARKS, values,
                                    Bookmarks.GUID + " = ?",
                                    new String[] { guid });

            if (updated == 0) {
                db.insert(TABLE_BOOKMARKS, Bookmarks.GUID, values);
                debug("Inserted special folder: " + guid);
            } else {
                debug("Updated special folder: " + guid);
            }
        }

        private boolean isSpecialFolder(ContentValues values) {
            String guid = values.getAsString(Bookmarks.GUID);
            if (guid == null)
                return false;

            return guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
                   guid.equals(Bookmarks.MENU_FOLDER_GUID) ||
                   guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID) ||
                   guid.equals(Bookmarks.UNFILED_FOLDER_GUID) ||
                   guid.equals(Bookmarks.TAGS_FOLDER_GUID);
        }

        private void migrateBookmarkFolder(SQLiteDatabase db, int folderId,
                BookmarkMigrator migrator) {
            Cursor c = null;

            debug("Migrating bookmark folder with id = " + folderId);

            String selection = Bookmarks.PARENT + " = " + folderId;
            String[] selectionArgs = null;

            boolean isRootFolder = (folderId == Bookmarks.FIXED_ROOT_ID);

            // If we're loading the root folder, we have to account for
            // any previously created special folder that was created without
            // setting a parent id (e.g. mobile folder) and making sure we're
            // not adding any infinite recursion as root's parent is root itself.
            if (isRootFolder) {
                selection = Bookmarks.GUID + " != ?" + " AND (" +
                            selection + " OR " + Bookmarks.PARENT + " = NULL)";
                selectionArgs = new String[] { Bookmarks.PLACES_FOLDER_GUID };
            }

            List<Integer> subFolders = new ArrayList<Integer>();
            List<ContentValues> invalidSpecialEntries = new ArrayList<ContentValues>();

            try {
                c = db.query(TABLE_BOOKMARKS_TMP,
                             null,
                             selection,
                             selectionArgs,
                             null, null, null);

                // The key point here is that bookmarks should be added in
                // parent order to avoid any problems with the foreign key
                // in Bookmarks.PARENT.
                while (c.moveToNext()) {
                    ContentValues values = new ContentValues();

                    // We're using a null projection in the query which
                    // means we're getting all columns from the table.
                    // It's safe to simply transform the row into the
                    // values to be inserted on the new table.
                    DatabaseUtils.cursorRowToContentValues(c, values);

                    boolean isSpecialFolder = isSpecialFolder(values);

                    // The mobile folder used to be created with PARENT = NULL.
                    // We want fix that here.
                    if (values.getAsLong(Bookmarks.PARENT) == null && isSpecialFolder)
                        values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);

                    if (isRootFolder && !isSpecialFolder) {
                        invalidSpecialEntries.add(values);
                        continue;
                    }

                    if (migrator != null)
                        migrator.updateForNewTable(values);

                    debug("Migrating bookmark: " + values.getAsString(Bookmarks.TITLE));
                    db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);

                    Integer type = values.getAsInteger(Bookmarks.TYPE);
                    if (type != null && type == Bookmarks.TYPE_FOLDER)
                        subFolders.add(values.getAsInteger(Bookmarks._ID));
                }
            } finally {
                if (c != null)
                    c.close();
            }

            // At this point is safe to assume that the mobile folder is
            // in the new table given that we've always created it on
            // database creation time.
            final int nInvalidSpecialEntries = invalidSpecialEntries.size();
            if (nInvalidSpecialEntries > 0) {
                Integer mobileFolderId = getMobileFolderId(db);
                if (mobileFolderId == null) {
                    Log.e(LOGTAG, "Error migrating invalid special folder entries: mobile folder id is null");
                    return;
                }

                debug("Found " + nInvalidSpecialEntries + " invalid special folder entries");
                for (int i = 0; i < nInvalidSpecialEntries; i++) {
                    ContentValues values = invalidSpecialEntries.get(i);
                    values.put(Bookmarks.PARENT, mobileFolderId);

                    db.insert(TABLE_BOOKMARKS, Bookmarks.URL, values);
                }
            }

            final int nSubFolders = subFolders.size();
            for (int i = 0; i < nSubFolders; i++) {
                int subFolderId = subFolders.get(i);
                migrateBookmarkFolder(db, subFolderId, migrator);
            }
        }

        private void migrateBookmarksTable(SQLiteDatabase db) {
            migrateBookmarksTable(db, null);
        }

        private void migrateBookmarksTable(SQLiteDatabase db, BookmarkMigrator migrator) {
            debug("Renaming bookmarks table to " + TABLE_BOOKMARKS_TMP);
            db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS +
                       " RENAME TO " + TABLE_BOOKMARKS_TMP);

            debug("Dropping views and indexes related to " + TABLE_BOOKMARKS);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);

            db.execSQL("DROP INDEX IF EXISTS bookmarks_url_index");
            db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index");
            db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index");

            createBookmarksTable(db);
            createBookmarksWithImagesView(db);

            createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
                R.string.bookmarks_folder_places, 0);

            migrateBookmarkFolder(db, Bookmarks.FIXED_ROOT_ID, migrator);

            // Ensure all special folders exist and have the
            // right folder hierarchy.
            createOrUpdateAllSpecialFolders(db);

            debug("Dropping bookmarks temporary table");
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS_TMP);
        }


        private void migrateHistoryTable(SQLiteDatabase db) {
            debug("Renaming history table to " + TABLE_HISTORY_TMP);
            db.execSQL("ALTER TABLE " + TABLE_HISTORY +
                       " RENAME TO " + TABLE_HISTORY_TMP);

            debug("Dropping views and indexes related to " + TABLE_HISTORY);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            db.execSQL("DROP INDEX IF EXISTS history_url_index");
            db.execSQL("DROP INDEX IF EXISTS history_guid_index");
            db.execSQL("DROP INDEX IF EXISTS history_modified_index");
            db.execSQL("DROP INDEX IF EXISTS history_visited_index");

            createHistoryTable(db);
            createHistoryWithImagesView(db);
            createCombinedWithImagesView(db);

            db.execSQL("INSERT INTO " + TABLE_HISTORY + " SELECT * FROM " + TABLE_HISTORY_TMP);

            debug("Dropping history temporary table");
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY_TMP);
        }

        private void migrateImagesTable(SQLiteDatabase db) {
            debug("Renaming images table to " + TABLE_IMAGES_TMP);
            db.execSQL("ALTER TABLE " + Obsolete.TABLE_IMAGES +
                       " RENAME TO " + TABLE_IMAGES_TMP);

            debug("Dropping views and indexes related to " + Obsolete.TABLE_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            db.execSQL("DROP INDEX IF EXISTS images_url_index");
            db.execSQL("DROP INDEX IF EXISTS images_guid_index");
            db.execSQL("DROP INDEX IF EXISTS images_modified_index");

            createImagesTable(db);
            createHistoryWithImagesView(db);
            createCombinedWithImagesView(db);

            db.execSQL("INSERT INTO " + Obsolete.TABLE_IMAGES + " SELECT * FROM " + TABLE_IMAGES_TMP);

            debug("Dropping images temporary table");
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES_TMP);
        }

        private void upgradeDatabaseFrom1to2(SQLiteDatabase db) {
            migrateBookmarksTable(db);
        }

        private void upgradeDatabaseFrom2to3(SQLiteDatabase db) {
            debug("Dropping view: " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);

            createBookmarksWithImagesView(db);

            debug("Dropping view: " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);

            createHistoryWithImagesView(db);
        }

        private void upgradeDatabaseFrom3to4(SQLiteDatabase db) {
            migrateBookmarksTable(db, new BookmarkMigrator3to4());
        }

        private void upgradeDatabaseFrom4to5(SQLiteDatabase db) {
            createCombinedWithImagesView(db);
        }

        private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
            debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            createCombinedWithImagesView(db);
        }

        private void upgradeDatabaseFrom6to7(SQLiteDatabase db) {
            debug("Removing history visits with NULL GUIDs");
            db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL");

            debug("Update images with NULL GUIDs");
            String[] columns = new String[] { Obsolete.Images._ID };
            Cursor cursor = null;
            try {
              cursor = db.query(Obsolete.TABLE_IMAGES, columns, Obsolete.Images.GUID + " IS NULL", null, null ,null, null, null);
              ContentValues values = new ContentValues();
              if (cursor.moveToFirst()) {
                  do {
                      values.put(Obsolete.Images.GUID, Utils.generateGuid());
                      db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images._ID + " = ?", new String[] {
                        cursor.getString(cursor.getColumnIndexOrThrow(Obsolete.Images._ID))
                      });
                  } while (cursor.moveToNext());
              }
            } finally {
              if (cursor != null)
                cursor.close();
            }

            migrateBookmarksTable(db);
            migrateHistoryTable(db);
            migrateImagesTable(db);
        }

        private void upgradeDatabaseFrom7to8(SQLiteDatabase db) {
            debug("Combining history entries with the same URL");

            final String TABLE_DUPES = "duped_urls";
            final String TOTAL = "total";
            final String LATEST = "latest";
            final String WINNER = "winner";

            db.execSQL("CREATE TEMP TABLE " + TABLE_DUPES + " AS" +
                      " SELECT " + History.URL + ", " +
                                  "SUM(" + History.VISITS + ") AS " + TOTAL + ", " +
                                  "MAX(" + History.DATE_MODIFIED + ") AS " + LATEST + ", " +
                                  "MAX(" + History._ID + ") AS " + WINNER +
                      " FROM " + TABLE_HISTORY +
                      " GROUP BY " + History.URL +
                      " HAVING count(" + History.URL + ") > 1");

            db.execSQL("CREATE UNIQUE INDEX " + TABLE_DUPES + "_url_index ON " +
                       TABLE_DUPES + " (" + History.URL + ")");

            final String fromClause = " FROM " + TABLE_DUPES + " WHERE " +
                                      qualifyColumn(TABLE_DUPES, History.URL) + " = " +
                                      qualifyColumn(TABLE_HISTORY, History.URL);

            db.execSQL("UPDATE " + TABLE_HISTORY +
                      " SET " + History.VISITS + " = (SELECT " + TOTAL + fromClause + "), " +
                                History.DATE_MODIFIED + " = (SELECT " + LATEST + fromClause + "), " +
                                History.IS_DELETED + " = " +
                                    "(" + History._ID + " <> (SELECT " + WINNER + fromClause + "))" +
                      " WHERE " + History.URL + " IN (SELECT " + History.URL + " FROM " + TABLE_DUPES + ")");

            db.execSQL("DROP TABLE " + TABLE_DUPES);
        }

        private void upgradeDatabaseFrom8to9(SQLiteDatabase db) {
            createOrUpdateSpecialFolder(db, Bookmarks.READING_LIST_FOLDER_GUID,
                R.string.bookmarks_folder_reading_list, 5);

            debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            createCombinedWithImagesViewOn9(db);
        }

        private void upgradeDatabaseFrom9to10(SQLiteDatabase db) {
            debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            createCombinedWithImagesViewOn10(db);
        }

        private void upgradeDatabaseFrom10to11(SQLiteDatabase db) {
            debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "("
                    + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")");

            createCombinedWithImagesViewOn11(db);
        }

        private void upgradeDatabaseFrom11to12(SQLiteDatabase db) {
            debug("Dropping view: " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);

            createCombinedViewOn12(db);
        }

        private void upgradeDatabaseFrom12to13(SQLiteDatabase db) {
            // Update images table with favicon URLs
            SQLiteDatabase faviconsDb = null;
            Cursor c = null;
            try {
                final String FAVICON_TABLE = "favicon_urls";
                final String FAVICON_URL = "favicon_url";
                final String FAVICON_PAGE = "page_url";

                String dbPath = mContext.getDatabasePath(Obsolete.FAVICON_DB).getPath();
                faviconsDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY);
                String[] columns = new String[] { FAVICON_URL, FAVICON_PAGE };
                c = faviconsDb.query(FAVICON_TABLE, columns, null, null, null, null, null, null);
                int faviconIndex = c.getColumnIndexOrThrow(FAVICON_URL);
                int pageIndex = c.getColumnIndexOrThrow(FAVICON_PAGE);
                while (c.moveToNext()) {
                    ContentValues values = new ContentValues(1);
                    String faviconUrl = c.getString(faviconIndex);
                    String pageUrl = c.getString(pageIndex);
                    values.put(FAVICON_URL, faviconUrl);
                    db.update(Obsolete.TABLE_IMAGES, values, Obsolete.Images.URL + " = ?", new String[] { pageUrl });
                }
            } catch (SQLException e) {
                // If we can't read from the database for some reason, we won't
                // be able to import the favicon URLs. This isn't a fatal
                // error, so continue the upgrade.
                Log.e(LOGTAG, "Exception importing from " + Obsolete.FAVICON_DB, e);
            } finally {
                if (c != null)
                    c.close();
                if (faviconsDb != null)
                    faviconsDb.close();
            }

            createFaviconsTable(db);

            // Import favicons into the favicons table
            db.execSQL("ALTER TABLE " + TABLE_HISTORY
                    + " ADD COLUMN " + History.FAVICON_ID + " INTEGER");
            db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS
                    + " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER");

            try {
                c = db.query(Obsolete.TABLE_IMAGES,
                        new String[] {
                            Obsolete.Images.URL,
                            Obsolete.Images.FAVICON_URL,
                            Obsolete.Images.FAVICON,
                            Obsolete.Images.DATE_MODIFIED,
                            Obsolete.Images.DATE_CREATED
                        },
                        Obsolete.Images.FAVICON + " IS NOT NULL",
                        null, null, null, null);

                while (c.moveToNext()) {
                    long faviconId = -1;
                    int faviconUrlIndex = c.getColumnIndexOrThrow(Obsolete.Images.FAVICON_URL);
                    String faviconUrl = null;
                    if (!c.isNull(faviconUrlIndex)) {
                        faviconUrl = c.getString(faviconUrlIndex);
                        Cursor c2 = null;
                        try {
                            c2 = db.query(TABLE_FAVICONS,
                                    new String[] { Favicons._ID },
                                    Favicons.URL + " = ?",
                                    new String[] { faviconUrl },
                                    null, null, null);
                            if (c2.moveToFirst()) {
                                faviconId = c2.getLong(c2.getColumnIndexOrThrow(Favicons._ID));
                            }
                        } finally {
                            if (c2 != null)
                                c2.close();
                        }
                    }

                    if (faviconId == -1) {
                        ContentValues values = new ContentValues(4);
                        values.put(Favicons.URL, faviconUrl);
                        values.put(Favicons.DATA, c.getBlob(c.getColumnIndexOrThrow(Obsolete.Images.FAVICON)));
                        values.put(Favicons.DATE_MODIFIED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_MODIFIED)));
                        values.put(Favicons.DATE_CREATED, c.getLong(c.getColumnIndexOrThrow(Obsolete.Images.DATE_CREATED)));
                        faviconId = db.insert(TABLE_FAVICONS, null, values);
                    }

                    ContentValues values = new ContentValues(1);
                    values.put(FaviconColumns.FAVICON_ID, faviconId);
                    db.update(TABLE_HISTORY, values, History.URL + " = ?",
                            new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
                    db.update(TABLE_BOOKMARKS, values, Bookmarks.URL + " = ?",
                            new String[] { c.getString(c.getColumnIndexOrThrow(Obsolete.Images.URL)) });
                }
            } finally {
                if (c != null)
                    c.close();
            }

            createThumbnailsTable(db);

            // Import thumbnails into the thumbnails table
            db.execSQL("INSERT INTO " + TABLE_THUMBNAILS + " ("
                    + Thumbnails.URL + ", "
                    + Thumbnails.DATA + ") "
                    + "SELECT " + Obsolete.Images.URL + ", " + Obsolete.Images.THUMBNAIL
                    + " FROM " + Obsolete.TABLE_IMAGES
                    + " WHERE " + Obsolete.Images.THUMBNAIL + " IS NOT NULL");

            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_BOOKMARKS_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_HISTORY_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + Obsolete.VIEW_COMBINED_WITH_IMAGES);
            db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);

            createBookmarksWithFaviconsView(db);
            createHistoryWithFaviconsView(db);
            createCombinedViewOn13(db);

            db.execSQL("DROP TABLE IF EXISTS " + Obsolete.TABLE_IMAGES);
        }

        private void upgradeDatabaseFrom13to14(SQLiteDatabase db) {
            createOrUpdateSpecialFolder(db, Bookmarks.PINNED_FOLDER_GUID,
                R.string.bookmarks_folder_pinned, 6);
        }

        private void upgradeDatabaseFrom14to15(SQLiteDatabase db) {
            Cursor c = null;
            try {
                // Get all the pinned bookmarks
                c = db.query(TABLE_BOOKMARKS,
                             new String[] { Bookmarks._ID, Bookmarks.URL },
                             Bookmarks.PARENT + " = ?",
                             new String[] { Integer.toString(Bookmarks.FIXED_PINNED_LIST_ID) },
                             null, null, null);

                while (c.moveToNext()) {
                    // Check if this URL can be parsed as a URI with a valid scheme.
                    String url = c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
                    if (Uri.parse(url).getScheme() != null) {
                        continue;
                    }

                    // If it can't, update the URL to be an encoded "user-entered" value.
                    ContentValues values = new ContentValues(1);
                    String newUrl = Uri.fromParts("user-entered", url, null).toString();
                    values.put(Bookmarks.URL, newUrl);
                    db.update(TABLE_BOOKMARKS, values, Bookmarks._ID + " = ?",
                              new String[] { Integer.toString(c.getInt(c.getColumnIndexOrThrow(Bookmarks._ID))) });
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }
        }

        private void upgradeDatabaseFrom15to16(SQLiteDatabase db) {
            db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
            db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);

            createCombinedViewOn16(db);
        }

        private void upgradeDatabaseFrom16to17(SQLiteDatabase db) {
            // Purge any 0-byte favicons/thumbnails
            try {
                db.execSQL("DELETE FROM " + TABLE_FAVICONS +
                        " WHERE length(" + Favicons.DATA + ") = 0");
                db.execSQL("DELETE FROM " + TABLE_THUMBNAILS +
                        " WHERE length(" + Thumbnails.DATA + ") = 0");
            } catch (SQLException e) {
                Log.e(LOGTAG, "Error purging invalid favicons or thumbnails", e);
            }
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            debug("Upgrading browser.db: " + db.getPath() + " from " +
                    oldVersion + " to " + newVersion);

            // We have to do incremental upgrades until we reach the current
            // database schema version.
            for (int v = oldVersion + 1; v <= newVersion; v++) {
                switch(v) {
                    case 2:
                        upgradeDatabaseFrom1to2(db);
                        break;

                    case 3:
                        upgradeDatabaseFrom2to3(db);
                        break;

                    case 4:
                        upgradeDatabaseFrom3to4(db);
                        break;

                    case 5:
                        upgradeDatabaseFrom4to5(db);
                        break;

                    case 6:
                        upgradeDatabaseFrom5to6(db);
                        break;

                    case 7:
                        upgradeDatabaseFrom6to7(db);
                        break;

                    case 8:
                        upgradeDatabaseFrom7to8(db);
                        break;

                    case 9:
                        upgradeDatabaseFrom8to9(db);
                        break;

                    case 10:
                        upgradeDatabaseFrom9to10(db);
                        break;

                    case 11:
                        upgradeDatabaseFrom10to11(db);
                        break;

                    case 12:
                        upgradeDatabaseFrom11to12(db);
                        break;

                    case 13:
                        upgradeDatabaseFrom12to13(db);
                        break;

                    case 14:
                        upgradeDatabaseFrom13to14(db);
                        break;

                    case 15:
                        upgradeDatabaseFrom14to15(db);
                        break;

                    case 16:
                        upgradeDatabaseFrom15to16(db);
                        break;

                    case 17:
                        upgradeDatabaseFrom16to17(db);
                        break;
                }
            }

            // If an upgrade after 12->13 fails, the entire upgrade is rolled
            // back, but we can't undo the deletion of favicon_urls.db if we
            // delete this in step 13; therefore, we wait until all steps are
            // complete before removing it.
            if (oldVersion < 13 && newVersion >= 13
                                && mContext.getDatabasePath(Obsolete.FAVICON_DB).exists()
                                && !mContext.deleteDatabase(Obsolete.FAVICON_DB)) {
                throw new SQLException("Could not delete " + Obsolete.FAVICON_DB);
            }
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            debug("Opening browser.db: " + db.getPath());

            Cursor cursor = null;
            try {
                cursor = db.rawQuery("PRAGMA foreign_keys=ON", null);
            } finally {
                if (cursor != null)
                    cursor.close();
            }
            cursor = null;
            try {
                cursor = db.rawQuery("PRAGMA synchronous=NORMAL", null);
            } finally {
                if (cursor != null)
                    cursor.close();
            }

            // From Honeycomb on, it's possible to run several db
            // commands in parallel using multiple connections.
            if (Build.VERSION.SDK_INT >= 11) {
                db.enableWriteAheadLogging();
                db.setLockingEnabled(false);
            } else {
                // Pre-Honeycomb, we can do some lesser optimizations.
                cursor = null;
                try {
                    cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
                } finally {
                    if (cursor != null)
                        cursor.close();
                }
            }
        }
    }

    private static final String[] mobileIdColumns = new String[] { Bookmarks._ID };
    private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID };

    private Integer getMobileFolderId(SQLiteDatabase db) {
        Cursor c = null;

        try {
            c = db.query(TABLE_BOOKMARKS,
                         mobileIdColumns,
                         Bookmarks.GUID + " = ?",
                         mobileIdSelectionArgs,
                         null, null, null);

            if (c == null || !c.moveToFirst())
                return null;

            return c.getInt(c.getColumnIndex(Bookmarks._ID));
        } finally {
            if (c != null)
                c.close();
        }
    }

    private DatabaseHelper getDatabaseHelperForProfile(String profile, boolean isTest) {
        // Each profile has a separate browser.db database. The target
        // profile is provided using a URI query argument in each request
        // to our content provider.

        // Always fallback to default profile if none has been provided.
        if (TextUtils.isEmpty(profile)) {
            profile = GeckoProfile.get(mContext).getName();
        }

        DatabaseHelper dbHelper;
        synchronized (this) {
            dbHelper = mDatabasePerProfile.get(profile);
            if (dbHelper != null) {
                return dbHelper;
            }

            String databasePath = getDatabasePath(profile, isTest);

            // Before bug 768532, the database was located outside if the
            // profile on Android 2.2. Make sure it is moved inside the profile
            // directory.
            if (Build.VERSION.SDK_INT == 8) {
                File oldPath = mContext.getDatabasePath("browser-" + profile + ".db");
                if (oldPath.exists()) {
                    oldPath.renameTo(new File(databasePath));
                }
            }

            dbHelper = new DatabaseHelper(getContext(), databasePath);
            mDatabasePerProfile.put(profile, dbHelper);

            DBUtils.ensureDatabaseIsNotLocked(dbHelper, databasePath);
        }

        debug("Created database helper for profile: " + profile);
        return dbHelper;
    }

    public String getDatabasePath(String profile, boolean isTest) {
        trace("Getting database path for profile: " + profile);

        if (isTest) {
            return DATABASE_NAME;
        }

        File profileDir = GeckoProfile.get(mContext, profile).getDir();
        if (profileDir == null) {
            debug("Couldn't find directory for profile: " + profile);
            return null;
        }

        String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
        debug("Successfully created database path for profile: " + databasePath);

        return databasePath;
    }

    private SQLiteDatabase getReadableDatabase(Uri uri) {
        trace("Getting readable database for URI: " + uri);

        String profile = null;

        if (uri != null)
            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);

        return getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
    }

    private SQLiteDatabase getWritableDatabase(Uri uri) {
        trace("Getting writable database for URI: " + uri);

        String profile = null;

        if (uri != null)
            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);

        return getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
    }

    private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
        // we cleanup records marked as deleted that are older than a
        // predefined max age. It's important not be too greedy here and
        // remove only a few old deleted records at a time.

        // The PARAM_SHOW_DELETED argument is necessary to return the records
        // that were marked as deleted. We use PARAM_IS_SYNC here to ensure
        // that we'll be actually deleting records instead of flagging them.
        Uri.Builder uriBuilder = targetUri.buildUpon()
                .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(DELETED_RECORDS_PURGE_LIMIT))
                .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
                .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1");

        String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
        if (!TextUtils.isEmpty(profile))
            uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_PROFILE, profile);

        if (isTest(fromUri))
            uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1");

        Uri uriWithArgs = uriBuilder.build();

        Cursor cursor = null;

        try {
            long now = System.currentTimeMillis();
            String selection = SyncColumns.IS_DELETED + " = 1 AND " +
                    SyncColumns.DATE_MODIFIED + " <= " + (now - MAX_AGE_OF_DELETED_RECORDS);

            cursor = query(uriWithArgs,
                           new String[] { CommonColumns._ID },
                           selection,
                           null,
                           null);

            while (cursor.moveToNext()) {
                Uri uriWithId = ContentUris.withAppendedId(uriWithArgs, cursor.getLong(0));
                delete(uriWithId, null, null);

                debug("Removed old deleted item with URI: " + uriWithId);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    /**
     * Remove enough history items to bring the database count below <code>retain</code>,
     * removing no items with a modified time after <code>keepAfter</code>.
     *
     * Provide <code>keepAfter</code> less than or equal to zero to skip that check.
     *
     * Items will be removed according to an approximate frecency calculation.
     *
     * Call this method within a transaction.
     */
    private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
        final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY);

        if (retain >= rows) {
            debug("Not expiring history: only have " + rows + " rows.");
            return;
        }

        final String sortOrder = BrowserContract.getFrecencySortOrder(false, true);
        final long toRemove = rows - retain;
        debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");

        final String sql;
        if (keepAfter > 0) {
            sql = "DELETE FROM " + TABLE_HISTORY + " " +
                  "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED +") < " + keepAfter + " " +
                  " AND " + History._ID + " IN ( SELECT " +
                    History._ID + " FROM " + TABLE_HISTORY + " " +
                    "ORDER BY " + sortOrder + " LIMIT " + toRemove +
                  ")";
        } else {
            sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
                  "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
                  "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
        }
        trace("Deleting using query: " + sql);
        db.execSQL(sql);
    }

    /**
     * Remove any thumbnails that for sites that aren't likely to be ever shown.
     * Items will be removed according to a frecency calculation and only if they are not pinned
     *
     * Call this method within a transaction.
     */
    private void expireThumbnails(final SQLiteDatabase db) {
        final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
        final String sql = "DELETE FROM " + TABLE_THUMBNAILS +
                           " WHERE " + Thumbnails.URL + " NOT IN ( " +
                             " SELECT " + Combined.URL +
                             " FROM " + VIEW_COMBINED +
                             " ORDER BY " + sortOrder +
                             " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT +
                           ") AND " + Thumbnails.URL + " NOT IN ( " +
                             " SELECT " + Bookmarks.URL +
                             " FROM " + TABLE_BOOKMARKS +
                             " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
                           ")";
        trace("Clear thumbs using query: " + sql);
        db.execSQL(sql);
    }

    private boolean isCallerSync(Uri uri) {
        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
        return !TextUtils.isEmpty(isSync);
    }

    private boolean isTest(Uri uri) {
        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
        return !TextUtils.isEmpty(isTest);
    }

    private boolean shouldShowDeleted(Uri uri) {
        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
        return !TextUtils.isEmpty(showDeleted);
    }

    private boolean shouldUpdateOrInsert(Uri uri) {
        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
        return Boolean.parseBoolean(insertIfNeeded);
    }

    private boolean shouldIncrementVisits(Uri uri) {
        String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
        return Boolean.parseBoolean(incrementVisits);
    }

    @Override
    public boolean onCreate() {
        debug("Creating BrowserProvider");

        synchronized (this) {
            mContext = getContext();
            mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
        }

        return true;
    }

    @Override
    public String getType(Uri uri) {
        final int match = URI_MATCHER.match(uri);

        trace("Getting URI type: " + uri);

        switch (match) {
            case BOOKMARKS:
                trace("URI is BOOKMARKS: " + uri);
                return Bookmarks.CONTENT_TYPE;
            case BOOKMARKS_ID:
                trace("URI is BOOKMARKS_ID: " + uri);
                return Bookmarks.CONTENT_ITEM_TYPE;
            case HISTORY:
                trace("URI is HISTORY: " + uri);
                return History.CONTENT_TYPE;
            case HISTORY_ID:
                trace("URI is HISTORY_ID: " + uri);
                return History.CONTENT_ITEM_TYPE;
            case SEARCH_SUGGEST:
                trace("URI is SEARCH_SUGGEST: " + uri);
                return SearchManager.SUGGEST_MIME_TYPE;
        }

        debug("URI has unrecognized type: " + uri);

        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        trace("Calling delete on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int deleted = 0;

        if (Build.VERSION.SDK_INT >= 11) {
            trace("Beginning delete transaction: " + uri);
            db.beginTransaction();
            try {
                deleted = deleteInTransaction(db, uri, selection, selectionArgs);
                db.setTransactionSuccessful();
                trace("Successful delete transaction: " + uri);
            } finally {
                db.endTransaction();
            }
        } else {
            deleted = deleteInTransaction(db, uri, selection, selectionArgs);
        }

        if (deleted > 0)
            getContext().getContentResolver().notifyChange(uri, null);

        return deleted;
    }

    @SuppressWarnings("fallthrough")
    public int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs) {
        trace("Calling delete in transaction on URI: " + uri);

        final int match = URI_MATCHER.match(uri);
        int deleted = 0;

        switch (match) {
            case BOOKMARKS_ID:
                trace("Delete on BOOKMARKS_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case BOOKMARKS: {
                trace("Deleting bookmarks: " + uri);
                deleted = deleteBookmarks(uri, selection, selectionArgs);
                deleteUnusedImages(uri);
                break;
            }

            case HISTORY_ID:
                trace("Delete on HISTORY_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case HISTORY: {
                trace("Deleting history: " + uri);
                deleted = deleteHistory(uri, selection, selectionArgs);
                deleteUnusedImages(uri);
                break;
            }

            case HISTORY_OLD: {
                String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY);
                long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW;
                int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT;

                if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) {
                    keepAfter = 0;
                    retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT;
                }
                expireHistory(db, retainCount, keepAfter);
                expireThumbnails(db);
                deleteUnusedImages(uri);
                break;
            }

            case FAVICON_ID:
                debug("Delete on FAVICON_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case FAVICONS: {
                trace("Deleting favicons: " + uri);
                deleted = deleteFavicons(uri, selection, selectionArgs);
                break;
            }

            case THUMBNAIL_ID:
                debug("Delete on THUMBNAIL_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case THUMBNAILS: {
                trace("Deleting thumbnails: " + uri);
                deleted = deleteThumbnails(uri, selection, selectionArgs);
                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown delete URI " + uri);
        }

        debug("Deleted " + deleted + " rows for URI: " + uri);

        return deleted;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        trace("Calling insert on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        Uri result = null;
        try {
            if (Build.VERSION.SDK_INT >= 11) {
                trace("Beginning insert transaction: " + uri);
                db.beginTransaction();
                try {
                    result = insertInTransaction(uri, values);
                    db.setTransactionSuccessful();
                    trace("Successful insert transaction: " + uri);
                } finally {
                    db.endTransaction();
                }
            } else {
                result = insertInTransaction(uri, values);
            }
        } catch (SQLException sqle) {
            Log.e(LOGTAG, "exception in DB operation", sqle);
        } catch (UnsupportedOperationException uoe) {
            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
        }

        if (result != null)
            getContext().getContentResolver().notifyChange(uri, null);

        return result;
    }

    public Uri insertInTransaction(Uri uri, ContentValues values) {
        trace("Calling insert in transaction on URI: " + uri);

        int match = URI_MATCHER.match(uri);
        long id = -1;

        switch (match) {
            case BOOKMARKS: {
                trace("Insert on BOOKMARKS: " + uri);
                id = insertBookmark(uri, values);
                break;
            }

            case HISTORY: {
                trace("Insert on HISTORY: " + uri);
                id = insertHistory(uri, values);
                break;
            }

            case FAVICONS: {
                trace("Insert on FAVICONS: " + uri);
                id = insertFavicon(uri, values);
                break;
            }

            case THUMBNAILS: {
                trace("Insert on THUMBNAILS: " + uri);
                id = insertThumbnail(uri, values);
                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown insert URI " + uri);
        }

        debug("Inserted ID in database: " + id);

        if (id >= 0)
            return ContentUris.withAppendedId(uri, id);

        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        trace("Calling update on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int updated = 0;

        if (Build.VERSION.SDK_INT >= 11) {
            trace("Beginning update transaction: " + uri);
            db.beginTransaction();
            try {
                updated = updateInTransaction(uri, values, selection, selectionArgs);
                db.setTransactionSuccessful();
                trace("Successful update transaction: " + uri);
            } finally {
                db.endTransaction();
            }
        } else {
            updated = updateInTransaction(uri, values, selection, selectionArgs);
        }

        if (updated > 0)
            getContext().getContentResolver().notifyChange(uri, null);

        return updated;
    }

    @SuppressWarnings("fallthrough")
    public int updateInTransaction(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        trace("Calling update in transaction on URI: " + uri);

        int match = URI_MATCHER.match(uri);
        int updated = 0;

        switch (match) {
            // We provide a dedicated (hacky) API for callers to bulk-update the positions of
            // folder children by passing an array of GUID strings as `selectionArgs`.
            // Each child will have its position column set to its index in the provided array.
            //
            // This avoids callers having to issue a large number of UPDATE queries through
            // the usual channels. See Bug 728783.
            //
            // Note that this is decidedly not a general-purpose API; use at your own risk.
            // `values` and `selection` are ignored.
            case BOOKMARKS_POSITIONS: {
                debug("Update on BOOKMARKS_POSITIONS: " + uri);
                updated = updateBookmarkPositions(uri, selectionArgs);
                break;
            }

            case BOOKMARKS_PARENT: {
                debug("Update on BOOKMARKS_PARENT: " + uri);
                updated = updateBookmarkParents(uri, values, selection, selectionArgs);
                break;
            }

            case BOOKMARKS_ID:
                debug("Update on BOOKMARKS_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case BOOKMARKS: {
                debug("Updating bookmark: " + uri);
                if (shouldUpdateOrInsert(uri))
                    updated = updateOrInsertBookmark(uri, values, selection, selectionArgs);
                else
                    updated = updateBookmarks(uri, values, selection, selectionArgs);
                break;
            }

            case HISTORY_ID:
                debug("Update on HISTORY_ID: " + uri);

                selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case HISTORY: {
                debug("Updating history: " + uri);
                if (shouldUpdateOrInsert(uri))
                    updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
                else
                    updated = updateHistory(uri, values, selection, selectionArgs);
                break;
            }

            case FAVICONS: {
                debug("Update on FAVICONS: " + uri);

                String url = values.getAsString(Favicons.URL);
                String faviconSelection = null;
                String[] faviconSelectionArgs = null;

                if (!TextUtils.isEmpty(url)) {
                    faviconSelection = Favicons.URL + " = ?";
                    faviconSelectionArgs = new String[] { url };
                }

                if (shouldUpdateOrInsert(uri))
                    updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
                else
                    updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);

                break;
            }

            case THUMBNAILS: {
                debug("Update on THUMBNAILS: " + uri);

                String url = values.getAsString(Thumbnails.URL);

                // if no URL is provided, update all of the entries
                if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL)))
                    updated = updateExistingThumbnail(uri, values, null, null);
                else if (shouldUpdateOrInsert(uri))
                    updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?",
                                                      new String[] { url });
                else
                    updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
                                                      new String[] { url });

                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown update URI " + uri);
        }

        debug("Updated " + updated + " rows for URI: " + uri);

        return updated;
    }

    private Cursor controlQuery(Uri uri,
                                String[] projection, String selection,
                                String[] selectionArgs, String sortOrder) {

        trace("controlQuery projection = " + projection);

        final String[] allFields = {
            Control.ENSURE_BOOKMARKS_MIGRATED,
            Control.ENSURE_HISTORY_MIGRATED
        };

        // null projection must return all fields.
        if (projection == null) {
            projection = allFields;
        }

        if (selection != null) {
            throw new UnsupportedOperationException("No selection in virtual CONTROL queries");
        }

        File profileDir = GeckoProfile.get(mContext).getDir();

        if (uri != null) {
            String profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
            if (!TextUtils.isEmpty(profile)) {
                profileDir = GeckoProfile.get(mContext, profile).getDir();
            }
        }

        MatrixCursor cursor = new MatrixCursor(projection);
        MatrixCursor.RowBuilder row = cursor.newRow();
        synchronized (this) {
            boolean wantBookmarks = false;
            boolean wantHistory   = false;

            for (String key : projection) {
                if (key.equals(Control.ENSURE_BOOKMARKS_MIGRATED)) {
                    wantBookmarks = true;
                } else if (key.equals(Control.ENSURE_HISTORY_MIGRATED)) {
                    wantHistory = true;
                }
            }

            if (wantHistory || wantBookmarks) {
                ProfileMigrator migrator = new ProfileMigrator(mContext);

                boolean needBookmarks = wantBookmarks && !migrator.areBookmarksMigrated();
                boolean needHistory = wantHistory && !migrator.isHistoryMigrated();

                if (needBookmarks || needHistory) {
                    migrator.launchPlaces(profileDir);

                    needBookmarks = wantBookmarks && !migrator.areBookmarksMigrated();
                    needHistory = wantHistory && !migrator.isHistoryMigrated();
                    // Bookmarks are expected to finish at the first run.
                    if (needBookmarks) {
                        Log.w(LOGTAG, "Bookmarks migration did not finish.");
                    }
                }

                // Now set the results.
                for (String key: projection) {
                    if (key.equals(Control.ENSURE_BOOKMARKS_MIGRATED)) {
                        row.add(needBookmarks ? 0 : 1);
                    } else if (key.equals(Control.ENSURE_HISTORY_MIGRATED)) {
                        row.add(needHistory   ? 0 : 1);
                    }
                }
            }
        }

        return cursor;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = getReadableDatabase(uri);
        final int match = URI_MATCHER.match(uri);

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
        String groupBy = null;

        switch (match) {
            case BOOKMARKS_FOLDER_ID:
            case BOOKMARKS_ID:
            case BOOKMARKS: {
                debug("Query is on bookmarks: " + uri);

                if (match == BOOKMARKS_ID) {
                    selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                            new String[] { Long.toString(ContentUris.parseId(uri)) });
                } else if (match == BOOKMARKS_FOLDER_ID) {
                    selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                            new String[] { Long.toString(ContentUris.parseId(uri)) });
                }

                if (!shouldShowDeleted(uri))
                    selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);

                if (TextUtils.isEmpty(sortOrder)) {
                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
                } else {
                    debug("Using sort order " + sortOrder + ".");
                }

                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);

                if (hasFaviconsInProjection(projection))
                    qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS);
                else
                    qb.setTables(TABLE_BOOKMARKS);

                break;
            }

            case HISTORY_ID:
                selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case HISTORY: {
                debug("Query is on history: " + uri);

                if (!shouldShowDeleted(uri))
                    selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);

                if (TextUtils.isEmpty(sortOrder))
                    sortOrder = DEFAULT_HISTORY_SORT_ORDER;

                qb.setProjectionMap(HISTORY_PROJECTION_MAP);

                if (hasFaviconsInProjection(projection))
                    qb.setTables(VIEW_HISTORY_WITH_FAVICONS);
                else
                    qb.setTables(TABLE_HISTORY);

                break;
            }

            case FAVICON_ID:
                selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case FAVICONS: {
                debug("Query is on favicons: " + uri);

                qb.setProjectionMap(FAVICONS_PROJECTION_MAP);
                qb.setTables(TABLE_FAVICONS);

                break;
            }

            case THUMBNAIL_ID:
                selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?");
                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { Long.toString(ContentUris.parseId(uri)) });
                // fall through
            case THUMBNAILS: {
                debug("Query is on thumbnails: " + uri);

                qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP);
                qb.setTables(TABLE_THUMBNAILS);

                break;
            }

            case SCHEMA: {
                debug("Query is on schema.");
                MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
                schemaCursor.newRow().add(DATABASE_VERSION);

                return schemaCursor;
            }

            case COMBINED: {
                debug("Query is on combined: " + uri);

                if (TextUtils.isEmpty(sortOrder))
                    sortOrder = DEFAULT_HISTORY_SORT_ORDER;

                // This will avoid duplicate entries in the awesomebar
                // results when a history entry has multiple bookmarks.
                groupBy = Combined.URL;

                qb.setProjectionMap(COMBINED_PROJECTION_MAP);

                if (hasFaviconsInProjection(projection))
                    qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
                else
                    qb.setTables(VIEW_COMBINED);

                break;
            }

            case CONTROL: {
                debug("Query is on control: " + uri);

                Cursor controlCursor =
                    controlQuery(uri, projection, selection, selectionArgs, sortOrder);

                return controlCursor;
            }

            case SEARCH_SUGGEST: {
                debug("Query is on search suggest: " + uri);
                selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
                                                                      Combined.TITLE + " LIKE ?)");

                String keyword = uri.getLastPathSegment();
                if (keyword == null)
                    keyword = "";

                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                        new String[] { "%" + keyword + "%",
                                       "%" + keyword + "%" });

                if (TextUtils.isEmpty(sortOrder))
                    sortOrder = DEFAULT_HISTORY_SORT_ORDER;

                qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP);
                qb.setTables(VIEW_COMBINED_WITH_FAVICONS);

                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown query URI " + uri);
        }

        trace("Running built query.");
        Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
                null, sortOrder, limit);
        cursor.setNotificationUri(getContext().getContentResolver(),
                BrowserContract.AUTHORITY_URI);

        return cursor;
    }

    int getUrlCount(SQLiteDatabase db, String table, String url) {
        Cursor c = db.query(table, new String[] { "COUNT(*)" },
                URLColumns.URL + " = ?", new String[] { url }, null, null,
                null);

        int count = 0;

        try {
            if (c.moveToFirst())
                count = c.getInt(0);
        } finally {
            c.close();
        }

        return count;
    }

    /**
     * Update the positions of bookmarks in batches.
     *
     * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
     */
    int updateBookmarkPositions(Uri uri, String[] guids) {
        if (guids == null)
            return 0;

        int guidsCount = guids.length;
        if (guidsCount == 0)
            return 0;

        final SQLiteDatabase db = getWritableDatabase(uri);
        int offset = 0;
        int updated = 0;

        db.beginTransaction();

        while (offset < guidsCount) {
            try {
                updated += updateBookmarkPositionsInTransaction(db, guids, offset,
                                                                MAX_POSITION_UPDATES_PER_QUERY);
            } catch (SQLException e) {
                Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e);

                // Need to restart the transaction.
                // The only way a caller knows that anything failed is that the
                // returned update count will be smaller than the requested
                // number of records.
                db.setTransactionSuccessful();
                db.endTransaction();

                db.beginTransaction();
            }

            offset += MAX_POSITION_UPDATES_PER_QUERY;
        }

        db.setTransactionSuccessful();
        db.endTransaction();

        return updated;
    }

    /**
     * Construct and execute an update expression that will modify the positions
     * of records in-place.
     */
    int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
                                             final int offset, final int max) {
        int guidsCount = guids.length;
        int processCount = Math.min(max, guidsCount - offset);

        // Each must appear twice: once in a CASE, and once in the IN clause.
        String[] args = new String[processCount * 2];
        System.arraycopy(guids, offset, args, 0, processCount);
        System.arraycopy(guids, offset, args, processCount, processCount);

        StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS +
                                            " SET " + Bookmarks.POSITION +
                                            " = CASE guid");

        // Build the CASE statement body for GUID/index pairs from offset up to
        // the computed limit.
        final int end = offset + processCount;
        int i = offset;
        for (; i < end; ++i) {
            if (guids[i] == null) {
                // We don't want to issue the query if not every GUID is specified.
                debug("updateBookmarkPositions called with null GUID at index " + i);
                return 0;
            }
            b.append(" WHEN ? THEN " + i);
        }

        b.append(" END WHERE " + Bookmarks.GUID + " IN (");
        i = 1;
        while (i++ < processCount) {
            b.append("?, ");
        }
        b.append("?)");
        db.execSQL(b.toString(), args);

        // We can't easily get a modified count without calling something like changes().
        return processCount;
    }

    /**
     * Construct an update expression that will modify the parents of any records
     * that match.
     */
    int updateBookmarkParents(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
        String where = Bookmarks._ID + " IN (" +
                       " SELECT DISTINCT " + Bookmarks.PARENT +
                       " FROM " + TABLE_BOOKMARKS +
                       " WHERE " + selection + " )";
        return getWritableDatabase(uri).update(TABLE_BOOKMARKS, values, where, selectionArgs);
    }

    long insertBookmark(Uri uri, ContentValues values) {
        // Generate values if not specified. Don't overwrite
        // if specified by caller.
        long now = System.currentTimeMillis();
        if (!values.containsKey(Bookmarks.DATE_CREATED)) {
            values.put(Bookmarks.DATE_CREATED, now);
        }

        if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
            values.put(Bookmarks.DATE_MODIFIED, now);
        }

        if (!values.containsKey(Bookmarks.GUID)) {
            values.put(Bookmarks.GUID, Utils.generateGuid());
        }

        if (!values.containsKey(Bookmarks.POSITION)) {
            debug("Inserting bookmark with no position for URI");
            values.put(Bookmarks.POSITION,
                       Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION));
        }

        String url = values.getAsString(Bookmarks.URL);
        Integer type = values.getAsInteger(Bookmarks.TYPE);

        debug("Inserting bookmark in database with URL: " + url);
        final SQLiteDatabase db = getWritableDatabase(uri);
        return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
    }


    int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        int updated = updateBookmarks(uri, values, selection, selectionArgs);
        if (updated > 0)
            return updated;

        insertBookmark(uri, values);

        // Return 0 if we added a new row
        return 0;
    }

    int updateBookmarks(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        trace("Updating bookmarks on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int updated = 0;

        final String[] bookmarksProjection = new String[] {
                Bookmarks._ID, // 0
                Bookmarks.URL, // 1
        };

        trace("Quering bookmarks to update on URI: " + uri);

        Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
                selection, selectionArgs, null, null, null);

        try {
            if (!values.containsKey(Bookmarks.DATE_MODIFIED))
                values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());

            boolean updatingUrl = values.containsKey(Bookmarks.URL);
            String url = null;

            if (updatingUrl)
                url = values.getAsString(Bookmarks.URL);

            while (cursor.moveToNext()) {
                long id = cursor.getLong(0);

                trace("Updating bookmark with ID: " + id);

                updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
                        new String[] { Long.toString(id) });
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }

        return updated;
    }

    long insertHistory(Uri uri, ContentValues values) {
        final SQLiteDatabase db = getWritableDatabase(uri);

        long now = System.currentTimeMillis();
        values.put(History.DATE_CREATED, now);
        values.put(History.DATE_MODIFIED, now);

        // Generate GUID for new history entry. Don't override specified GUIDs.
        if (!values.containsKey(History.GUID)) {
          values.put(History.GUID, Utils.generateGuid());
        }

        String url = values.getAsString(History.URL);

        debug("Inserting history in database with URL: " + url);
        return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
    }

    int updateOrInsertHistory(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        int updated = updateHistory(uri, values, selection, selectionArgs);
        if (updated > 0)
            return updated;

        // Insert a new entry if necessary
        if (!values.containsKey(History.VISITS))
            values.put(History.VISITS, 1);
        if (!values.containsKey(History.TITLE))
            values.put(History.TITLE, values.getAsString(History.URL));

        insertHistory(uri, values);

        // Return 0 if we added a new row
        return 0;
    }

    int updateHistory(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        trace("Updating history on URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);
        int updated = 0;

        final String[] historyProjection = new String[] {
            History._ID,   // 0
            History.URL,   // 1
            History.VISITS // 2
        };

        Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
                selectionArgs, null, null, null);

        try {
            if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
                values.put(Bookmarks.DATE_MODIFIED,  System.currentTimeMillis());
            }

            boolean updatingUrl = values.containsKey(History.URL);
            String url = null;

            if (updatingUrl)
                url = values.getAsString(History.URL);

            while (cursor.moveToNext()) {
                long id = cursor.getLong(0);

                trace("Updating history entry with ID: " + id);

                if (shouldIncrementVisits(uri)) {
                    long existing = cursor.getLong(2);
                    Long additional = values.getAsLong(History.VISITS);

                    // Increment visit count by a specified amount, or default to increment by 1
                    values.put(History.VISITS, existing + ((additional != null) ? additional.longValue() : 1));
                }

                updated += db.update(TABLE_HISTORY, values, "_id = ?",
                        new String[] { Long.toString(id) });
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }

        return updated;
    }

    private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) {
        ContentValues updateValues = new ContentValues(1);
        updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
        db.update(TABLE_HISTORY,
                  updateValues,
                  History.URL + " = ?",
                  new String[] { pageUrl });
        db.update(TABLE_BOOKMARKS,
                  updateValues,
                  Bookmarks.URL + " = ?",
                  new String[] { pageUrl });
    }

    long insertFavicon(Uri uri, ContentValues values) {
        return insertFavicon(getWritableDatabase(uri), values);
    }

    long insertFavicon(SQLiteDatabase db, ContentValues values) {
        String faviconUrl = values.getAsString(Favicons.URL);
        String pageUrl = null;
        long faviconId;

        trace("Inserting favicon for URL: " + faviconUrl);

        stripEmptyByteArray(values, Favicons.DATA);

        // Extract the page URL from the ContentValues
        if (values.containsKey(Favicons.PAGE_URL)) {
            pageUrl = values.getAsString(Favicons.PAGE_URL);
            values.remove(Favicons.PAGE_URL);
        }

        long now = System.currentTimeMillis();
        values.put(Favicons.DATE_CREATED, now);
        values.put(Favicons.DATE_MODIFIED, now);
        faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);

        if (pageUrl != null) {
            updateFaviconIdsForUrl(db, pageUrl, faviconId);
        }

        return faviconId;
    }

    int updateOrInsertFavicon(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return updateFavicon(uri, values, selection, selectionArgs,
                true /* insert if needed */);
    }

    int updateExistingFavicon(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return updateFavicon(uri, values, selection, selectionArgs,
                false /* only update, no insert */);
    }

    int updateFavicon(Uri uri, ContentValues values, String selection,
            String[] selectionArgs, boolean insertIfNeeded) {
        String faviconUrl = values.getAsString(Favicons.URL);
        String pageUrl = null;
        int updated = 0;
        final SQLiteDatabase db = getWritableDatabase(uri);
        Cursor cursor = null;
        Long faviconId = null;
        long now = System.currentTimeMillis();

        trace("Updating favicon for URL: " + faviconUrl);

        stripEmptyByteArray(values, Favicons.DATA);

        // Extract the page URL from the ContentValues
        if (values.containsKey(Favicons.PAGE_URL)) {
            pageUrl = values.getAsString(Favicons.PAGE_URL);
            values.remove(Favicons.PAGE_URL);
        }

        values.put(Favicons.DATE_MODIFIED, now);

        // If there's no favicon URL given and we're inserting if needed, skip
        // the update and only do an insert (otherwise all rows would be
        // updated)
        if (!(insertIfNeeded && (faviconUrl == null))) {
            updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs);
        }

        if (updated > 0) {
            if ((faviconUrl != null) && (pageUrl != null)) {
                try {
                    cursor = db.query(TABLE_FAVICONS,
                                      new String[] { Favicons._ID },
                                      Favicons.URL + " = ?",
                                      new String[] { faviconUrl },
                                      null, null, null);
                    if (cursor.moveToFirst()) {
                        faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID));
                    }
                } finally {
                    if (cursor != null)
                        cursor.close();
                }
            }
        } else if (insertIfNeeded) {
            values.put(Favicons.DATE_CREATED, now);

            trace("No update, inserting favicon for URL: " + faviconUrl);
            faviconId = db.insert(TABLE_FAVICONS, null, values);
            updated = 1;
        }

        if (pageUrl != null) {
            updateFaviconIdsForUrl(db, pageUrl, faviconId);
        }

        return updated;
    }

    long insertThumbnail(Uri uri, ContentValues values) {
        String url = values.getAsString(Thumbnails.URL);
        final SQLiteDatabase db = getWritableDatabase(uri);

        trace("Inserting thumbnail for URL: " + url);

        stripEmptyByteArray(values, Thumbnails.DATA);

        return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
    }

    int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return updateThumbnail(uri, values, selection, selectionArgs,
                true /* insert if needed */);
    }

    int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return updateThumbnail(uri, values, selection, selectionArgs,
                false /* only update, no insert */);
    }

    int updateThumbnail(Uri uri, ContentValues values, String selection,
            String[] selectionArgs, boolean insertIfNeeded) {
        String url = values.getAsString(Thumbnails.URL);
        int updated = 0;
        final SQLiteDatabase db = getWritableDatabase(uri);

        stripEmptyByteArray(values, Thumbnails.DATA);

        trace("Updating thumbnail for URL: " + url);

        updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);

        if (updated == 0 && insertIfNeeded) {
            trace("No update, inserting thumbnail for URL: " + url);
            db.insert(TABLE_THUMBNAILS, null, values);
            updated = 1;
        }

        return updated;
    }

    /**
     * Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
     * @param values        ContentValues of query
     * @param columnName    Name of data column to verify
     */
    private void stripEmptyByteArray(ContentValues values, String columnName) {
        if (values.containsKey(columnName)) {
            byte[] data = values.getAsByteArray(columnName);
            if (data == null || data.length == 0) {
                Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
                values.putNull(columnName);
            }
        }
    }

    int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
        debug("Deleting history entry for URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);

        if (isCallerSync(uri)) {
            return db.delete(TABLE_HISTORY, selection, selectionArgs);
        }

        debug("Marking history entry as deleted for URI: " + uri);

        ContentValues values = new ContentValues();
        values.put(History.IS_DELETED, 1);

        // Wipe sensitive data.
        values.putNull(History.TITLE);
        values.put(History.URL, "");          // Column is NOT NULL.
        values.put(History.DATE_CREATED, 0);
        values.put(History.DATE_LAST_VISITED, 0);
        values.put(History.VISITS, 0);
        values.put(History.DATE_MODIFIED, System.currentTimeMillis());

        cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
        return db.update(TABLE_HISTORY, values, selection, selectionArgs);
    }

    int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
        debug("Deleting bookmarks for URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);

        if (isCallerSync(uri)) {
            return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
        }

        debug("Marking bookmarks as deleted for URI: " + uri);

        ContentValues values = new ContentValues();
        values.put(Bookmarks.IS_DELETED, 1);

        cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
        return updateBookmarks(uri, values, selection, selectionArgs);
    }

    int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
        debug("Deleting favicons for URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);

        return db.delete(TABLE_FAVICONS, selection, selectionArgs);
    }

    int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) {
        debug("Deleting thumbnails for URI: " + uri);

        final SQLiteDatabase db = getWritableDatabase(uri);

        return db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
    }

    int deleteUnusedImages(Uri uri) {
        debug("Deleting all unused favicons and thumbnails for URI: " + uri);

        String faviconSelection = Favicons._ID + " NOT IN "
                + "(SELECT " + History.FAVICON_ID
                + " FROM " + TABLE_HISTORY
                + " WHERE " + History.IS_DELETED + " = 0"
                + " AND " + History.FAVICON_ID + " IS NOT NULL"
                + " UNION ALL SELECT " + Bookmarks.FAVICON_ID
                + " FROM " + TABLE_BOOKMARKS
                + " WHERE " + Bookmarks.IS_DELETED + " = 0"
                + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)";

        String thumbnailSelection = Thumbnails.URL + " NOT IN "
                + "(SELECT " + History.URL
                + " FROM " + TABLE_HISTORY
                + " WHERE " + History.IS_DELETED + " = 0"
                + " AND " + History.URL + " IS NOT NULL"
                + " UNION ALL SELECT " + Bookmarks.URL
                + " FROM " + TABLE_BOOKMARKS
                + " WHERE " + Bookmarks.IS_DELETED + " = 0"
                + " AND " + Bookmarks.URL + " IS NOT NULL)";

        return deleteFavicons(uri, faviconSelection, null) +
               deleteThumbnails(uri, thumbnailSelection, null);
    }

    @Override
    public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        boolean failures = false;
        SQLiteDatabase db = null;

        if (numOperations >= 1) {
            // We only have 1 database for all Uri's that we can get
            db = getWritableDatabase(operations.get(0).getUri());
        } else {
            // The original Android implementation returns a zero-length
            // array in this case, we do the same.
            return results;
        }

        // Note that the apply() call may cause us to generate
        // additional transactions for the invidual operations.
        // But Android's wrapper for SQLite supports nested transactions,
        // so this will do the right thing.
        db.beginTransaction();

        for (int i = 0; i < numOperations; i++) {
            try {
                results[i] = operations.get(i).apply(this, results, i);
            } catch (SQLException e) {
                Log.w(LOGTAG, "SQLite Exception during applyBatch: ", e);
                // The Android API makes it implementation-defined whether
                // the failure of a single operation makes all others abort
                // or not. For our use cases, best-effort operation makes
                // more sense. Rolling back and forcing the caller to retry
                // after it figures out what went wrong isn't very convenient
                // anyway.
                // Signal failed operation back, so the caller knows what
                // went through and what didn't.
                results[i] = new ContentProviderResult(0);
                failures = true;
                // http://www.sqlite.org/lang_conflict.html
                // Note that we need a new transaction, subsequent operations
                // on this one will fail (we're in ABORT by default, which
                // isn't IGNORE). We still need to set it as successful to let
                // everything before the failed op go through.
                // We can't set conflict resolution on API level < 8, and even
                // above 8 it requires splitting the call per operation
                // (insert/update/delete).
                db.setTransactionSuccessful();
                db.endTransaction();
                db.beginTransaction();
            } catch (OperationApplicationException e) {
                // Repeat of above.
                results[i] = new ContentProviderResult(0);
                failures = true;
                db.setTransactionSuccessful();
                db.endTransaction();
                db.beginTransaction();
            }
        }

        trace("Flushing DB applyBatch...");
        db.setTransactionSuccessful();
        db.endTransaction();

        if (failures) {
            throw new OperationApplicationException();
        }

        return results;
    }

    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        if (values == null)
            return 0;

        int numValues = values.length;
        int successes = 0;

        final SQLiteDatabase db = getWritableDatabase(uri);

        db.beginTransaction();

        try {
            for (int i = 0; i < numValues; i++) {
                insertInTransaction(uri, values[i]);
                successes++;
            }
            trace("Flushing DB bulkinsert...");
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }

        if (successes > 0)
            mContext.getContentResolver().notifyChange(uri, null);

        return successes;
    }
}
