Bug 704490 - Introduce new local bookmarks/history database (r=blassey)
authorLucas Rocha <lucasr@mozilla.com>
Thu, 08 Dec 2011 16:37:21 +0000
changeset 82272 bb39bae574f80934758a8fbd2f0ad2bd704e9731
parent 82271 8a49a1709b30c1118ef55dcc9b7d381cefd9866e
child 82273 7769ec8647b90d102182d26373f0ea95f71aefaf
push id3940
push userlrocha@mozilla.com
push dateThu, 08 Dec 2011 16:37:54 +0000
treeherdermozilla-inbound@bb39bae574f8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblassey
bugs704490
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 704490 - Introduce new local bookmarks/history database (r=blassey) Local DB is disabled by default for now.
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/Makefile.in
mobile/android/base/db/BrowserContract.java
mobile/android/base/db/BrowserProvider.java
mobile/android/base/db/LocalBrowserDB.java
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -134,10 +134,14 @@
         <activity android:name="org.mozilla.gecko.TabsTray"
                   android:theme="@style/Gecko.Translucent"/>
 
         <activity android:name="org.mozilla.gecko.GeckoPreferences"
                   android:theme="@style/Gecko.TitleBar"
                   android:label="@string/preferences_title"
                   android:excludeFromRecents="true"/>
 
+        <provider android:name="org.mozilla.gecko.db.BrowserProvider"
+                  android:authorities="org.mozilla.gecko.providers.browser"
+                  android:exported="false"/>
+
     </application>
 </manifest> 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -49,18 +49,21 @@ DIST_FILES = package-name.txt
 
 JAVAFILES = \
   AboutHomeContent.java \
   AlertNotification.java \
   AwesomeBar.java \
   AwesomeBarTabs.java \
   BrowserToolbar.java \
   ConfirmPreference.java \
+  db/BrowserContract.java \
   db/AndroidBrowserDB.java \
   db/BrowserDB.java \
+  db/BrowserProvider.java \
+  db/LocalBrowserDB.java \
   DoorHanger.java \
   DoorHangerPopup.java \
   Favicons.java \
   FloatUtils.java \
   GeckoActionBar.java \
   GeckoApp.java \
   GeckoAppShell.java \
   GeckoAsyncTask.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/BrowserContract.java
