diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java | 2237 |
1 files changed, 2237 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java new file mode 100644 index 000000000..f823d9060 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java @@ -0,0 +1,2237 @@ +/* -*- 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 java.io.File; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.mozilla.apache.commons.codec.binary.Base32; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.R; +import org.mozilla.gecko.annotation.RobocopTarget; +import org.mozilla.gecko.db.BrowserContract.ActivityStreamBlocklist; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserContract.Favicons; +import org.mozilla.gecko.db.BrowserContract.History; +import org.mozilla.gecko.db.BrowserContract.Visits; +import org.mozilla.gecko.db.BrowserContract.PageMetadata; +import org.mozilla.gecko.db.BrowserContract.Numbers; +import org.mozilla.gecko.db.BrowserContract.ReadingListItems; +import org.mozilla.gecko.db.BrowserContract.SearchHistory; +import org.mozilla.gecko.db.BrowserContract.Thumbnails; +import org.mozilla.gecko.db.BrowserContract.UrlAnnotations; +import org.mozilla.gecko.fxa.FirefoxAccounts; +import org.mozilla.gecko.reader.SavedReaderViewHelper; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.android.RepoUtils; +import org.mozilla.gecko.util.FileUtils; + +import static org.mozilla.gecko.db.DBUtils.qualifyColumn; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + + +// public for robocop testing +public final class BrowserDatabaseHelper extends SQLiteOpenHelper { + private static final String LOGTAG = "GeckoBrowserDBHelper"; + + // Replace the Bug number below with your Bug that is conducting a DB upgrade, as to force a merge conflict with any + // other patches that require a DB upgrade. + public static final int DATABASE_VERSION = 36; // Bug 1301717 + public static final String DATABASE_NAME = "browser.db"; + + final protected Context mContext; + + static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; + static final String TABLE_HISTORY = History.TABLE_NAME; + static final String TABLE_VISITS = Visits.TABLE_NAME; + static final String TABLE_PAGE_METADATA = PageMetadata.TABLE_NAME; + static final String TABLE_FAVICONS = Favicons.TABLE_NAME; + static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; + static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME; + static final String TABLE_TABS = TabsProvider.TABLE_TABS; + static final String TABLE_CLIENTS = TabsProvider.TABLE_CLIENTS; + static final String TABLE_LOGINS = BrowserContract.Logins.TABLE_LOGINS; + static final String TABLE_DELETED_LOGINS = BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS; + static final String TABLE_DISABLED_HOSTS = BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS; + static final String TABLE_ANNOTATIONS = UrlAnnotations.TABLE_NAME; + + static final String VIEW_COMBINED = Combined.VIEW_NAME; + static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; + static final String VIEW_BOOKMARKS_WITH_ANNOTATIONS = Bookmarks.VIEW_WITH_ANNOTATIONS; + static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; + static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; + + 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_BOOKMARKS_JOIN_ANNOTATIONS = TABLE_BOOKMARKS + " JOIN " + + TABLE_ANNOTATIONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.URL); + + 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 String TABLE_BOOKMARKS_TMP = TABLE_BOOKMARKS + "_tmp"; + static final String TABLE_HISTORY_TMP = TABLE_HISTORY + "_tmp"; + + private static final String[] mobileIdColumns = new String[] { Bookmarks._ID }; + private static final String[] mobileIdSelectionArgs = new String[] { Bookmarks.MOBILE_FOLDER_GUID }; + + private boolean didCreateTabsTable = false; + private boolean didCreateCurrentReadingListTable = false; + + public BrowserDatabaseHelper(Context context, String databasePath) { + super(context, databasePath, null, DATABASE_VERSION); + mContext = context; + } + + private void createBookmarksTable(SQLiteDatabase db) { + debug("Creating " + TABLE_BOOKMARKS + " table"); + + 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, " + + "FOREIGN KEY (" + Bookmarks.PARENT + ") REFERENCES " + + TABLE_BOOKMARKS + "(" + Bookmarks._ID + ")" + + ");"); + + 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," + + // Can we drop VISITS count? Can we calculate it in the Combined view as a sum? + // See Bug 1277329. + History.VISITS + " INTEGER NOT NULL DEFAULT 0," + + History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0," + + History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0," + + History.FAVICON_ID + " INTEGER," + + History.DATE_LAST_VISITED + " INTEGER," + + History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," + + History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0," + + 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 createVisitsTable(SQLiteDatabase db) { + debug("Creating " + TABLE_VISITS + " table"); + db.execSQL("CREATE TABLE " + TABLE_VISITS + "(" + + Visits._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Visits.HISTORY_GUID + " TEXT NOT NULL," + + Visits.VISIT_TYPE + " TINYINT NOT NULL DEFAULT 1," + + Visits.DATE_VISITED + " INTEGER NOT NULL, " + + Visits.IS_LOCAL + " TINYINT NOT NULL DEFAULT 1, " + + + "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " + + TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" + + ");"); + + db.execSQL("CREATE UNIQUE INDEX visits_history_guid_and_date_visited_index ON " + TABLE_VISITS + "(" + + Visits.HISTORY_GUID + "," + Visits.DATE_VISITED + ")"); + db.execSQL("CREATE INDEX visits_history_guid_index ON " + TABLE_VISITS + "(" + Visits.HISTORY_GUID + ")"); + } + + 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_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" + + ");"); + } + + private void createPageMetadataTable(SQLiteDatabase db) { + debug("Creating " + TABLE_PAGE_METADATA + " table"); + db.execSQL("CREATE TABLE " + TABLE_PAGE_METADATA + "(" + + PageMetadata._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + PageMetadata.HISTORY_GUID + " TEXT NOT NULL," + + PageMetadata.DATE_CREATED + " INTEGER NOT NULL, " + + PageMetadata.HAS_IMAGE + " TINYINT NOT NULL DEFAULT 0, " + + PageMetadata.JSON + " TEXT NOT NULL, " + + + "FOREIGN KEY (" + Visits.HISTORY_GUID + ") REFERENCES " + + TABLE_HISTORY + "(" + History.GUID + ") ON DELETE CASCADE ON UPDATE CASCADE" + + ");"); + + // Establish a 1-to-1 relationship with History table. + db.execSQL("CREATE UNIQUE INDEX page_metadata_history_guid ON " + TABLE_PAGE_METADATA + "(" + + PageMetadata.HISTORY_GUID + ")"); + // Improve performance of commonly occurring selections. + db.execSQL("CREATE INDEX page_metadata_history_guid_and_has_image ON " + TABLE_PAGE_METADATA + "(" + + PageMetadata.HISTORY_GUID + ", " + PageMetadata.HAS_IMAGE + ")"); + } + + 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 createBookmarksWithAnnotationsView(SQLiteDatabase db) { + debug("Creating " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " view"); + + db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_BOOKMARKS_WITH_ANNOTATIONS + " AS " + + "SELECT " + qualifyColumn(TABLE_BOOKMARKS, "*") + + ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.KEY) + " AS " + Bookmarks.ANNOTATION_KEY + + ", " + qualifyColumn(TABLE_ANNOTATIONS, UrlAnnotations.VALUE) + " AS " + Bookmarks.ANNOTATION_VALUE + + " FROM " + TABLE_BOOKMARKS_JOIN_ANNOTATIONS); + } + + 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 createClientsTable(SQLiteDatabase db) { + debug("Creating " + TABLE_CLIENTS + " table"); + + // Table for client's name-guid mapping. + db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" + + BrowserContract.Clients.GUID + " TEXT PRIMARY KEY," + + BrowserContract.Clients.NAME + " TEXT," + + BrowserContract.Clients.LAST_MODIFIED + " INTEGER," + + BrowserContract.Clients.DEVICE_TYPE + " TEXT" + + ");"); + } + + private void createTabsTable(SQLiteDatabase db, final String tableName) { + debug("Creating tabs.db: " + db.getPath()); + debug("Creating " + tableName + " table"); + + // Table for each tab on any client. + db.execSQL("CREATE TABLE " + tableName + "(" + + BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + BrowserContract.Tabs.CLIENT_GUID + " TEXT," + + BrowserContract.Tabs.TITLE + " TEXT," + + BrowserContract.Tabs.URL + " TEXT," + + BrowserContract.Tabs.HISTORY + " TEXT," + + BrowserContract.Tabs.FAVICON + " TEXT," + + BrowserContract.Tabs.LAST_USED + " INTEGER," + + BrowserContract.Tabs.POSITION + " INTEGER, " + + "FOREIGN KEY (" + BrowserContract.Tabs.CLIENT_GUID + ") REFERENCES " + + TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ") ON DELETE CASCADE" + + ");"); + + didCreateTabsTable = true; + } + + private void createTabsTableIndices(SQLiteDatabase db, final String tableName) { + // Indices on CLIENT_GUID and POSITION. + db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID + + " ON " + tableName + "(" + BrowserContract.Tabs.CLIENT_GUID + ")"); + db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION + + " ON " + tableName + "(" + BrowserContract.Tabs.POSITION + ")"); + } + + // Insert a client row for our local Fennec client. + private void createLocalClient(SQLiteDatabase db) { + debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table"); + + ContentValues values = new ContentValues(); + values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); + db.insertOrThrow(TABLE_CLIENTS, null, values); + } + + private void createCombinedViewOn19(SQLiteDatabase db) { + /* + The v19 combined view removes the redundant subquery from the v16 + combined view and reorders the columns as necessary to prevent this + from breaking any code that might be referencing columns by index. + + The rows in the ensuing view are, in order: + + Combined.BOOKMARK_ID + Combined.HISTORY_ID + Combined._ID (always 0) + Combined.URL + Combined.TITLE + Combined.VISITS + Combined.DATE_LAST_VISITED + Combined.FAVICON_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. + */ + + db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + + + // Bookmarks without history. + " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + + "-1 AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._ID + "," + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + + "-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) + + + // 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. + " WHEN 0 THEN " + + "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._ID) + " AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._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 + "," + + 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 + + + // We really shouldn't be selecting deleted bookmarks, but oh well. + " FROM " + TABLE_HISTORY + " LEFT OUTER JOIN " + TABLE_BOOKMARKS + + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + + " WHERE " + + qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + + "(" + + // The left outer join didn't match... + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + + + // ... or it's a bookmark. This is less efficient than filtering prior + // to the join if you have lots of folders. + 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 createCombinedViewOn33(final SQLiteDatabase db) { + /* + Builds on top of v19 combined view, and adds the following aggregates: + - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits + - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits + - Combined.LOCAL_VISITS_COUNT - total number of local visits + - Combined.REMOTE_VISITS_COUNT - total number of remote visits + + Any code written prior to v33 referencing columns by index directly remains intact + (yet must die a fiery death), as new columns were added to the end of the list. + + The rows in the ensuing view are, in order: + Combined.BOOKMARK_ID + Combined.HISTORY_ID + Combined._ID (always 0) + Combined.URL + Combined.TITLE + Combined.VISITS + Combined.DATE_LAST_VISITED + Combined.FAVICON_ID + Combined.LOCAL_DATE_LAST_VISITED + Combined.REMOTE_DATE_LAST_VISITED + Combined.LOCAL_VISITS_COUNT + Combined.REMOTE_VISITS_COUNT + */ + db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + + + // Bookmarks without history. + " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + + "-1 AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._ID + "," + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + + "-1 AS " + Combined.VISITS + ", " + + "-1 AS " + Combined.DATE_LAST_VISITED + "," + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + + "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + + "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + + "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " + + "0 AS " + Combined.REMOTE_VISITS_COUNT + + " 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) + + + // 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. + " WHEN 0 THEN " + + "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._ID) + " AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._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 + "," + + 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 + "," + + + // Figure out "last visited" days using MAX values for visit timestamps. + // We use CASE statements here to separate local from remote visits. + "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " + + "WHEN 1 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " + + "ELSE 0 END" + + "), 0) AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + + + "COALESCE(MAX(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " " + + "WHEN 0 THEN " + qualifyColumn(TABLE_VISITS, Visits.DATE_VISITED) + " " + + "ELSE 0 END" + + "), 0) AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + + + // Sum up visit counts for local and remote visit types. Again, use CASE to separate the two. + "COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0) AS " + Combined.LOCAL_VISITS_COUNT + ", " + + "COALESCE(SUM(CASE " + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + " WHEN 0 THEN 1 ELSE 0 END), 0) AS " + Combined.REMOTE_VISITS_COUNT + + + // We need to JOIN on Visits in order to compute visit counts + " FROM " + TABLE_HISTORY + " " + + "LEFT OUTER JOIN " + TABLE_VISITS + + " ON " + qualifyColumn(TABLE_HISTORY, History.GUID) + " = " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " " + + + // We really shouldn't be selecting deleted bookmarks, but oh well. + "LEFT OUTER JOIN " + TABLE_BOOKMARKS + + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + + " WHERE " + + qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + + "(" + + // The left outer join didn't match... + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + + + // ... or it's a bookmark. This is less efficient than filtering prior + // to the join if you have lots of folders. + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + + + ") GROUP BY " + qualifyColumn(TABLE_HISTORY, History.GUID) + ); + + 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 createCombinedViewOn34(final SQLiteDatabase db) { + /* + Builds on top of v33 combined view, and instead of calculating the following aggregates, gets them + from the history table: + - Combined.LOCAL_DATE_LAST_VISITED - last date visited for all local visits + - Combined.REMOTE_DATE_LAST_VISITED - last date visited for all remote visits + - Combined.LOCAL_VISITS_COUNT - total number of local visits + - Combined.REMOTE_VISITS_COUNT - total number of remote visits + + Any code written prior to v33 referencing columns by index directly remains intact + (yet must die a fiery death), as new columns were added to the end of the list. + + The rows in the ensuing view are, in order: + Combined.BOOKMARK_ID + Combined.HISTORY_ID + Combined._ID (always 0) + Combined.URL + Combined.TITLE + Combined.VISITS + Combined.DATE_LAST_VISITED + Combined.FAVICON_ID + Combined.LOCAL_DATE_LAST_VISITED + Combined.REMOTE_DATE_LAST_VISITED + Combined.LOCAL_VISITS_COUNT + Combined.REMOTE_VISITS_COUNT + */ + db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_COMBINED + " AS" + + + // Bookmarks without history. + " SELECT " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + "," + + "-1 AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._ID + "," + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " AS " + Combined.URL + ", " + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TITLE) + " AS " + Combined.TITLE + ", " + + "-1 AS " + Combined.VISITS + ", " + + "-1 AS " + Combined.DATE_LAST_VISITED + "," + + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " AS " + Combined.FAVICON_ID + "," + + "0 AS " + Combined.LOCAL_DATE_LAST_VISITED + ", " + + "0 AS " + Combined.REMOTE_DATE_LAST_VISITED + ", " + + "0 AS " + Combined.LOCAL_VISITS_COUNT + ", " + + "0 AS " + Combined.REMOTE_VISITS_COUNT + + " 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) + + + // 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. + " WHEN 0 THEN " + + "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._ID) + " AS " + Combined.HISTORY_ID + "," + + "0 AS " + Combined._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 + "," + + 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 + "," + + + qualifyColumn(TABLE_HISTORY, History.LOCAL_DATE_LAST_VISITED) + " AS " + Combined.LOCAL_DATE_LAST_VISITED + "," + + qualifyColumn(TABLE_HISTORY, History.REMOTE_DATE_LAST_VISITED) + " AS " + Combined.REMOTE_DATE_LAST_VISITED + "," + + qualifyColumn(TABLE_HISTORY, History.LOCAL_VISITS) + " AS " + Combined.LOCAL_VISITS_COUNT + "," + + qualifyColumn(TABLE_HISTORY, History.REMOTE_VISITS) + " AS " + Combined.REMOTE_VISITS_COUNT + + + // We need to JOIN on Visits in order to compute visit counts + " FROM " + TABLE_HISTORY + " " + + + // We really shouldn't be selecting deleted bookmarks, but oh well. + "LEFT OUTER JOIN " + TABLE_BOOKMARKS + + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.URL) + " = " + qualifyColumn(TABLE_HISTORY, History.URL) + + " WHERE " + + qualifyColumn(TABLE_HISTORY, History.IS_DELETED) + " = 0 AND " + + "(" + + // The left outer join didn't match... + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.TYPE) + " IS NULL OR " + + + // ... or it's a bookmark. This is less efficient than filtering prior + // to the join if you have lots of folders. + 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 createLoginsTable(SQLiteDatabase db, final String tableName) { + debug("Creating logins.db: " + db.getPath()); + debug("Creating " + tableName + " table"); + + // Table for each login. + db.execSQL("CREATE TABLE " + tableName + "(" + + BrowserContract.Logins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + BrowserContract.Logins.HOSTNAME + " TEXT NOT NULL," + + BrowserContract.Logins.HTTP_REALM + " TEXT," + + BrowserContract.Logins.FORM_SUBMIT_URL + " TEXT," + + BrowserContract.Logins.USERNAME_FIELD + " TEXT NOT NULL," + + BrowserContract.Logins.PASSWORD_FIELD + " TEXT NOT NULL," + + BrowserContract.Logins.ENCRYPTED_USERNAME + " TEXT NOT NULL," + + BrowserContract.Logins.ENCRYPTED_PASSWORD + " TEXT NOT NULL," + + BrowserContract.Logins.GUID + " TEXT UNIQUE NOT NULL," + + BrowserContract.Logins.ENC_TYPE + " INTEGER NOT NULL, " + + BrowserContract.Logins.TIME_CREATED + " INTEGER," + + BrowserContract.Logins.TIME_LAST_USED + " INTEGER," + + BrowserContract.Logins.TIME_PASSWORD_CHANGED + " INTEGER," + + BrowserContract.Logins.TIMES_USED + " INTEGER" + + ");"); + } + + private void createLoginsTableIndices(SQLiteDatabase db, final String tableName) { + // No need to create an index on GUID, it is an unique column. + db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME + + " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + ")"); + db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_FORM_SUBMIT_URL + + " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.FORM_SUBMIT_URL + ")"); + db.execSQL("CREATE INDEX " + LoginsProvider.INDEX_LOGINS_HOSTNAME_HTTP_REALM + + " ON " + tableName + "(" + BrowserContract.Logins.HOSTNAME + "," + BrowserContract.Logins.HTTP_REALM + ")"); + } + + private void createDeletedLoginsTable(SQLiteDatabase db, final String tableName) { + debug("Creating deleted_logins.db: " + db.getPath()); + debug("Creating " + tableName + " table"); + + // Table for each deleted login. + db.execSQL("CREATE TABLE " + tableName + "(" + + BrowserContract.DeletedLogins._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + BrowserContract.DeletedLogins.GUID + " TEXT UNIQUE NOT NULL," + + BrowserContract.DeletedLogins.TIME_DELETED + " INTEGER NOT NULL" + + ");"); + } + + private void createDisabledHostsTable(SQLiteDatabase db, final String tableName) { + debug("Creating disabled_hosts.db: " + db.getPath()); + debug("Creating " + tableName + " table"); + + // Table for each disabled host. + db.execSQL("CREATE TABLE " + tableName + "(" + + BrowserContract.LoginsDisabledHosts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + BrowserContract.LoginsDisabledHosts.HOSTNAME + " TEXT UNIQUE NOT NULL ON CONFLICT REPLACE" + + ");"); + } + + @Override + public void onCreate(SQLiteDatabase db) { + debug("Creating browser.db: " + db.getPath()); + + for (Table table : BrowserProvider.sTables) { + table.onCreate(db); + } + + createBookmarksTable(db); + createHistoryTable(db); + createFaviconsTable(db); + createThumbnailsTable(db); + createClientsTable(db); + createLocalClient(db); + createTabsTable(db, TABLE_TABS); + createTabsTableIndices(db, TABLE_TABS); + + + createBookmarksWithFaviconsView(db); + createHistoryWithFaviconsView(db); + + createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID, + R.string.bookmarks_folder_places, 0); + + createOrUpdateAllSpecialFolders(db); + createSearchHistoryTable(db); + createUrlAnnotationsTable(db); + createNumbersTable(db); + + createDeletedLoginsTable(db, TABLE_DELETED_LOGINS); + createDisabledHostsTable(db, TABLE_DISABLED_HOSTS); + createLoginsTable(db, TABLE_LOGINS); + createLoginsTableIndices(db, TABLE_LOGINS); + + createBookmarksWithAnnotationsView(db); + + createVisitsTable(db); + createCombinedViewOn34(db); + + createActivityStreamBlocklistTable(db); + + createPageMetadataTable(db); + } + + /** + * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB. + * + * @param tabsDBFile Path to existing tabs.db. + * @param destinationDB The destination database. + */ + public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) { + createClientsTable(destinationDB); + createTabsTable(destinationDB, TABLE_TABS); + createTabsTableIndices(destinationDB, TABLE_TABS); + + SQLiteDatabase oldTabsDB = null; + try { + oldTabsDB = SQLiteDatabase.openDatabase(tabsDBFile.getPath(), null, SQLiteDatabase.OPEN_READONLY); + + if (!DBUtils.copyTable(oldTabsDB, TABLE_CLIENTS, destinationDB, TABLE_CLIENTS)) { + Log.e(LOGTAG, "Failed to migrate table clients; ignoring."); + } + if (!DBUtils.copyTable(oldTabsDB, TABLE_TABS, destinationDB, TABLE_TABS)) { + Log.e(LOGTAG, "Failed to migrate table tabs; ignoring."); + } + } catch (Exception e) { + Log.e(LOGTAG, "Exception occurred while trying to copy from " + tabsDBFile.getPath() + + " to " + destinationDB.getPath() + "; ignoring.", e); + } finally { + if (oldTabsDB != null) { + oldTabsDB.close(); + } + } + } + + /** + * We used to have a separate history extensions database which was used by Sync to store arrays + * of visits for individual History GUIDs. It was only used by Sync. + * This function migrates contents of that database over to the Visits table. + * + * Warning to callers: this method might throw IllegalStateException if we fail to allocate a + * cursor to read HistoryExtensionsDB data for whatever reason. See Bug 1280409. + * + * @param historyExtensionDb Source History Extensions database + * @param db Destination database + */ + private void copyHistoryExtensionDataToVisitsTable(final SQLiteDatabase historyExtensionDb, final SQLiteDatabase db) { + final String historyExtensionTable = "HistoryExtension"; + final String columnGuid = "guid"; + final String columnVisits = "visits"; + + final Cursor historyExtensionCursor = historyExtensionDb.query(historyExtensionTable, + new String[] {columnGuid, columnVisits}, + null, null, null, null, null); + // Ignore null or empty cursor, we can't (or have nothing to) copy at this point. + if (historyExtensionCursor == null) { + return; + } + try { + if (!historyExtensionCursor.moveToFirst()) { + return; + } + + final int guidCol = historyExtensionCursor.getColumnIndexOrThrow(columnGuid); + + // Use prepared (aka "compiled") SQL statements because they are much faster when we're inserting + // lots of data. We avoid GC churn and recompilation of SQL statements on every insert. + // NB #1: OR IGNORE clause applies to UNIQUE, NOT NULL, CHECK, and PRIMARY KEY constraints. + // It does not apply to Foreign Key constraints, but in our case, at this point in time, foreign key + // constraints are disabled anyway. + // We care about OR IGNORE because we want to ensure that in case of (GUID,DATE) + // clash (the UNIQUE constraint), we will not fail the transaction, and just skip conflicting row. + // Clash might occur if visits array we got from Sync has duplicate (guid,date) records. + // NB #2: IS_LOCAL is always 0, since we consider all visits coming from Sync to be remote. + final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + " (" + + Visits.DATE_VISITED + "," + + Visits.VISIT_TYPE + "," + + Visits.HISTORY_GUID + "," + + Visits.IS_LOCAL + ") VALUES (?, ?, ?, " + Visits.VISIT_IS_REMOTE + ")"; + final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement); + + do { + final String guid = historyExtensionCursor.getString(guidCol); + + // Sanity check, let's not risk a bad incoming GUID. + if (guid == null || guid.isEmpty()) { + continue; + } + + // First, check if history with given GUID exists in the History table. + // We might have a lot of entries in the HistoryExtensionDatabase whose GUID doesn't + // match one in the History table. Let's avoid doing unnecessary work by first checking if + // GUID exists locally. + // Note that we don't have foreign key constraints enabled at this point. + // See Bug 1266232 for details. + if (!isGUIDPresentInHistoryTable(db, guid)) { + continue; + } + + final JSONArray visitsInHistoryExtensionDB = RepoUtils.getJSONArrayFromCursor(historyExtensionCursor, columnVisits); + + if (visitsInHistoryExtensionDB == null) { + continue; + } + + final int histExtVisitCount = visitsInHistoryExtensionDB.size(); + + debug("Inserting " + histExtVisitCount + " visits from history extension db for GUID: " + guid); + for (int i = 0; i < histExtVisitCount; i++) { + final JSONObject visit = (JSONObject) visitsInHistoryExtensionDB.get(i); + + // Sanity check. + if (visit == null) { + continue; + } + + // Let's not rely on underlying data being correct, and guard against casting failures. + // Since we can't recover from this (other than ignoring this visit), let's not fail user's migration. + final Long date; + final Long visitType; + try { + date = (Long) visit.get("date"); + visitType = (Long) visit.get("type"); + } catch (ClassCastException e) { + continue; + } + // Sanity check our incoming data. + if (date == null || visitType == null) { + continue; + } + + // Bind parameters use a 1-based index. + compiledInsertStatement.clearBindings(); + compiledInsertStatement.bindLong(1, date); + compiledInsertStatement.bindLong(2, visitType); + compiledInsertStatement.bindString(3, guid); + compiledInsertStatement.executeInsert(); + } + } while (historyExtensionCursor.moveToNext()); + } finally { + // We return on a null cursor, so don't have to check it here. + historyExtensionCursor.close(); + } + } + + private boolean isGUIDPresentInHistoryTable(final SQLiteDatabase db, String guid) { + final Cursor historyCursor = db.query( + History.TABLE_NAME, + new String[] {History.GUID}, History.GUID + " = ?", new String[] {guid}, + null, null, null); + if (historyCursor == null) { + return false; + } + try { + // No history record found for given GUID + if (!historyCursor.moveToFirst()) { + return false; + } + } finally { + historyCursor.close(); + } + + return true; + } + + private void createSearchHistoryTable(SQLiteDatabase db) { + debug("Creating " + SearchHistory.TABLE_NAME + " table"); + + db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" + + SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " + + SearchHistory.DATE_LAST_VISITED + " INTEGER, " + + SearchHistory.VISITS + " INTEGER ) "); + + db.execSQL("CREATE INDEX idx_search_history_last_visited ON " + + SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")"); + } + + private void createActivityStreamBlocklistTable(final SQLiteDatabase db) { + debug("Creating " + ActivityStreamBlocklist.TABLE_NAME + " table"); + + db.execSQL("CREATE TABLE " + ActivityStreamBlocklist.TABLE_NAME + "(" + + ActivityStreamBlocklist._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ActivityStreamBlocklist.URL + " TEXT UNIQUE NOT NULL, " + + ActivityStreamBlocklist.CREATED + " INTEGER NOT NULL)"); + } + + private void createReadingListTable(final SQLiteDatabase db, final String tableName) { + debug("Creating " + TABLE_READING_LIST + " table"); + + db.execSQL("CREATE TABLE " + tableName + "(" + + ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ReadingListItems.GUID + " TEXT UNIQUE, " + // Server-assigned. + + ReadingListItems.CONTENT_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.STATUS_UNFETCHED + ", " + + ReadingListItems.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_STATUS_NEW + ", " + + ReadingListItems.SYNC_CHANGE_FLAGS + " TINYINT NOT NULL DEFAULT " + ReadingListItems.SYNC_CHANGE_NONE + ", " + + + ReadingListItems.CLIENT_LAST_MODIFIED + " INTEGER NOT NULL, " + // Client time. + ReadingListItems.SERVER_LAST_MODIFIED + " INTEGER, " + // Server-assigned. + + // Server-assigned. + ReadingListItems.SERVER_STORED_ON + " INTEGER, " + + ReadingListItems.ADDED_ON + " INTEGER, " + // Client time. Shouldn't be null, but not enforced. Formerly DATE_CREATED. + ReadingListItems.MARKED_READ_ON + " INTEGER, " + + + // These boolean flags represent the server 'status', 'unread', 'is_article', and 'favorite' fields. + ReadingListItems.IS_DELETED + " TINYINT NOT NULL DEFAULT 0, " + + ReadingListItems.IS_ARCHIVED + " TINYINT NOT NULL DEFAULT 0, " + + ReadingListItems.IS_UNREAD + " TINYINT NOT NULL DEFAULT 1, " + + ReadingListItems.IS_ARTICLE + " TINYINT NOT NULL DEFAULT 0, " + + ReadingListItems.IS_FAVORITE + " TINYINT NOT NULL DEFAULT 0, " + + + ReadingListItems.URL + " TEXT NOT NULL, " + + ReadingListItems.TITLE + " TEXT, " + + ReadingListItems.RESOLVED_URL + " TEXT, " + + ReadingListItems.RESOLVED_TITLE + " TEXT, " + + + ReadingListItems.EXCERPT + " TEXT, " + + + ReadingListItems.ADDED_BY + " TEXT, " + + ReadingListItems.MARKED_READ_BY + " TEXT, " + + + ReadingListItems.WORD_COUNT + " INTEGER DEFAULT 0, " + + ReadingListItems.READ_POSITION + " INTEGER DEFAULT 0 " + + "); "); + + didCreateCurrentReadingListTable = true; // Mostly correct, in the absence of transactions. + } + + private void createReadingListIndices(final SQLiteDatabase db, final String tableName) { + // No need to create an index on GUID; it's a UNIQUE column. + db.execSQL("CREATE INDEX reading_list_url ON " + tableName + "(" + + ReadingListItems.URL + ")"); + db.execSQL("CREATE INDEX reading_list_content_status ON " + tableName + "(" + + ReadingListItems.CONTENT_STATUS + ")"); + } + + private void createUrlAnnotationsTable(final SQLiteDatabase db) { + debug("Creating " + UrlAnnotations.TABLE_NAME + " table"); + + db.execSQL("CREATE TABLE " + UrlAnnotations.TABLE_NAME + "(" + + UrlAnnotations._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + UrlAnnotations.URL + " TEXT NOT NULL, " + + UrlAnnotations.KEY + " TEXT NOT NULL, " + + UrlAnnotations.VALUE + " TEXT, " + + UrlAnnotations.DATE_CREATED + " INTEGER NOT NULL, " + + UrlAnnotations.DATE_MODIFIED + " INTEGER NOT NULL, " + + UrlAnnotations.SYNC_STATUS + " TINYINT NOT NULL DEFAULT " + UrlAnnotations.SyncStatus.NEW.getDBValue() + + " );"); + + db.execSQL("CREATE INDEX idx_url_annotations_url_key ON " + + UrlAnnotations.TABLE_NAME + "(" + UrlAnnotations.URL + ", " + UrlAnnotations.KEY + ")"); + } + + 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.PINNED_FOLDER_GUID, + R.string.bookmarks_folder_pinned, 5); + } + + 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.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 void createNumbersTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + " INTEGER PRIMARY KEY AUTOINCREMENT)"); + + if (db.getVersion() >= 3007011) { // SQLite 3.7.11 + // This is only available in SQLite >= 3.7.11, see release notes: + // "Enhance the INSERT syntax to allow multiple rows to be inserted via the VALUES clause" + final String numbers = "(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)," + + "(10),(11),(12),(13),(14),(15),(16),(17),(18),(19)," + + "(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)," + + "(30),(31),(32),(33),(34),(35),(36),(37),(38),(39)," + + "(40),(41),(42),(43),(44),(45),(46),(47),(48),(49)," + + "(50)"; + + db.execSQL("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES " + numbers); + } else { + final SQLiteStatement statement = db.compileStatement("INSERT INTO " + Numbers.TABLE_NAME + " (" + Numbers.POSITION + ") VALUES (?)"); + + for (int i = 0; i <= Numbers.MAX_VALUE; i++) { + statement.bindLong(1, i); + statement.executeInsert(); + } + } + } + + 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 INDEX IF EXISTS bookmarks_url_index"); + db.execSQL("DROP INDEX IF EXISTS bookmarks_type_deleted_index"); + db.execSQL("DROP INDEX IF EXISTS bookmarks_guid_index"); + db.execSQL("DROP INDEX IF EXISTS bookmarks_modified_index"); + + createBookmarksTable(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); + } + + /** + * Migrate a history table from some old version to the newest one by creating the new table and + * copying all the data over. + */ + 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 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); + + 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 upgradeDatabaseFrom3to4(SQLiteDatabase db) { + migrateBookmarksTable(db, new BookmarkMigrator3to4()); + } + + private void upgradeDatabaseFrom6to7(SQLiteDatabase db) { + debug("Removing history visits with NULL GUIDs"); + db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.GUID + " IS NULL"); + + migrateBookmarksTable(db); + migrateHistoryTable(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 upgradeDatabaseFrom10to11(SQLiteDatabase db) { + db.execSQL("CREATE INDEX bookmarks_type_deleted_index ON " + TABLE_BOOKMARKS + "(" + + Bookmarks.TYPE + ", " + Bookmarks.IS_DELETED + ")"); + } + + private void upgradeDatabaseFrom12to13(SQLiteDatabase db) { + createFaviconsTable(db); + + // Add favicon_id column to the history/bookmarks tables. We wrap this in a try-catch + // because the column *may* already exist at this point (depending on how many upgrade + // steps have been performed in this operation). In which case these queries will throw, + // but we don't care. + try { + db.execSQL("ALTER TABLE " + TABLE_HISTORY + + " ADD COLUMN " + History.FAVICON_ID + " INTEGER"); + db.execSQL("ALTER TABLE " + TABLE_BOOKMARKS + + " ADD COLUMN " + Bookmarks.FAVICON_ID + " INTEGER"); + } catch (SQLException e) { + // Don't care. + debug("Exception adding favicon_id column. We're probably fine." + e); + } + + createThumbnailsTable(db); + + db.execSQL("DROP VIEW IF EXISTS bookmarks_with_images"); + db.execSQL("DROP VIEW IF EXISTS history_with_images"); + db.execSQL("DROP VIEW IF EXISTS combined_with_images"); + + createBookmarksWithFaviconsView(db); + createHistoryWithFaviconsView(db); + + db.execSQL("DROP TABLE IF EXISTS 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) { + // No harm in creating the v19 combined view here: means we don't need two almost-identical + // functions to define both the v16 and v19 ones. The upgrade path will redundantly drop + // and recreate the view again. *shrug* + createV19CombinedView(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); + } + } + + /* + * Moves reading list items from 'bookmarks' table to 'reading_list' table. + */ + private void upgradeDatabaseFrom17to18(SQLiteDatabase db) { + debug("Moving reading list items from 'bookmarks' table to 'reading_list' table"); + + final String selection = Bookmarks.PARENT + " = ? AND " + Bookmarks.IS_DELETED + " = ? "; + final String[] selectionArgs = { String.valueOf(Bookmarks.FIXED_READING_LIST_ID), "0" }; + final String[] projection = { Bookmarks._ID, + Bookmarks.GUID, + Bookmarks.URL, + Bookmarks.DATE_MODIFIED, + Bookmarks.DATE_CREATED, + Bookmarks.TITLE }; + + try { + db.beginTransaction(); + + // Create 'reading_list' table. + createReadingListTable(db, TABLE_READING_LIST); + + // Get all the reading list items from bookmarks table. + final Cursor cursor = db.query(TABLE_BOOKMARKS, projection, selection, selectionArgs, null, null, null); + + if (cursor == null) { + // This should never happen. + db.setTransactionSuccessful(); + return; + } + + try { + // Insert reading list items into reading_list table. + while (cursor.moveToNext()) { + debug(DatabaseUtils.dumpCurrentRowToString(cursor)); + final ContentValues values = new ContentValues(); + + // We don't preserve bookmark GUIDs. + DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.URL, values, ReadingListItems.URL); + DatabaseUtils.cursorStringToContentValues(cursor, Bookmarks.TITLE, values, ReadingListItems.TITLE); + DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_CREATED, values, ReadingListItems.ADDED_ON); + DatabaseUtils.cursorLongToContentValues(cursor, Bookmarks.DATE_MODIFIED, values, ReadingListItems.CLIENT_LAST_MODIFIED); + + db.insertOrThrow(TABLE_READING_LIST, null, values); + } + } finally { + cursor.close(); + } + + // Delete reading list items from bookmarks table. + db.delete(TABLE_BOOKMARKS, + Bookmarks.PARENT + " = ? ", + new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); + + // Delete reading list special folder. + db.delete(TABLE_BOOKMARKS, + Bookmarks._ID + " = ? ", + new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); + + // Create indices. + createReadingListIndices(db, TABLE_READING_LIST); + + // Done. + db.setTransactionSuccessful(); + } catch (SQLException e) { + Log.e(LOGTAG, "Error migrating reading list items", e); + } finally { + db.endTransaction(); + } + } + + private void upgradeDatabaseFrom18to19(SQLiteDatabase db) { + // Redefine the "combined" view... + createV19CombinedView(db); + + // Kill any history entries with NULL URL. This ostensibly can't happen... + db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.URL + " IS NULL"); + + // Similar for bookmark types. Replaces logic from the combined view, also shouldn't happen. + db.execSQL("UPDATE " + TABLE_BOOKMARKS + " SET " + + Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK + + " WHERE " + Bookmarks.TYPE + " IS NULL"); + } + + private void upgradeDatabaseFrom19to20(SQLiteDatabase db) { + createSearchHistoryTable(db); + } + + private void upgradeDatabaseFrom21to22(SQLiteDatabase db) { + if (didCreateCurrentReadingListTable) { + debug("No need to add CONTENT_STATUS to reading list; we just created with the current schema."); + return; + } + + debug("Adding CONTENT_STATUS column to reading list table."); + + try { + db.execSQL("ALTER TABLE " + TABLE_READING_LIST + + " ADD COLUMN " + ReadingListItems.CONTENT_STATUS + + " TINYINT DEFAULT " + ReadingListItems.STATUS_UNFETCHED); + + db.execSQL("CREATE INDEX reading_list_content_status ON " + TABLE_READING_LIST + "(" + + ReadingListItems.CONTENT_STATUS + ")"); + } catch (SQLiteException e) { + // We're betting that an error here means that the table already has the column, + // so we're failing due to the duplicate column name. + Log.e(LOGTAG, "Error upgrading database from 21 to 22", e); + } + } + + private void upgradeDatabaseFrom22to23(SQLiteDatabase db) { + if (didCreateCurrentReadingListTable) { + // If we just created this table it is already in the expected >= 23 schema. Trying + // to run this migration will crash because columns that were in the <= 22 schema + // no longer exist. + debug("No need to rev reading list schema; we just created with the current schema."); + return; + } + + debug("Rewriting reading list table."); + createReadingListTable(db, "tmp_rl"); + + // Remove indexes. We don't need them now, and we'll be throwing away the table. + db.execSQL("DROP INDEX IF EXISTS reading_list_url"); + db.execSQL("DROP INDEX IF EXISTS reading_list_guid"); + db.execSQL("DROP INDEX IF EXISTS reading_list_content_status"); + + // This used to be a part of the no longer existing ReadingListProvider, since we're deleting + // this table later in the second migration, and since sync for this table never existed, + // we don't care about the device name here. + final String thisDevice = "_fake_device_name_that_will_be_discarded_in_the_next_migration_"; + db.execSQL("INSERT INTO tmp_rl (" + + // Here are the columns we can preserve. + ReadingListItems._ID + ", " + + ReadingListItems.URL + ", " + + ReadingListItems.TITLE + ", " + + ReadingListItems.RESOLVED_TITLE + ", " + // = TITLE (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE) + ReadingListItems.RESOLVED_URL + ", " + // = URL (if CONTENT_STATUS = STATUS_FETCHED_ARTICLE) + ReadingListItems.EXCERPT + ", " + + ReadingListItems.IS_UNREAD + ", " + // = !READ + ReadingListItems.IS_DELETED + ", " + // = 0 + ReadingListItems.GUID + ", " + // = NULL + ReadingListItems.CLIENT_LAST_MODIFIED + ", " + // = DATE_MODIFIED + ReadingListItems.ADDED_ON + ", " + // = DATE_CREATED + ReadingListItems.CONTENT_STATUS + ", " + + ReadingListItems.MARKED_READ_BY + ", " + // if READ + ", = this device + ReadingListItems.ADDED_BY + // = this device + ") " + + "SELECT " + + "_id, url, title, " + + "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN title ELSE NULL END, " + // RESOLVED_TITLE. + "CASE content_status WHEN " + ReadingListItems.STATUS_FETCHED_ARTICLE + " THEN url ELSE NULL END, " + // RESOLVED_URL. + "excerpt, " + + "CASE read WHEN 1 THEN 0 ELSE 1 END, " + // IS_UNREAD. + "0, " + // IS_DELETED. + "NULL, modified, created, content_status, " + + "CASE read WHEN 1 THEN ? ELSE NULL END, " + // MARKED_READ_BY. + "?" + // ADDED_BY. + " FROM " + TABLE_READING_LIST + + " WHERE deleted = 0", + new String[] {thisDevice, thisDevice}); + + // Now switch these tables over and recreate the indices. + db.execSQL("DROP TABLE " + TABLE_READING_LIST); + db.execSQL("ALTER TABLE tmp_rl RENAME TO " + TABLE_READING_LIST); + + createReadingListIndices(db, TABLE_READING_LIST); + } + + private void upgradeDatabaseFrom23to24(SQLiteDatabase db) { + // Version 24 consolidates the tabs and clients table into browser.db. Before, they lived in tabs.db. + // It's easier to copy the existing data than to arrange for Sync to re-populate it. + try { + final File oldTabsDBFile = new File(GeckoProfile.get(mContext).getDir(), "tabs.db"); + copyTabsDB(oldTabsDBFile, db); + } catch (Exception e) { + Log.e(LOGTAG, "Got exception copying tabs and clients data from tabs.db to browser.db; ignoring.", e); + } + + // Delete the database, the shared memory, and the log. + for (String filename : new String[] { "tabs.db", "tabs.db-shm", "tabs.db-wal" }) { + final File file = new File(GeckoProfile.get(mContext).getDir(), filename); + try { + FileUtils.delete(file); + } catch (Exception e) { + Log.e(LOGTAG, "Exception occurred while trying to delete " + file.getPath() + "; ignoring.", e); + } + } + } + + private void upgradeDatabaseFrom24to25(SQLiteDatabase db) { + if (didCreateTabsTable) { + // This migration adds a foreign key constraint (the table scheme stays identical, except + // for the new constraint) - hence it is safe to run this migration on a newly created tabs + // table - but it's unnecessary hence we should avoid doing so. + debug("No need to rev tabs schema; foreign key constraint exists."); + return; + } + + debug("Rewriting tabs table."); + createTabsTable(db, "tmp_tabs"); + + // Remove indexes. We don't need them now, and we'll be throwing away the table. + db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_GUID); + db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_POSITION); + + db.execSQL("INSERT INTO tmp_tabs (" + + // Here are the columns we can preserve. + BrowserContract.Tabs._ID + ", " + + BrowserContract.Tabs.CLIENT_GUID + ", " + + BrowserContract.Tabs.TITLE + ", " + + BrowserContract.Tabs.URL + ", " + + BrowserContract.Tabs.HISTORY + ", " + + BrowserContract.Tabs.FAVICON + ", " + + BrowserContract.Tabs.LAST_USED + ", " + + BrowserContract.Tabs.POSITION + + ") " + + "SELECT " + + "_id, client_guid, title, url, history, favicon, last_used, position" + + " FROM " + TABLE_TABS); + + // Now switch these tables over and recreate the indices. + db.execSQL("DROP TABLE " + TABLE_TABS); + db.execSQL("ALTER TABLE tmp_tabs RENAME TO " + TABLE_TABS); + createTabsTableIndices(db, TABLE_TABS); + didCreateTabsTable = true; + } + + private void upgradeDatabaseFrom25to26(SQLiteDatabase db) { + debug("Dropping unnecessary indices"); + db.execSQL("DROP INDEX IF EXISTS clients_guid_index"); + db.execSQL("DROP INDEX IF EXISTS thumbnails_url_index"); + db.execSQL("DROP INDEX IF EXISTS favicons_url_index"); + } + + private void upgradeDatabaseFrom27to28(final SQLiteDatabase db) { + debug("Adding url annotations table"); + createUrlAnnotationsTable(db); + } + + private void upgradeDatabaseFrom28to29(SQLiteDatabase db) { + debug("Adding numbers table"); + createNumbersTable(db); + } + + private void upgradeDatabaseFrom29to30(final SQLiteDatabase db) { + debug("creating logins table"); + createDeletedLoginsTable(db, TABLE_DELETED_LOGINS); + createDisabledHostsTable(db, TABLE_DISABLED_HOSTS); + createLoginsTable(db, TABLE_LOGINS); + createLoginsTableIndices(db, TABLE_LOGINS); + } + + // Get the cache path for a URL, based on the storage format in place during the 27to28 transition. + // This is a reimplementation of _toHashedPath from ReaderMode.jsm - given that we're likely + // to migrate the SavedReaderViewHelper implementation at some point, it seems safest to have a local + // implementation here - moreover this is probably faster than calling into JS. + // This is public only to allow for testing. + @RobocopTarget + public static String getReaderCacheFileNameForURL(String url) { + try { + // On KitKat and above we can use java.nio.charset.StandardCharsets.UTF_8 in place of "UTF8" + // which avoids having to handle UnsupportedCodingException + byte[] utf8 = url.getBytes("UTF8"); + + final MessageDigest digester = MessageDigest.getInstance("MD5"); + byte[] hash = digester.digest(utf8); + + final String hashString = new Base32().encodeAsString(hash); + return hashString.substring(0, hashString.indexOf('=')) + ".json"; + } catch (UnsupportedEncodingException e) { + // This should never happen + throw new IllegalStateException("UTF8 encoding not available - can't process readercache filename"); + } catch (NoSuchAlgorithmException e) { + // This should also never happen + throw new IllegalStateException("MD5 digester unavailable - can't process readercache filename"); + } + } + + /* + * Moves reading list items from the 'reading_list' table back into the 'bookmarks' table. This time the + * reading list items are placed into a "Reading List" folder, which is a subfolder of the mobile-bookmarks table. + */ + private void upgradeDatabaseFrom30to31(SQLiteDatabase db) { + // We only need to do the migration if reading-list items already exist. We could do a query of count(*) on + // TABLE_READING_LIST, however if we are doing the migration, we'll need to query all items in the reading-list, + // hence we might as well just query all items, and proceed with the migration if cursor.count > 0. + + // We try to retain the original ordering below. Our LocalReadingListAccessor actually coalesced + // SERVER_STORED_ON with ADDED_ON to determine positioning, however reading list syncing was never + // implemented hence SERVER_STORED will have always been null. + final Cursor readingListCursor = db.query(TABLE_READING_LIST, + new String[] { + ReadingListItems.URL, + ReadingListItems.TITLE, + ReadingListItems.ADDED_ON, + ReadingListItems.CLIENT_LAST_MODIFIED + }, + ReadingListItems.IS_DELETED + " = 0", + null, + null, + null, + ReadingListItems.ADDED_ON + " DESC"); + + // We'll want to walk the cache directory, so that we can (A) bookkeep readercache items + // that we want and (B) delete unneeded readercache items. (B) shouldn't actually happen, but + // is possible if there were bugs in our reader-caching code. + // We need to construct this here since we populate this map while walking the DB cursor, + // and use the map later when walking the cache. + final Map<String, String> fileToURLMap = new HashMap<>(); + + + try { + if (!readingListCursor.moveToFirst()) { + return; + } + + final Integer mobileBookmarksID = getMobileFolderId(db); + + if (mobileBookmarksID == null) { + // This folder is created either on DB creation or during the 3-4 or 6-7 migrations. + throw new IllegalStateException("mobile bookmarks folder must already exist"); + } + + final long now = System.currentTimeMillis(); + + // We try to retain the same order as the reading-list would show. We should hopefully be reading the + // items in the order they are displayed on screen (final param of db.query above), by providing + // a position we should obtain the same ordering in the bookmark folder. + long position = 0; + + final int titleColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.TITLE); + final int createdColumnID = readingListCursor.getColumnIndexOrThrow(ReadingListItems.ADDED_ON); + + // This isn't the most efficient implementation, but the migration is one-off, and this + // also more maintainable than the SQL equivalent (generating the guids correctly is + // difficult in SQLite). + do { + final ContentValues readingListItemValues = new ContentValues(); + + final String url = readingListCursor.getString(readingListCursor.getColumnIndexOrThrow(ReadingListItems.URL)); + + readingListItemValues.put(Bookmarks.PARENT, mobileBookmarksID); + readingListItemValues.put(Bookmarks.GUID, Utils.generateGuid()); + readingListItemValues.put(Bookmarks.URL, url); + // Title may be null, however we're expecting a String - we can generate an empty string if needed: + if (!readingListCursor.isNull(titleColumnID)) { + readingListItemValues.put(Bookmarks.TITLE, readingListCursor.getString(titleColumnID)); + } else { + readingListItemValues.put(Bookmarks.TITLE, ""); + } + readingListItemValues.put(Bookmarks.DATE_CREATED, readingListCursor.getLong(createdColumnID)); + readingListItemValues.put(Bookmarks.DATE_MODIFIED, now); + readingListItemValues.put(Bookmarks.POSITION, position); + + db.insert(TABLE_BOOKMARKS, + null, + readingListItemValues); + + final String cacheFileName = getReaderCacheFileNameForURL(url); + fileToURLMap.put(cacheFileName, url); + + position++; + } while (readingListCursor.moveToNext()); + + } finally { + readingListCursor.close(); + // We need to do this work here since we might be returning (we return early if the + // reading-list table is empty). + db.execSQL("DROP TABLE IF EXISTS " + TABLE_READING_LIST); + createBookmarksWithAnnotationsView(db); + } + + final File profileDir = GeckoProfile.get(mContext).getDir(); + final File cacheDir = new File(profileDir, "readercache"); + + // At the time of this migration the SavedReaderViewHelper becomes a 1:1 mirror of reader view + // url-annotations. This may change in future implementations, however currently we only need to care + // about standard bookmarks (untouched during this migration) and bookmarks with a reader + // view annotation (which we're creating here, and which are guaranteed to be saved offline). + // + // This is why we have to migrate the cache items (instead of cleaning the cache + // and rebuilding it). We simply don't support uncached reader view bookmarks, and we would + // break existing reading list items (they would convert into plain bookmarks without + // reader view). This helps ensure that offline content isn't lost during the migration. + if (cacheDir.exists() && cacheDir.isDirectory()) { + SavedReaderViewHelper savedReaderViewHelper = SavedReaderViewHelper.getSavedReaderViewHelper(mContext); + + // Usually we initialise the helper during onOpen(). However onUpgrade() is run before + // onOpen() hence we need to manually initialise it at this stage. + savedReaderViewHelper.loadItems(); + + for (File cacheFile : cacheDir.listFiles()) { + if (fileToURLMap.containsKey(cacheFile.getName())) { + final String url = fileToURLMap.get(cacheFile.getName()); + final String path = cacheFile.getAbsolutePath(); + long size = cacheFile.length(); + + savedReaderViewHelper.put(url, path, size); + } else { + // This should never happen, but we don't actually know whether or not orphaned + // items happened in the wild. + boolean deleted = cacheFile.delete(); + + if (!deleted) { + Log.w(LOGTAG, "Failed to delete orphaned saved reader view file."); + } + } + } + } + } + + private void upgradeDatabaseFrom31to32(final SQLiteDatabase db) { + debug("Adding visits table"); + createVisitsTable(db); + + debug("Migrating visits from history extension db into visits table"); + String historyExtensionDbName = "history_extension_database"; + + SQLiteDatabase historyExtensionDb = null; + final File historyExtensionsDatabase = mContext.getDatabasePath(historyExtensionDbName); + + // Primary goal of this migration is to improve Top Sites experience by distinguishing between + // local and remote visits. If Sync is enabled, we rely on visit data from Sync and treat it as remote. + // However, if Sync is disabled but we detect evidence that it was enabled at some point (HistoryExtensionsDB is present) + // then we synthesize visits from the History table, but we mark them all as "remote". This will ensure + // that once user starts browsing around, their Top Sites will reflect their local browsing history. + // Otherwise, we risk overwhelming their Top Sites with remote history, just as we did before this migration. + try { + // If FxAccount exists (Sync is enabled) then port data over to the Visits table. + if (FirefoxAccounts.firefoxAccountsExist(mContext)) { + try { + historyExtensionDb = SQLiteDatabase.openDatabase(historyExtensionsDatabase.getPath(), null, + SQLiteDatabase.OPEN_READONLY); + + if (historyExtensionDb != null) { + copyHistoryExtensionDataToVisitsTable(historyExtensionDb, db); + } + + // If we fail to open HistoryExtensionDatabase, then synthesize visits marking them as remote + } catch (SQLiteException e) { + Log.w(LOGTAG, "Couldn't open history extension database; synthesizing visits instead", e); + synthesizeAndInsertVisits(db, false); + + // It's possible that we might fail to copy over visit data from the HistoryExtensionsDB, + // so let's synthesize visits marking them as remote. See Bug 1280409. + } catch (IllegalStateException e) { + Log.w(LOGTAG, "Couldn't copy over history extension data; synthesizing visits instead", e); + synthesizeAndInsertVisits(db, false); + } + + // FxAccount doesn't exist, but there's evidence Sync was enabled at some point. + // Synthesize visits from History table marking them all as remote. + } else if (historyExtensionsDatabase.exists()) { + synthesizeAndInsertVisits(db, false); + + // FxAccount doesn't exist and there's no evidence sync was ever enabled. + // Synthesize visits from History table marking them all as local. + } else { + synthesizeAndInsertVisits(db, true); + } + } finally { + if (historyExtensionDb != null) { + historyExtensionDb.close(); + } + } + + // Delete history extensions database if it's present. + if (historyExtensionsDatabase.exists()) { + if (!mContext.deleteDatabase(historyExtensionDbName)) { + Log.e(LOGTAG, "Couldn't remove history extension database"); + } + } + } + + private void synthesizeAndInsertVisits(final SQLiteDatabase db, boolean markAsLocal) { + final Cursor cursor = db.query( + History.TABLE_NAME, + new String[] {History.GUID, History.VISITS, History.DATE_LAST_VISITED}, + null, null, null, null, null); + if (cursor == null) { + Log.e(LOGTAG, "Null cursor while selecting all history records"); + return; + } + + try { + if (!cursor.moveToFirst()) { + Log.e(LOGTAG, "No history records to synthesize visits for."); + return; + } + + int guidCol = cursor.getColumnIndexOrThrow(History.GUID); + int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS); + int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED); + + // Re-use compiled SQL statements for faster inserts. + // Visit Type is going to be 1, which is the column's default value. + final String insertSqlStatement = "INSERT OR IGNORE INTO " + Visits.TABLE_NAME + "(" + + Visits.DATE_VISITED + "," + + Visits.HISTORY_GUID + "," + + Visits.IS_LOCAL + + ") VALUES (?, ?, ?)"; + final SQLiteStatement compiledInsertStatement = db.compileStatement(insertSqlStatement); + + // For each history record, insert as many visits as there are recorded in the VISITS column. + do { + final int numberOfVisits = cursor.getInt(visitsCol); + final String guid = cursor.getString(guidCol); + final long lastVisitedDate = cursor.getLong(dateCol); + + // Sanity check. + if (guid == null) { + continue; + } + + // In a strange case that lastVisitedDate is a very low number, let's not introduce + // negative timestamps into our data. + if (lastVisitedDate - numberOfVisits < 0) { + continue; + } + + for (int i = 0; i < numberOfVisits; i++) { + final long offsetVisitedDate = lastVisitedDate - i; + compiledInsertStatement.clearBindings(); + compiledInsertStatement.bindLong(1, offsetVisitedDate); + compiledInsertStatement.bindString(2, guid); + // Very old school, 1 is true and 0 is false :) + if (markAsLocal) { + compiledInsertStatement.bindLong(3, Visits.VISIT_IS_LOCAL); + } else { + compiledInsertStatement.bindLong(3, Visits.VISIT_IS_REMOTE); + } + compiledInsertStatement.executeInsert(); + } + } while (cursor.moveToNext()); + } catch (Exception e) { + Log.e(LOGTAG, "Error while synthesizing visits for history record", e); + } finally { + cursor.close(); + } + } + + private void updateHistoryTableAddVisitAggregates(final SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE_HISTORY + + " ADD COLUMN " + History.LOCAL_VISITS + " INTEGER NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE_HISTORY + + " ADD COLUMN " + History.REMOTE_VISITS + " INTEGER NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE_HISTORY + + " ADD COLUMN " + History.LOCAL_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE_HISTORY + + " ADD COLUMN " + History.REMOTE_DATE_LAST_VISITED + " INTEGER NOT NULL DEFAULT 0"); + } + + private void calculateHistoryTableVisitAggregates(final SQLiteDatabase db) { + // Note that we convert from microseconds (timestamps in the visits table) to milliseconds + // (timestamps in the history table). Sync works in microseconds, so for visits Fennec stores + // timestamps in microseconds as well - but the rest of the timestamps are stored in milliseconds. + db.execSQL("UPDATE " + TABLE_HISTORY + " SET " + + History.LOCAL_VISITS + " = (" + + "SELECT COALESCE(SUM(" + qualifyColumn(TABLE_VISITS, Visits.IS_LOCAL) + "), 0)" + + " FROM " + TABLE_VISITS + + " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + + "), " + + History.REMOTE_VISITS + " = (" + + "SELECT COALESCE(SUM(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN 1 ELSE 0 END), 0)" + + " FROM " + TABLE_VISITS + + " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + + "), " + + History.LOCAL_DATE_LAST_VISITED + " = (" + + "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 1 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" + + " FROM " + TABLE_VISITS + + " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + + "), " + + History.REMOTE_DATE_LAST_VISITED + " = (" + + "SELECT COALESCE(MAX(CASE " + Visits.IS_LOCAL + " WHEN 0 THEN " + Visits.DATE_VISITED + " ELSE 0 END), 0) / 1000" + + " FROM " + TABLE_VISITS + + " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + + ") " + + "WHERE EXISTS " + + "(SELECT " + Visits._ID + + " FROM " + TABLE_VISITS + + " WHERE " + qualifyColumn(TABLE_VISITS, Visits.HISTORY_GUID) + " = " + qualifyColumn(TABLE_HISTORY, History.GUID) + ")" + ); + } + + private void upgradeDatabaseFrom32to33(final SQLiteDatabase db) { + createV33CombinedView(db); + } + + private void upgradeDatabaseFrom33to34(final SQLiteDatabase db) { + updateHistoryTableAddVisitAggregates(db); + calculateHistoryTableVisitAggregates(db); + createV34CombinedView(db); + } + + private void upgradeDatabaseFrom34to35(final SQLiteDatabase db) { + createActivityStreamBlocklistTable(db); + } + + private void upgradeDatabaseFrom35to36(final SQLiteDatabase db) { + createPageMetadataTable(db); + } + + private void createV33CombinedView(final SQLiteDatabase db) { + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); + + createCombinedViewOn33(db); + } + + private void createV34CombinedView(final SQLiteDatabase db) { + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); + + createCombinedViewOn34(db); + } + + private void createV19CombinedView(SQLiteDatabase db) { + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS); + + createCombinedViewOn19(db); + } + + @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 4: + upgradeDatabaseFrom3to4(db); + break; + + case 7: + upgradeDatabaseFrom6to7(db); + break; + + case 8: + upgradeDatabaseFrom7to8(db); + break; + + case 11: + upgradeDatabaseFrom10to11(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; + + case 18: + upgradeDatabaseFrom17to18(db); + break; + + case 19: + upgradeDatabaseFrom18to19(db); + break; + + case 20: + upgradeDatabaseFrom19to20(db); + break; + + case 22: + upgradeDatabaseFrom21to22(db); + break; + + case 23: + upgradeDatabaseFrom22to23(db); + break; + + case 24: + upgradeDatabaseFrom23to24(db); + break; + + case 25: + upgradeDatabaseFrom24to25(db); + break; + + case 26: + upgradeDatabaseFrom25to26(db); + break; + + // case 27 occurs in UrlMetadataTable.onUpgrade + + case 28: + upgradeDatabaseFrom27to28(db); + break; + + case 29: + upgradeDatabaseFrom28to29(db); + break; + + case 30: + upgradeDatabaseFrom29to30(db); + break; + + case 31: + upgradeDatabaseFrom30to31(db); + break; + + case 32: + upgradeDatabaseFrom31to32(db); + break; + + case 33: + upgradeDatabaseFrom32to33(db); + break; + + case 34: + upgradeDatabaseFrom33to34(db); + break; + + case 35: + upgradeDatabaseFrom34to35(db); + break; + + case 36: + upgradeDatabaseFrom35to36(db); + break; + } + } + + for (Table table : BrowserProvider.sTables) { + table.onUpgrade(db, oldVersion, newVersion); + } + + // Delete the obsolete favicon database after all other upgrades complete. + // This can probably equivalently be moved into upgradeDatabaseFrom12to13. + if (oldVersion < 13 && newVersion >= 13) { + if (mContext.getDatabasePath("favicon_urls.db").exists()) { + mContext.deleteDatabase("favicon_urls.db"); + } + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + debug("Opening browser.db: " + db.getPath()); + + // Force explicit readercache loading - we won't access readercache state for bookmarks + // until we actually know what our bookmarks are. Bookmarks are stored in the DB, hence + // it is sufficient to ensure that the readercache is loaded before the DB can be accessed. + // Note, this takes ~4-6ms to load on an N4 (compared to 20-50ms for most DB queries), and + // is only done once, hence this shouldn't have noticeable impact on performance. Moreover + // this is run on a background thread and therefore won't block UI code during startup. + SavedReaderViewHelper.getSavedReaderViewHelper(mContext).loadItems(); + + 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) { + // Modern Android allows WAL to be enabled through a mode flag. + if (Build.VERSION.SDK_INT < 16) { + db.enableWriteAheadLogging(); + + // This does nothing on 16+. + 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(); + } + } + } + + // Calculate these once, at initialization. isLoggable is too expensive to + // have in-line in each log call. + private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); + private static final 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); + } + } + + 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 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"); + } + } +} + |