@@ -0,0 +1,114 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Android code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Lucas Rocha <lucasr@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+package org.mozilla.gecko.db;
+
+import android.net.Uri;
+
+public class BrowserContract {
+    public static final String AUTHORITY = "org.mozilla.gecko.providers.browser";
+
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    public static final String DEFAULT_PROFILE = "default";
+
+    public static final String PARAM_PROFILE = "profile";
+
+    public static final String PARAM_LIMIT = "limit";
+
+    interface SyncColumns {
+        public static final String GUID = "guid";
+
+        public static final String DATE_CREATED = "created";
+
+        public static final String DATE_MODIFIED = "modified";
+    }
+
+    interface CommonColumns {
+        public static final String _ID = "_id";
+
+        public static final String URL = "url";
+
+        public static final String TITLE = "title";
+    }
+
+    interface ImageColumns {
+        public static final String FAVICON = "favicon";
+
+        public static final String THUMBNAIL = "thumbnail";
+    }
+
+    public static final class Images implements ImageColumns, SyncColumns {
+        private Images() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+        public static final String URL = "url_key";
+
+        public static final String FAVICON_URL = "favicon_url";
+    }
+
+    public static final class Bookmarks implements CommonColumns, ImageColumns, SyncColumns {
+        private Bookmarks() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
+
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
+
+        public static final String IS_FOLDER = "folder";
+
+        public static final String PARENT = "parent";
+
+        public static final String POSITION = "position";
+    }
+
+    public static final class History implements CommonColumns, ImageColumns, SyncColumns {
+        private History() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
+
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
+
+        public static final String DATE_LAST_VISITED = "date";
+
+        public static final String VISITS = "visits";
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -0,0 +1,927 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Android code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Lucas Rocha <lucasr@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+package org.mozilla.gecko.db;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.UUID;
+
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.CommonColumns;
+import org.mozilla.gecko.db.BrowserContract.History;
+import org.mozilla.gecko.db.BrowserContract.Images;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class BrowserProvider extends ContentProvider {
+    private static final String LOGTAG = "GeckoBrowserProvider";
+
+    static final String DATABASE_NAME = "browser.db";
+
+    static final String TABLE_BOOKMARKS = "bookmarks";
+    static final String TABLE_HISTORY = "history";
+    static final String TABLE_IMAGES = "images";
+
+    // Bookmark matches
+    static final int BOOKMARKS = 100;
+    static final int BOOKMARKS_ID = 101;
+    static final int BOOKMARKS_FOLDER_ID = 102;
+
+    // History matches
+    static final int HISTORY = 200;
+    static final int HISTORY_ID = 201;
+
+    // Image matches
+    static final int IMAGES = 300;
+
+    static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.IS_FOLDER
+            + " DESC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
+            + " ASC";
+
+    static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
+
+    static final String TABLE_BOOKMARKS_JOIN_IMAGES = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
+            TABLE_IMAGES + " ON " + qualifyColumnValue(TABLE_BOOKMARKS, Bookmarks.URL) +
+            " = " + qualifyColumnValue(TABLE_IMAGES, Images.URL);
+
+    static final String TABLE_HISTORY_JOIN_IMAGES = TABLE_HISTORY + " LEFT OUTER JOIN " +
+            TABLE_IMAGES + " ON " + qualifyColumnValue(TABLE_HISTORY, History.URL) +
+            " = " + qualifyColumnValue(TABLE_IMAGES, Images.URL);
+
+    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>();
+
+    private HashMap<String, DatabaseHelper> mDatabasePerProfile;
+
+    static {
+        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/folder/#", BOOKMARKS_FOLDER_ID);
+
+        map = BOOKMARKS_PROJECTION_MAP;
+        map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID));
+        map.put(Bookmarks.TITLE, Bookmarks.TITLE);
+        map.put(Bookmarks.URL, Bookmarks.URL);
+        map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
+        map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
+        map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
+        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
+        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
+        map.put(Bookmarks.DATE_CREATED, qualifyColumn(TABLE_BOOKMARKS, Bookmarks.DATE_CREATED));
+        map.put(Bookmarks.DATE_MODIFIED, qualifyColumn(TABLE_BOOKMARKS, Bookmarks.DATE_MODIFIED));
+        map.put(Bookmarks.GUID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks.GUID));
+
+        // History
+        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
+        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
+
+        map = HISTORY_PROJECTION_MAP;
+        map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID));
+        map.put(History.TITLE, History.TITLE);
+        map.put(History.URL, History.URL);
+        map.put(History.FAVICON, History.FAVICON);
+        map.put(History.THUMBNAIL, History.THUMBNAIL);
+        map.put(History.VISITS, History.VISITS);
+        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
+        map.put(History.DATE_CREATED, qualifyColumn(TABLE_HISTORY, History.DATE_CREATED));
+        map.put(History.DATE_MODIFIED, qualifyColumn(TABLE_HISTORY, History.DATE_MODIFIED));
+        map.put(History.GUID, qualifyColumn(TABLE_HISTORY, History.GUID));
+
+        // Images
+        URI_MATCHER.addURI(BrowserContract.AUTHORITY, "images", IMAGES);
+
+        map = IMAGES_PROJECTION_MAP;
+        map.put(Images.URL, Images.URL);
+        map.put(Images.FAVICON, Images.FAVICON);
+        map.put(Images.FAVICON_URL, Images.FAVICON_URL);
+        map.put(Images.THUMBNAIL, Images.THUMBNAIL);
+        map.put(Images.DATE_CREATED, qualifyColumn(TABLE_IMAGES, Images.DATE_CREATED));
+        map.put(Images.DATE_MODIFIED, qualifyColumn(TABLE_IMAGES, Images.DATE_MODIFIED));
+        map.put(Images.GUID, qualifyColumn(TABLE_IMAGES, Images.GUID));
+    }
+
+    static final String qualifyColumn(String table, String column) {
+        return table + "." + column + " AS " + column;
+    }
+
+    static final String qualifyColumnValue(String table, String column) {
+        return table + "." + column;
+    }
+
+    // This is available in Android >= 11. Implemented locally to be
+    // compatible with older versions.
+    public static String concatenateWhere(String a, String b) {
+        if (TextUtils.isEmpty(a)) {
+            return b;
+        }
+
+        if (TextUtils.isEmpty(b)) {
+            return a;
+        }
+
+        return "(" + a + ") AND (" + b + ")";
+    }
+
+    // This is available in Android >= 11. Implemented locally to be
+    // compatible with older versions.
+    public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
+        if (originalValues == null || originalValues.length == 0) {
+            return newValues;
+        }
+
+        if (newValues == null || newValues.length == 0) {
+            return originalValues;
+        }
+
+        String[] result = new String[originalValues.length + newValues.length];
+        System.arraycopy(originalValues, 0, result, 0, originalValues.length);
+        System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
+
+        return result;
+    }
+
+    final class DatabaseHelper extends SQLiteOpenHelper {
+        static final int DATABASE_VERSION = 1;
+
+        public DatabaseHelper(Context context, String databasePath) {
+            super(context, databasePath, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            Log.d(LOGTAG, "Creating browser.db: " + db.getPath());
+
+            Log.d(LOGTAG, "Creating " + TABLE_BOOKMARKS + " table");
+            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
+                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Bookmarks.TITLE + " TEXT," +
+                    Bookmarks.URL + " TEXT," +
+                    Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
+                    Bookmarks.PARENT + " INTEGER," +
+                    Bookmarks.POSITION + " INTEGER NOT NULL," +
+                    Bookmarks.DATE_CREATED + " INTEGER," +
+                    Bookmarks.DATE_MODIFIED + " INTEGER," +
+                    Bookmarks.GUID + " TEXT" +
+                    ");");
+
+            Log.d(LOGTAG, "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" +
+                    ");");
+
+            Log.d(LOGTAG, "Creating " + TABLE_IMAGES + " table");
+            db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
+                    Images.URL + " TEXT UNIQUE NOT NULL," +
+                    Images.FAVICON + " BLOB," +
+                    Images.FAVICON_URL + " TEXT," +
+                    Images.THUMBNAIL + " BLOB," +
+                    Images.DATE_CREATED + " INTEGER," +
+                    Images.DATE_MODIFIED + " INTEGER," +
+                    Images.GUID + " TEXT" +
+                    ");");
+
+            db.execSQL("CREATE INDEX images_url_index ON " + TABLE_IMAGES + "("
+                    + Images.URL + ")");
+
+            // FIXME: Create default bookmarks here
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.d(LOGTAG, "Upgrading browser.db: " + db.getPath() + " from " +
+                    oldVersion + " to " + newVersion);
+
+            // Do nothing for now
+        }
+
+        @Override
+        public void onOpen(SQLiteDatabase db) {
+            Log.d(LOGTAG, "Opening browser.db: " + db.getPath());
+
+            // From Honeycomb on, it's possible to run several db
+            // commands in parallel using multiple connections.
+            if (Build.VERSION.SDK_INT >= 11)
+                db.enableWriteAheadLogging();
+        }
+    }
+
+    private DatabaseHelper getDatabaseHelperForProfile(String profile) {
+        Log.d(LOGTAG, "Getting database helper for profile: " + profile);
+
+        // 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 non has been provided
+        if (TextUtils.isEmpty(profile)) {
+            Log.d(LOGTAG, "No profile provided, using default");
+            profile = BrowserContract.DEFAULT_PROFILE;
+        }
+
+        DatabaseHelper dbHelper = mDatabasePerProfile.get(profile);
+
+        if (dbHelper == null) {
+            synchronized (this) {
+                dbHelper = new DatabaseHelper(getContext(), getDatabasePath(profile));
+                mDatabasePerProfile.put(profile, dbHelper);
+            }
+        }
+
+        Log.d(LOGTAG, "Successfully created database helper for profile: " + profile);
+
+        return dbHelper;
+    }
+
+    private String getDatabasePath(String profile) {
+        Log.d(LOGTAG, "Getting database path for profile: " + profile);
+
+        // On Android releases older than 2.3, it's not possible to use
+        // SQLiteOpenHelper with a full path. Fallback to using separate
+        // db files per profile in the app directory.
+        if (Build.VERSION.SDK_INT <= 8) {
+            return "browser-" + profile + ".db";
+        }
+
+        File profileDir = GeckoApp.mAppContext.getProfileDir(profile);
+        if (profileDir == null) {
+            Log.d(LOGTAG, "Couldn't find directory for profile: " + profile);
+            return null;
+        }
+
+        String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
+        Log.d(LOGTAG, "Successfully created database path for profile: " + databasePath);
+
+        return databasePath;
+    }
+
+    private SQLiteDatabase getReadableDatabase(Uri uri) {
+        Log.d(LOGTAG, "Getting readable database for URI: " + uri);
+
+        String profile = null;
+
+        if (uri != null)
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+
+        return getDatabaseHelperForProfile(profile).getReadableDatabase();
+    }
+
+    private SQLiteDatabase getWritableDatabase(Uri uri) {
+        Log.d(LOGTAG, "Getting writable database for URI: " + uri);
+
+        String profile = null;
+
+        if (uri != null)
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+
+        return getDatabaseHelperForProfile(profile).getWritableDatabase();
+    }
+
+    @Override
+    public boolean onCreate() {
+        Log.d(LOGTAG, "Creating BrowserProvider");
+
+        mDatabasePerProfile = new HashMap<String, DatabaseHelper>();
+
+        return true;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        final int match = URI_MATCHER.match(uri);
+
+        Log.d(LOGTAG, "Getting URI type: " + uri);
+
+        switch (match) {
+            case BOOKMARKS:
+                Log.d(LOGTAG, "URI is BOOKMARKS: " + uri);
+                return Bookmarks.CONTENT_TYPE;
+            case BOOKMARKS_ID:
+                Log.d(LOGTAG, "URI is BOOKMARKS_ID: " + uri);
+                return Bookmarks.CONTENT_ITEM_TYPE;
+            case HISTORY:
+                Log.d(LOGTAG, "URI is HISTORY: " + uri);
+                return History.CONTENT_TYPE;
+            case HISTORY_ID:
+                Log.d(LOGTAG, "URI is HISTORY_ID: " + uri);
+                return History.CONTENT_ITEM_TYPE;
+        }
+
+        Log.d(LOGTAG, "URI has unrecognized type: " + uri);
+
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        Log.d(LOGTAG, "Calling delete on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int deleted = 0;
+
+        if (Build.VERSION.SDK_INT >= 11) {
+            Log.d(LOGTAG, "Beginning delete transaction: " + uri);
+            db.beginTransaction();
+            try {
+                deleted = deleteInTransaction(uri, selection, selectionArgs);
+                db.setTransactionSuccessful();
+                Log.d(LOGTAG, "Successful delete transaction: " + uri);
+            } finally {
+                db.endTransaction();
+            }
+        } else {
+            deleted = deleteInTransaction(uri, selection, selectionArgs);
+        }
+
+        return deleted;
+    }
+
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+        Log.d(LOGTAG, "Calling delete in transaction on URI: " + uri);
+
+        final int match = URI_MATCHER.match(uri);
+        int deleted = 0;
+
+        switch (match) {
+            case BOOKMARKS_ID:
+                Log.d(LOGTAG, "Delete on BOOKMARKS_ID: " + uri);
+
+                selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
+                selectionArgs = appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            case BOOKMARKS: {
+                Log.d(LOGTAG, "Deleting bookmarks: " + uri);
+                deleted = deleteBookmarks(uri, selection, selectionArgs);
+                deleteUnusedImages(uri);
+                break;
+        }
+
+        case HISTORY_ID:
+            Log.d(LOGTAG, "Delete on HISTORY_ID: " + uri);
+
+            selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
+            selectionArgs = appendSelectionArgs(selectionArgs,
+                    new String[] { Long.toString(ContentUris.parseId(uri)) });
+            // fall through
+        case HISTORY: {
+            Log.d(LOGTAG, "Deleting history: " + uri);
+            deleted = deleteHistory(uri, selection, selectionArgs);
+            deleteUnusedImages(uri);
+            break;
+        }
+
+        default:
+            throw new UnsupportedOperationException("Unknown delete URI " + uri);
+        }
+
+        Log.d(LOGTAG, "Deleted " + deleted + " rows for URI: " + uri);
+
+        return deleted;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        Log.d(LOGTAG, "Calling insert on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        Uri result = null;
+
+        if (Build.VERSION.SDK_INT >= 11) {
+            Log.d(LOGTAG, "Beginning insert transaction: " + uri);
+            db.beginTransaction();
+            try {
+                result = insertInTransaction(uri, values);
+                db.setTransactionSuccessful();
+                Log.d(LOGTAG, "Successful insert transaction: " + uri);
+            } finally {
+                db.endTransaction();
+            }
+        } else {
+            result = insertInTransaction(uri, values);
+        }
+
+        return result;
+    }
+
+    public Uri insertInTransaction(Uri uri, ContentValues values) {
+        Log.d(LOGTAG, "Calling insert in transaction on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int match = URI_MATCHER.match(uri);
+        long id = -1;
+
+        switch (match) {
+            case BOOKMARKS: {
+                Log.d(LOGTAG, "Insert on BOOKMARKS: " + uri);
+
+                long now = System.currentTimeMillis();
+                values.put(Bookmarks.DATE_CREATED, now);
+                values.put(Bookmarks.DATE_MODIFIED, now);
+
+                // Generate GUID for new bookmark
+                values.put(Bookmarks.GUID, UUID.randomUUID().toString());
+
+                if (!values.containsKey(Bookmarks.POSITION)) {
+                    Log.d(LOGTAG, "Inserting bookmark with no position for URI");
+                    values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
+                }
+
+                String url = values.getAsString(Bookmarks.URL);
+                ContentValues imageValues = extractImageValues(values, url);
+                Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER);
+
+                if ((isFolder == null || !isFolder) && imageValues != null
+                        && !TextUtils.isEmpty(url)) {
+                    Log.d(LOGTAG, "Inserting bookmark image for URL: " + url);
+                    updateImage(uri, imageValues, Images.URL + " = ?", new String[] { url });
+                }
+
+                Log.d(LOGTAG, "Inserting bookmark in database with URL: " + url);
+                id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
+                break;
+            }
+
+            case HISTORY: {
+                Log.d(LOGTAG, "Insert on HISTORY: " + uri);
+
+                long now = System.currentTimeMillis();
+                values.put(History.DATE_CREATED, now);
+                values.put(History.DATE_MODIFIED, now);
+
+                // Generate GUID for new history entry
+                values.put(History.GUID, UUID.randomUUID().toString());
+
+                String url = values.getAsString(History.URL);
+
+                ContentValues imageValues = extractImageValues(values,
+                        values.getAsString(History.URL));
+
+                if (imageValues != null) {
+                    Log.d(LOGTAG, "Inserting history image for URL: " + url);
+                    updateImage(uri, imageValues, Images.URL + " = ?", new String[] { url });
+                }
+
+                Log.d(LOGTAG, "Inserting history in database with URL: " + url);
+                id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
+                break;
+            }
+
+            default:
+                throw new UnsupportedOperationException("Unknown insert URI " + uri);
+        }
+
+        Log.d(LOGTAG, "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) {
+        Log.d(LOGTAG, "Calling update on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int updated = 0;
+
+        if (Build.VERSION.SDK_INT >= 11) {
+            Log.d(LOGTAG, "Beginning update transaction: " + uri);
+            db.beginTransaction();
+            try {
+                updated = updateInTransaction(uri, values, selection, selectionArgs);
+                db.setTransactionSuccessful();
+                Log.d(LOGTAG, "Successful update transaction: " + uri);
+            } finally {
+                db.endTransaction();
+            }
+        } else {
+            updated = updateInTransaction(uri, values, selection, selectionArgs);
+        }
+
+        return updated;
+    }
+
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        Log.d(LOGTAG, "Calling update in transaction on URI: " + uri);
+
+        int match = URI_MATCHER.match(uri);
+        int updated = 0;
+
+        switch (match) {
+            case BOOKMARKS_ID:
+                Log.d(LOGTAG, "Update on BOOKMARKS_ID: " + uri);
+
+                selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
+                selectionArgs = appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            case BOOKMARKS: {
+                Log.d(LOGTAG, "Updating bookmark: " + uri);
+                updated = updateBookmarks(uri, values, selection, selectionArgs);
+                break;
+            }
+
+            case HISTORY_ID:
+                Log.d(LOGTAG, "Update on HISTORY_ID: " + uri);
+
+                selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
+                selectionArgs = appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            case HISTORY: {
+                Log.d(LOGTAG, "Updating history: " + uri);
+                updated = updateHistory(uri, values, selection, selectionArgs);
+                break;
+            }
+
+            case IMAGES: {
+                Log.d(LOGTAG, "Update on IMAGES: " + uri);
+
+                String url = values.getAsString(Images.URL);
+
+                if (TextUtils.isEmpty(url))
+                    throw new IllegalArgumentException("Images.URL is required");
+
+                updated = updateImage(uri, values, Images.URL + " = ?", new String[] { url });
+
+                break;
+            }
+
+            default:
+                throw new UnsupportedOperationException("Unknown update URI " + uri);
+        }
+
+        Log.d(LOGTAG, "Updated " + updated + " rows for URI: " + uri);
+
+        return updated;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        Log.d(LOGTAG, "Querying with URI: " + uri);
+
+        SQLiteDatabase db = getReadableDatabase(uri);
+        final int match = URI_MATCHER.match(uri);
+
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+
+        switch (match) {
+            case BOOKMARKS_FOLDER_ID:
+            case BOOKMARKS_ID:
+            case BOOKMARKS: {
+                Log.d(LOGTAG, "Query is on bookmarks: " + uri);
+
+                if (match == BOOKMARKS_ID) {
+                    Log.d(LOGTAG, "Query is BOOKMARKS_ID: " + uri);
+                    selection = concatenateWhere(selection,
+                            qualifyColumnValue(TABLE_BOOKMARKS, Bookmarks._ID) + " = ?");
+                    selectionArgs = appendSelectionArgs(selectionArgs,
+                            new String[] { Long.toString(ContentUris.parseId(uri)) });
+                } else if (match == BOOKMARKS_FOLDER_ID) {
+                    Log.d(LOGTAG, "Query is BOOKMARKS_FOLDER_ID: " + uri);
+                    selection = concatenateWhere(selection,
+                            qualifyColumnValue(TABLE_BOOKMARKS, Bookmarks.PARENT) + " = ?");
+                    selectionArgs = appendSelectionArgs(selectionArgs,
+                            new String[] { Long.toString(ContentUris.parseId(uri)) });
+                }
+
+                if (TextUtils.isEmpty(sortOrder)) {
+                    Log.d(LOGTAG, "Using default sort order on query: " + uri);
+                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
+                }
+
+                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
+                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
+
+                break;
+            }
+
+            case HISTORY_ID:
+            case HISTORY: {
+                Log.d(LOGTAG, "Query is on history: " + uri);
+
+                if (match == HISTORY_ID) {
+                    Log.d(LOGTAG, "Query is HISTORY_ID: " + uri);
+                    selection = concatenateWhere(selection,
+                            qualifyColumnValue(TABLE_HISTORY, History._ID) + " = ?");
+                    selectionArgs = appendSelectionArgs(selectionArgs,
+                            new String[] { Long.toString(ContentUris.parseId(uri)) });
+                }
+
+                if (TextUtils.isEmpty(sortOrder))
+                    sortOrder = DEFAULT_HISTORY_SORT_ORDER;
+
+                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
+                qb.setTables(TABLE_HISTORY_JOIN_IMAGES);
+
+                break;
+            }
+
+            case IMAGES: {
+                Log.d(LOGTAG, "Query is on images: " + uri);
+
+                qb.setProjectionMap(IMAGES_PROJECTION_MAP);
+                qb.setTables(TABLE_IMAGES);
+
+                break;
+            }
+
+            default:
+                throw new UnsupportedOperationException("Unknown query URI " + uri);
+        }
+
+        Log.d(LOGTAG, "Finally running the built query: " + uri);
+        Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
+                null, sortOrder, limit);
+        cursor.setNotificationUri(getContext().getContentResolver(),
+                BrowserContract.AUTHORITY_URI);
+
+        return cursor;
+    }
+
+    ContentValues extractImageValues(ContentValues values, String url) {
+        Log.d(LOGTAG, "Extracting image values for URI: " + url);
+
+        ContentValues imageValues = null;
+
+        if (values.containsKey(Bookmarks.FAVICON)) {
+            Log.d(LOGTAG, "Has favicon value on URL: " + url);
+            imageValues = new ContentValues();
+            imageValues.put(Images.FAVICON,
+                    values.getAsByteArray(Bookmarks.FAVICON));
+            values.remove(Bookmarks.FAVICON);
+        }
+
+        if (values.containsKey(Bookmarks.THUMBNAIL)) {
+            Log.d(LOGTAG, "Has favicon value on URL: " + url);
+            if (imageValues == null)
+                imageValues = new ContentValues();
+
+            imageValues.put(Images.THUMBNAIL,
+                    values.getAsByteArray(Bookmarks.THUMBNAIL));
+            values.remove(Bookmarks.THUMBNAIL);
+        }
+
+        if (imageValues != null && url != null) {
+            Log.d(LOGTAG, "Has URL value");
+            imageValues.put(Images.URL, url);
+        }
+
+        return imageValues;
+    }
+
+    int getUrlCount(SQLiteDatabase db, String table, String url) {
+        Cursor c = db.query(table, new String[] { "COUNT(*)" },
+                CommonColumns.URL + " = ?", new String[] { url }, null, null,
+                null);
+
+        int count = 0;
+
+        try {
+            if (c.moveToFirst())
+                count = c.getInt(0);
+        } finally {
+            c.close();
+        }
+
+        return count;
+    }
+
+    int updateBookmarks(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        Log.d(LOGTAG, "Updating bookmarks on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int updated = 0;
+
+        final String[] bookmarksProjection = new String[] {
+                Bookmarks._ID, // 0
+                Bookmarks.URL, // 1
+        };
+
+        Log.d(LOGTAG, "Quering bookmarks to update on URI: " + uri);
+
+        Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
+                selection, selectionArgs, null, null, null);
+
+        try {
+            values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+
+            boolean updatingUrl = values.containsKey(Bookmarks.URL);
+            String url = null;
+
+            if (updatingUrl)
+                url = values.getAsString(Bookmarks.URL);
+
+            ContentValues imageValues = extractImageValues(values, url);
+
+            while (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+
+                Log.d(LOGTAG, "Updating bookmark with ID: " + id);
+
+                updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
+                        new String[] { Long.toString(id) });
+
+                if (imageValues == null)
+                    continue;
+
+                if (!updatingUrl) {
+                    url = cursor.getString(1);
+                    imageValues.put(Images.URL, url);
+                }
+
+                if (!TextUtils.isEmpty(url)) {
+                    Log.d(LOGTAG, "Updating bookmark image for URL: " + url);
+                    updateImage(uri, imageValues, Images.URL + " = ?", new String[] { url });
+                }
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+
+        return updated;
+    }
+
+    int updateHistory(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        Log.d(LOGTAG, "Updating history on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int updated = 0;
+
+        final String[] historyProjection = new String[] { History._ID, // 0
+                History.URL, // 1
+        };
+
+        Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
+                selectionArgs, null, null, null);
+
+        try {
+            values.put(History.DATE_MODIFIED, System.currentTimeMillis());
+
+            boolean updatingUrl = values.containsKey(History.URL);
+            String url = null;
+
+            if (updatingUrl)
+                url = values.getAsString(History.URL);
+
+            ContentValues imageValues = extractImageValues(values, url);
+
+            while (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+
+                Log.d(LOGTAG, "Updating history entry with ID: " + id);
+
+                updated += db.update(TABLE_HISTORY, values, "_id = ?",
+                        new String[] { Long.toString(id) });
+
+                if (imageValues == null)
+                    continue;
+
+                if (!updatingUrl) {
+                    url = cursor.getString(1);
+                    imageValues.put(Images.URL, url);
+                }
+
+                if (!TextUtils.isEmpty(url)) {
+                    Log.d(LOGTAG, "Updating history image for URL: " + url);
+                    updateImage(uri, imageValues, Images.URL + " = ?", new String[] { url });
+                }
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+
+        return updated;
+    }
+
+    int updateImage(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        String url = values.getAsString(Images.URL);
+
+        Log.d(LOGTAG, "Updating image for URL: " + url);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        long now = System.currentTimeMillis();
+
+        // Thumbnails update on every page load. We don't want to flood
+        // sync with meaningless last modified date. Only update modified
+        // date when favicons bits change.
+        if (values.containsKey(Images.FAVICON) || values.containsKey(Images.FAVICON_URL))
+            values.put(Images.DATE_MODIFIED, now);
+
+        Log.d(LOGTAG, "Trying to update image for URL: " + url);
+        int updated = db.update(TABLE_IMAGES, values, selection, selectionArgs);
+
+        if (updated == 0) {
+            // Generate GUID for new image
+            values.put(Images.GUID, UUID.randomUUID().toString());
+            values.put(Images.DATE_CREATED, now);
+            values.put(Images.DATE_MODIFIED, now);
+
+            Log.d(LOGTAG, "No update, inserting image for URL: " + url);
+            db.insert(TABLE_IMAGES, Images.FAVICON, values);
+            updated = 1;
+        }
+
+        return updated;
+    }
+
+    int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
+        Log.d(LOGTAG, "Deleting history entry for URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        return db.delete(TABLE_HISTORY, selection, selectionArgs);
+    }
+
+    int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
+        Log.d(LOGTAG, "Deleting bookmarks for URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
+    }
+
+    int deleteUnusedImages(Uri uri) {
+        Log.d(LOGTAG, "Deleting all unused images for URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        String selection = Images.URL + " NOT IN (SELECT " + Bookmarks.URL +
+                " FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.URL +
+                " IS NOT NULL) AND " + Images.URL + " NOT IN (SELECT " +
+                History.URL + " FROM " + TABLE_HISTORY + " WHERE " +
+                History.URL + " IS NOT NULL)";
+
+        return db.delete(TABLE_IMAGES, selection, null);
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -0,0 +1,345 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Android code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Lucas Rocha <lucasr@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+package org.mozilla.gecko.db;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Date;
+
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.CommonColumns;
+import org.mozilla.gecko.db.BrowserContract.History;
+import org.mozilla.gecko.db.BrowserContract.ImageColumns;
+import org.mozilla.gecko.db.BrowserContract.Images;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.provider.Browser;
+
+public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
+    // Same as android.provider.Browser for consistency
+    private static final int MAX_HISTORY_COUNT = 250;
+
+    // Same as android.provider.Browser for consistency
+    public static final int TRUNCATE_N_OLDEST = 5;
+
+    private final String mProfile;
+
+    public LocalBrowserDB(String profile) {
+        mProfile = profile;
+    }
+
+    private Uri appendProfileAndLimit(Uri uri, int limit) {
+        return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).
+                appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)).build();
+    }
+
+    private Uri appendProfile(Uri uri) {
+        return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).build();
+    }
+
+    public Cursor filter(ContentResolver cr, CharSequence constraint, int limit) {
+        Cursor c = cr.query(appendProfileAndLimit(History.CONTENT_URI, limit),
+                            new String[] { History._ID,
+                                           History.URL,
+                                           History.TITLE,
+                                           History.FAVICON,
+                                           History.THUMBNAIL },
+                            "(" + History.URL + " LIKE ? OR " + History.TITLE + " LIKE ?)",
+                            new String[] {"%" + constraint.toString() + "%", "%" + constraint.toString() + "%"},
+                            // ORDER BY is number of visits times a multiplier from 1 - 120 of how recently the site
+                            // was accessed with a site accessed today getting 120 and a site accessed 119 or more
+                            // days ago getting 1
+                            History.VISITS + " * MAX(1, (" +
+                            History.DATE_LAST_VISITED + " - " + new Date().getTime() + ") / 86400000 + 120) DESC");
+
+        return new LocalDBCursor(c);
+    }
+
+    private void truncateHistory(ContentResolver cr) {
+        Cursor cursor = null;
+
+        try {
+            cursor = cr.query(appendProfile(History.CONTENT_URI),
+                              new String[] { History._ID },
+                              null,
+                              null,
+                              History.DATE_LAST_VISITED + " ASC");
+
+            if (cursor.getCount() < MAX_HISTORY_COUNT)
+                return;
+
+            if (cursor.moveToFirst()) {
+                for (int i = 0; i < TRUNCATE_N_OLDEST; i++) {
+                    Uri historyUri = ContentUris.withAppendedId(History.CONTENT_URI, cursor.getLong(0)); 
+                    cr.delete(appendProfile(historyUri), null, null);
+
+                    if (!cursor.moveToNext())
+                        break;
+                }
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+    }
+
+    public void updateVisitedHistory(ContentResolver cr, String uri) {
+        long now = System.currentTimeMillis();
+        Cursor cursor = null;
+
+        try {
+            final String[] projection = new String[] {
+                    History._ID,    // 0
+                    History.VISITS, // 1
+            };
+
+            cursor = cr.query(appendProfile(History.CONTENT_URI),
+                              projection,
+                              History.URL + " = ?",
+                              new String[] { uri },
+                              null);
+
+            if (cursor.moveToFirst()) {
+                ContentValues values = new ContentValues();
+
+                values.put(History.VISITS, cursor.getInt(1) + 1);
+                values.put(History.DATE_LAST_VISITED, now);
+
+                Uri historyUri = ContentUris.withAppendedId(History.CONTENT_URI, cursor.getLong(0));
+                cr.update(appendProfile(historyUri), values, null, null);
+            } else {
+                // Ensure we don't blow up our database with too
+                // many history items.
+                truncateHistory(cr);
+
+                ContentValues values = new ContentValues();
+
+                values.put(History.URL, uri);
+                values.put(History.VISITS, 1);
+                values.put(History.DATE_LAST_VISITED, now);
+                values.put(History.TITLE, uri);
+
+                cr.insert(appendProfile(History.CONTENT_URI), values);
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+    }
+
+    public void updateHistoryTitle(ContentResolver cr, String uri, String title) {
+        ContentValues values = new ContentValues();
+        values.put(History.TITLE, title);
+
+        cr.update(appendProfile(History.CONTENT_URI),
+                  values,
+                  History.URL + " = ?",
+                  new String[] { uri });
+    }
+
+    public Cursor getAllVisitedHistory(ContentResolver cr) {
+        Cursor c = cr.query(appendProfile(History.CONTENT_URI),
+                            new String[] { History.URL },
+                            History.VISITS + " > 0",
+                            null,
+                            null);
+
+        return new LocalDBCursor(c);
+    }
+
+    public Cursor getRecentHistory(ContentResolver cr, int limit) {
+        Cursor c = cr.query(appendProfileAndLimit(History.CONTENT_URI, limit),
+                            new String[] { History._ID,
+                                           History.URL,
+                                           History.TITLE,
+                                           History.FAVICON,
+                                           History.DATE_LAST_VISITED },
+                            History.DATE_LAST_VISITED + " > 0",
+                            null,
+                            History.DATE_LAST_VISITED + " DESC");
+
+        return new LocalDBCursor(c);
+    }
+
+    public void clearHistory(ContentResolver cr) {
+        cr.delete(appendProfile(History.CONTENT_URI), null, null);
+    }
+
+    public Cursor getAllBookmarks(ContentResolver cr) {
+        Cursor c = cr.query(appendProfile(Bookmarks.CONTENT_URI),
+                            new String[] { Bookmarks._ID,
+                                           Bookmarks.URL,
+                                           Bookmarks.TITLE,
+                                           Bookmarks.FAVICON },
+                            Bookmarks.IS_FOLDER + " = 0",
+                            null,
+                            Bookmarks.TITLE + " ASC");
+
+        return new LocalDBCursor(c);
+    }
+
+    public boolean isBookmark(ContentResolver cr, String uri) {
+        Cursor cursor = cr.query(appendProfile(Bookmarks.CONTENT_URI),
+                                 new String[] { Bookmarks._ID },
+                                 Bookmarks.URL + " = ?",
+                                 new String[] { uri },
+                                 Bookmarks.URL);
+
+        int count = cursor.getCount();
+        cursor.close();
+
+        return (count == 1);
+    }
+
+    public void addBookmark(ContentResolver cr, String title, String uri) {
+        ContentValues values = new ContentValues();
+        values.put(Browser.BookmarkColumns.TITLE, title);
+        values.put(Bookmarks.URL, uri);
+
+        int updated = cr.update(appendProfile(Bookmarks.CONTENT_URI),
+                                values,
+                                Bookmarks.URL + " = ?",
+                                new String[] { uri });
+
+        if (updated == 0)
+            cr.insert(appendProfile(Bookmarks.CONTENT_URI), values);
+    }
+
+    public void removeBookmark(ContentResolver cr, String uri) {
+        cr.delete(appendProfile(Bookmarks.CONTENT_URI),
+                  Bookmarks.URL + " = ?",
+                  new String[] { uri });
+    }
+
+    public BitmapDrawable getFaviconForUrl(ContentResolver cr, String uri) {
+        Cursor c = cr.query(appendProfile(Images.CONTENT_URI),
+                            new String[] { Images.FAVICON },
+                            Images.URL + " = ?",
+                            new String[] { uri },
+                            null);
+
+        if (!c.moveToFirst()) {
+            c.close();
+            return null;
+        }
+
+        int faviconIndex = c.getColumnIndexOrThrow(Images.FAVICON);
+
+        byte[] b = c.getBlob(faviconIndex);
+        c.close();
+
+        if (b == null)
+            return null;
+
+        Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
+        return new BitmapDrawable(bitmap);
+    }
+
+    public void updateFaviconForUrl(ContentResolver cr, String uri,
+            BitmapDrawable favicon) {
+        Bitmap bitmap = favicon.getBitmap();
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+
+        ContentValues values = new ContentValues();
+        values.put(Images.FAVICON, stream.toByteArray());
+        values.put(Images.URL, uri);
+
+        cr.update(appendProfile(Images.CONTENT_URI),
+                  values,
+                  Images.URL + " = ?",
+                  new String[] { uri });
+    }
+
+    public void updateThumbnailForUrl(ContentResolver cr, String uri,
+            BitmapDrawable thumbnail) {
+        Bitmap bitmap = thumbnail.getBitmap();
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream);
+
+        ContentValues values = new ContentValues();
+        values.put(Images.THUMBNAIL, stream.toByteArray());
+        values.put(Images.URL, uri);
+
+        cr.update(appendProfile(Images.CONTENT_URI),
+                  values,
+                  Images.URL + " = ?",
+                  new String[] { uri });
+    }
+
+    private static class LocalDBCursor extends CursorWrapper {
+        public LocalDBCursor(Cursor c) {
+            super(c);
+        }
+
+        private String translateColumnName(String columnName) {
+            if (columnName.equals(BrowserDB.URLColumns.URL)) {
+                columnName = CommonColumns.URL;
+            } else if (columnName.equals(BrowserDB.URLColumns.TITLE)) {
+                columnName = CommonColumns.TITLE;
+            } else if (columnName.equals(BrowserDB.URLColumns.FAVICON)) {
+                columnName = ImageColumns.FAVICON;
+            } else if (columnName.equals(BrowserDB.URLColumns.THUMBNAIL)) {
+                columnName = ImageColumns.THUMBNAIL;
+            } else if (columnName.equals(BrowserDB.URLColumns.DATE_LAST_VISITED)) {
+                columnName = History.DATE_LAST_VISITED;
+            }
+
+            return columnName;
+        }
+
+        @Override
+        public int getColumnIndex(String columnName) {
+            return super.getColumnIndex(translateColumnName(columnName));
+        }
+
+        @Override
+        public int getColumnIndexOrThrow(String columnName) {
+            return super.getColumnIndexOrThrow(translateColumnName(columnName));
+        }
+    }
+}
\ No newline at end of file