Bug 727264 - Update Profile Migration to use Cursors. r=blassey
authorGian-Carlo Pascutto <gpascutto@mozilla.com>
Mon, 27 Feb 2012 12:28:22 +0100
changeset 87832 3e6935243310b57940c140238306dab8fefafa51
parent 87831 ff36792efde97c8409f92fa4ad5644a9caa6a9ef
child 87833 0bf80d3cebf5d8cdc251d2c4432651350eddc3c7
push id22160
push usermbrubeck@mozilla.com
push dateTue, 28 Feb 2012 17:21:33 +0000
treeherdermozilla-central@dde4e0089a18 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblassey
bugs727264
milestone13.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 727264 - Update Profile Migration to use Cursors. r=blassey
mobile/android/base/Makefile.in
mobile/android/base/ProfileMigrator.java
mobile/android/base/sqlite/MatrixBlobCursor.java
mobile/android/base/sqlite/SQLiteBridge.java
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -91,16 +91,17 @@ FENNEC_JAVA_FILES = \
   GeckoProfile.java \
   GeckoStateListDrawable.java \
   GeckoThread.java \
   GlobalHistory.java \
   LinkPreference.java \
   ProfileMigrator.java \
   PromptService.java \
   sqlite/ByteBufferInputStream.java \
+  sqlite/MatrixBlobCursor.java \
   sqlite/SQLiteBridge.java \
   sqlite/SQLiteBridgeException.java \
   SetupScreen.java \
   SurfaceLockInfo.java \
   Tab.java \
   Tabs.java \
   TabsTray.java \
   gfx/BitmapUtils.java \
--- a/mobile/android/base/ProfileMigrator.java
+++ b/mobile/android/base/ProfileMigrator.java
@@ -40,37 +40,37 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.ImageColumns;
 import org.mozilla.gecko.db.BrowserContract.Images;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.sqlite.ByteBufferInputStream;
 import org.mozilla.gecko.sqlite.SQLiteBridge;
 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.SQLException;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.provider.Browser;
 import android.util.Log;
 import android.net.Uri;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.File;
-import java.nio.ByteBuffer;
+import java.io.InputStream;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -215,27 +215,30 @@ public class ProfileMigrator {
         }
 
         // We want to know the id of special root folders in the places DB,
         // and replace them by the corresponding root id in the Android DB.
         protected void calculateReroot(SQLiteBridge db) {
             mRerootMap = new HashMap<Long, Long>();
 
             try {
-                ArrayList<Object[]> queryResult = db.query(kRootQuery);
-                final int rootCol = db.getColumnIndex(kRootName);
-                final int folderCol = db.getColumnIndex(kRootFolderId);
+                Cursor cursor = db.rawQuery(kRootQuery, null);
+                final int rootCol = cursor.getColumnIndex(kRootName);
+                final int folderCol = cursor.getColumnIndex(kRootFolderId);
 
-                for (Object[] resultRow: queryResult) {
-                    String name = (String)resultRow[rootCol];
-                    long placesFolderId = Integer.parseInt((String)resultRow[folderCol]);
+                cursor.moveToFirst();
+                while (!cursor.isAfterLast()) {
+                    String name = cursor.getString(rootCol);
+                    long placesFolderId = cursor.getLong(folderCol);
                     mRerootMap.put(placesFolderId, getFolderId(name));
                     Log.v(LOGTAG, "Name: " + name + ", pid=" + placesFolderId
                           + ", nid=" + mRerootMap.get(placesFolderId));
+                    cursor.moveToNext();
                 }
+                cursor.close();
             } catch (SQLiteBridgeException e) {
                 Log.e(LOGTAG, "Failed to get bookmark roots: ", e);
                 return;
             }
         }
 
         // Get a list of the last times an URL was accessed
         protected Map<String, Long> gatherBrowserDBHistory() {
@@ -332,25 +335,25 @@ public class ProfileMigrator {
                     mCr.insert(getHistoryUri(), values);
                 }
             } finally {
                 if (cursor != null)
                     cursor.close();
             }
         }
 
-        protected BitmapDrawable decodeImageData(ByteBuffer data) {
-            ByteBufferInputStream byteStream = new ByteBufferInputStream(data);
+        protected BitmapDrawable decodeImageData(byte[] data) {
+            InputStream byteStream = new ByteArrayInputStream(data);
             BitmapDrawable image =
                 (BitmapDrawable)Drawable.createFromStream(byteStream, "src");
             return image;
         }
 
         protected void addFavicon(String url, String faviconUrl, String faviconGuid,
-                                  String mime, ByteBuffer data) {
+                                  String mime, byte[] data) {
             // Some GIFs can cause us to lock up completely
             // without exceptions or anything. Not cool.
             if (mime == null || mime.compareTo("image/gif") == 0) {
                 return;
             }
             BitmapDrawable image = null;
             // Decode non-PNG images.
             if (mime.compareTo("image/png") != 0) {
@@ -368,19 +371,17 @@ public class ProfileMigrator {
                 if (image != null) {
                     Bitmap bitmap = image.getBitmap();
                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
                     bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
                     values.put(Images.FAVICON, stream.toByteArray());
                 } else {
                     // PNG images can be passed directly. Well, aside
                     // from having to convert them into a byte[].
-                    byte[] byteArray = new byte[data.remaining()];
-                    data.get(byteArray);
-                    values.put(Images.FAVICON, byteArray);
+                    values.put(Images.FAVICON, data);
                 }
 
                 values.put(Images.URL, url);
                 values.put(Images.FAVICON_URL, faviconUrl);
                 // Restore deleted record if possible
                 values.put(Images.IS_DELETED, 0);
                 values.put(Images.GUID, faviconGuid);
 
@@ -407,46 +408,48 @@ public class ProfileMigrator {
                     /* current time */
                     Long.toString(System.currentTimeMillis()),
                     /*
                        History entries to return. No point
                        in retrieving more than we can store.
                      */
                     Integer.toString(BrowserDB.getMaxHistoryCount())
                 };
-                ArrayList<Object[]> queryResult =
-                    db.query(kHistoryQuery, queryParams);
-                final int urlCol = db.getColumnIndex(kHistoryUrl);
-                final int titleCol = db.getColumnIndex(kHistoryTitle);
-                final int dateCol = db.getColumnIndex(kHistoryDate);
-                final int visitsCol = db.getColumnIndex(kHistoryVisits);
-                final int faviconMimeCol = db.getColumnIndex(kFaviconMime);
-                final int faviconDataCol = db.getColumnIndex(kFaviconData);
-                final int faviconUrlCol = db.getColumnIndex(kFaviconUrl);
-                final int faviconGuidCol = db.getColumnIndex(kFaviconGuid);
+                Cursor cursor = db.rawQuery(kHistoryQuery, queryParams);
+                final int urlCol = cursor.getColumnIndex(kHistoryUrl);
+                final int titleCol = cursor.getColumnIndex(kHistoryTitle);
+                final int dateCol = cursor.getColumnIndex(kHistoryDate);
+                final int visitsCol = cursor.getColumnIndex(kHistoryVisits);
+                final int faviconMimeCol = cursor.getColumnIndex(kFaviconMime);
+                final int faviconDataCol = cursor.getColumnIndex(kFaviconData);
+                final int faviconUrlCol = cursor.getColumnIndex(kFaviconUrl);
+                final int faviconGuidCol = cursor.getColumnIndex(kFaviconGuid);
 
-                for (Object[] resultRow: queryResult) {
-                    String url = (String)resultRow[urlCol];
-                    String title = (String)resultRow[titleCol];
-                    long date = Long.parseLong((String)(resultRow[dateCol])) / (long)1000;
-                    int visits = Integer.parseInt((String)(resultRow[visitsCol]));
-                    ByteBuffer faviconDataBuff = (ByteBuffer)resultRow[faviconDataCol];
-                    String faviconMime = (String)resultRow[faviconMimeCol];
-                    String faviconUrl = (String)resultRow[faviconUrlCol];
-                    String faviconGuid = (String)resultRow[faviconGuidCol];
+                cursor.moveToFirst();
+                while (!cursor.isAfterLast()) {
+                    String url = cursor.getString(urlCol);
+                    String title = cursor.getString(titleCol);
+                    long date = cursor.getLong(dateCol) / (long)1000;
+                    int visits = cursor.getInt(visitsCol);
+                    byte[] faviconDataBuff = cursor.getBlob(faviconDataCol);
+                    String faviconMime = cursor.getString(faviconMimeCol);
+                    String faviconUrl = cursor.getString(faviconUrlCol);
+                    String faviconGuid = cursor.getString(faviconGuidCol);
 
                     try {
                         placesHistory.add(url);
                         addFavicon(url, faviconUrl, faviconGuid,
                                    faviconMime, faviconDataBuff);
                         addHistory(browserDBHistory, url, title, date, visits);
                     } catch (Exception e) {
                         Log.e(LOGTAG, "Error adding history entry: ", e);
                     }
+                    cursor.moveToNext();
                 }
+                cursor.close();
             } catch (SQLiteBridgeException e) {
                 Log.e(LOGTAG, "Failed to get history: ", e);
                 return;
             }
             // GlobalHistory access communicates with Gecko
             // and must run on its thread
             GeckoAppShell.getHandler().post(new Runnable() {
                     public void run() {
@@ -494,30 +497,30 @@ public class ProfileMigrator {
             }
             if (updated == 0) {
                 mCr.insert(getBookmarksUri(), values);
             }
         }
 
         protected void migrateBookmarks(SQLiteBridge db) {
             try {
-                ArrayList<Object[]> queryResult = db.query(kBookmarkQuery);
-                final int urlCol = db.getColumnIndex(kBookmarkUrl);
-                final int titleCol = db.getColumnIndex(kBookmarkTitle);
-                final int guidCol = db.getColumnIndex(kBookmarkGuid);
-                final int idCol = db.getColumnIndex(kBookmarkId);
-                final int typeCol = db.getColumnIndex(kBookmarkType);
-                final int parentCol = db.getColumnIndex(kBookmarkParent);
-                final int addedCol = db.getColumnIndex(kBookmarkAdded);
-                final int modifiedCol = db.getColumnIndex(kBookmarkModified);
-                final int positionCol = db.getColumnIndex(kBookmarkPosition);
-                final int faviconMimeCol = db.getColumnIndex(kFaviconMime);
-                final int faviconDataCol = db.getColumnIndex(kFaviconData);
-                final int faviconUrlCol = db.getColumnIndex(kFaviconUrl);
-                final int faviconGuidCol = db.getColumnIndex(kFaviconGuid);
+                Cursor cursor = db.rawQuery(kBookmarkQuery, null);
+                final int urlCol = cursor.getColumnIndex(kBookmarkUrl);
+                final int titleCol = cursor.getColumnIndex(kBookmarkTitle);
+                final int guidCol = cursor.getColumnIndex(kBookmarkGuid);
+                final int idCol = cursor.getColumnIndex(kBookmarkId);
+                final int typeCol = cursor.getColumnIndex(kBookmarkType);
+                final int parentCol = cursor.getColumnIndex(kBookmarkParent);
+                final int addedCol = cursor.getColumnIndex(kBookmarkAdded);
+                final int modifiedCol = cursor.getColumnIndex(kBookmarkModified);
+                final int positionCol = cursor.getColumnIndex(kBookmarkPosition);
+                final int faviconMimeCol = cursor.getColumnIndex(kFaviconMime);
+                final int faviconDataCol = cursor.getColumnIndex(kFaviconData);
+                final int faviconUrlCol = cursor.getColumnIndex(kFaviconUrl);
+                final int faviconGuidCol = cursor.getColumnIndex(kFaviconGuid);
 
                 // The keys are places IDs.
                 Set<Long> openFolders = new HashSet<Long>();
                 Set<Long> knownFolders = new HashSet<Long>(mRerootMap.keySet());
 
                 // We iterate over all bookmarks, and add all bookmarks that
                 // have their parent folders present. If there are bookmarks
                 // that we can't add, we remember what these are and try again
@@ -530,43 +533,48 @@ public class ProfileMigrator {
                 do {
                     // Reset the set of missing folders that block us from
                     // adding entries.
                     openFolders.clear();
 
                     int added = 0;
                     int skipped = 0;
 
-                    for (Object[] resultRow: queryResult) {
-                        long id = Long.parseLong((String)resultRow[idCol]);
+                    cursor.moveToFirst();
+                    while (!cursor.isAfterLast()) {
+                        long id = cursor.getLong(idCol);
 
                         // Already processed? if so just skip
-                        if (processedBookmarks.contains(id))
+                        if (processedBookmarks.contains(id)) {
+                            cursor.moveToNext();
                             continue;
+                        }
 
-                        int type = Integer.parseInt((String)resultRow[typeCol]);
-                        long parent = Long.parseLong((String)resultRow[parentCol]);
+                        int type = cursor.getInt(typeCol);
+                        long parent = cursor.getLong(parentCol);
 
                         // Places has an explicit root folder, id=1 parent=0.
                         // Skip that.
-                        if (id == 1 && parent == 0 && type == kPlacesTypeFolder)
+                        if (id == 1 && parent == 0 && type == kPlacesTypeFolder) {
+                            cursor.moveToNext();
                             continue;
+                        }
 
-                        String url = (String)resultRow[urlCol];
-                        String title = (String)resultRow[titleCol];
-                        String guid = (String)resultRow[guidCol];
+                        String url = cursor.getString(urlCol);
+                        String title = cursor.getString(titleCol);
+                        String guid = cursor.getString(guidCol);
                         long dateadded =
-                            Long.parseLong((String)resultRow[addedCol]) / (long)1000;
+                            cursor.getLong(addedCol) / (long)1000;
                         long datemodified =
-                            Long.parseLong((String)resultRow[modifiedCol]) / (long)1000;
-                        long position = Long.parseLong((String)resultRow[positionCol]);
-                        ByteBuffer faviconDataBuff = (ByteBuffer)resultRow[faviconDataCol];
-                        String faviconMime = (String)resultRow[faviconMimeCol];
-                        String faviconUrl = (String)resultRow[faviconUrlCol];
-                        String faviconGuid = (String)resultRow[faviconGuidCol];
+                            cursor.getLong(modifiedCol) / (long)1000;
+                        long position = cursor.getLong(positionCol);
+                        byte[] faviconDataBuff = cursor.getBlob(faviconDataCol);
+                        String faviconMime = cursor.getString(faviconMimeCol);
+                        String faviconUrl = cursor.getString(faviconUrlCol);
+                        String faviconGuid = cursor.getString(faviconGuidCol);
 
                         // Is the parent for this bookmark already added?
                         // If so, we can add the bookmark itself.
                         if (knownFolders.contains(parent)) {
                             try {
                                 boolean isFolder = (type == kPlacesTypeFolder);
                                 addBookmark(url, title, guid, parent,
                                             dateadded, datemodified,
@@ -585,16 +593,17 @@ public class ProfileMigrator {
                                 Log.e(LOGTAG, "Error adding bookmark: ", e);
                             }
                             added++;
                         } else {
                             // We have to postpone until parent is processed;
                             openFolders.add(parent);
                             skipped++;
                         }
+                        cursor.moveToNext();
                     }
 
                     // Now check if any of the new folders we added was a folder
                     // that we were blocked on, by intersecting openFolders and
                     // knownFolders. If this is empty, we're done because the next
                     // iteration can't make progress.
                     boolean changed = openFolders.retainAll(knownFolders);
 
@@ -603,16 +612,18 @@ public class ProfileMigrator {
                     // those folders are orphans. Report this situation here.
                     if (openFolders.isEmpty() && changed) {
                         Log.w(LOGTAG, "Orphaned bookmarks found, not imported");
                     }
                     iterations++;
                     Log.i(LOGTAG, "Iteration = " + iterations + ", added " + added +
                           " bookmark(s), skipped " + skipped + " bookmark(s)");
                 } while (!openFolders.isEmpty());
+
+                cursor.close();
             } catch (SQLiteBridgeException e) {
                 Log.e(LOGTAG, "Failed to get bookmarks: ", e);
                 return;
             }
         }
 
         protected void migratePlaces(File aFile) {
             String dbPath = aFile.getPath() + "/places.sqlite";
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sqlite/MatrixBlobCursor.java
@@ -0,0 +1,310 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mozilla.gecko.sqlite;
+
+import org.mozilla.gecko.sqlite.SQLiteBridgeException;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.database.DatabaseUtils;
+import java.lang.UnsupportedOperationException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/*
+ * Android's AbstractCursor throws on getBlob()
+ * and MatrixCursor forgot to override it. This was fixed
+ * at some point but old devices are still SOL.
+ * Oh, and everything in MatrixCursor is private instead of
+ * protected, so we need to entirely duplicate it here,
+ * instad of just being able to add the missing method.
+ */
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ */
+public class MatrixBlobCursor extends AbstractCursor {
+
+    private final String[] columnNames;
+    private Object[] data;
+    private int rowCount = 0;
+    private final int columnCount;
+
+    /**
+     * Constructs a new cursor with the given initial capacity.
+     *
+     * @param columnNames names of the columns, the ordering of which
+     *  determines column ordering elsewhere in this cursor
+     * @param initialCapacity in rows
+     */
+    public MatrixBlobCursor(String[] columnNames, int initialCapacity) {
+        this.columnNames = columnNames;
+        this.columnCount = columnNames.length;
+
+        if (initialCapacity < 1) {
+            initialCapacity = 1;
+        }
+
+        this.data = new Object[columnCount * initialCapacity];
+    }
+
+    /**
+     * Constructs a new cursor.
+     *
+     * @param columnNames names of the columns, the ordering of which
+     *  determines column ordering elsewhere in this cursor
+     */
+    public MatrixBlobCursor(String[] columnNames) {
+        this(columnNames, 16);
+    }
+
+    /**
+     * Gets value at the given column for the current row.
+     */
+    protected Object get(int column) {
+        if (column < 0 || column >= columnCount) {
+            throw new CursorIndexOutOfBoundsException("Requested column: "
+                    + column + ", # of columns: " +  columnCount);
+        }
+        if (mPos < 0) {
+            throw new CursorIndexOutOfBoundsException("Before first row.");
+        }
+        if (mPos >= rowCount) {
+            throw new CursorIndexOutOfBoundsException("After last row.");
+        }
+        return data[mPos * columnCount + column];
+    }
+
+    /**
+     * Adds a new row to the end and returns a builder for that row. Not safe
+     * for concurrent use.
+     *
+     * @return builder which can be used to set the column values for the new
+     *  row
+     */
+    public RowBuilder newRow() {
+        rowCount++;
+        int endIndex = rowCount * columnCount;
+        ensureCapacity(endIndex);
+        int start = endIndex - columnCount;
+        return new RowBuilder(start, endIndex);
+    }
+
+    /**
+     * Adds a new row to the end with the given column values. Not safe
+     * for concurrent use.
+     *
+     * @throws IllegalArgumentException if {@code columnValues.length !=
+     *  columnNames.length}
+     * @param columnValues in the same order as the the column names specified
+     *  at cursor construction time
+     */
+    public void addRow(Object[] columnValues) {
+        if (columnValues.length != columnCount) {
+            throw new IllegalArgumentException("columnNames.length = "
+                    + columnCount + ", columnValues.length = "
+                    + columnValues.length);
+        }
+
+        int start = rowCount++ * columnCount;
+        ensureCapacity(start + columnCount);
+        System.arraycopy(columnValues, 0, data, start, columnCount);
+    }
+
+    /**
+     * Adds a new row to the end with the given column values. Not safe
+     * for concurrent use.
+     *
+     * @throws IllegalArgumentException if {@code columnValues.size() !=
+     *  columnNames.length}
+     * @param columnValues in the same order as the the column names specified
+     *  at cursor construction time
+     */
+    public void addRow(Iterable<?> columnValues) {
+        int start = rowCount * columnCount;
+        int end = start + columnCount;
+        ensureCapacity(end);
+
+        if (columnValues instanceof ArrayList<?>) {
+            addRow((ArrayList<?>) columnValues, start);
+            return;
+        }
+
+        int current = start;
+        Object[] localData = data;
+        for (Object columnValue : columnValues) {
+            if (current == end) {
+                // TODO: null out row?
+                throw new IllegalArgumentException(
+                        "columnValues.size() > columnNames.length");
+            }
+            localData[current++] = columnValue;
+        }
+
+        if (current != end) {
+            // TODO: null out row?
+            throw new IllegalArgumentException(
+                    "columnValues.size() < columnNames.length");
+        }
+
+        // Increase row count here in case we encounter an exception.
+        rowCount++;
+    }
+
+    /** Optimization for {@link ArrayList}. */
+    private void addRow(ArrayList<?> columnValues, int start) {
+        int size = columnValues.size();
+        if (size != columnCount) {
+            throw new IllegalArgumentException("columnNames.length = "
+                    + columnCount + ", columnValues.size() = " + size);
+        }
+
+        rowCount++;
+        Object[] localData = data;
+        for (int i = 0; i < size; i++) {
+            localData[start + i] = columnValues.get(i);
+        }
+    }
+
+    /** Ensures that this cursor has enough capacity. */
+    private void ensureCapacity(int size) {
+        if (size > data.length) {
+            Object[] oldData = this.data;
+            int newSize = data.length * 2;
+            if (newSize < size) {
+                newSize = size;
+            }
+            this.data = new Object[newSize];
+            System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+        }
+    }
+
+    /**
+     * Builds a row, starting from the left-most column and adding one column
+     * value at a time. Follows the same ordering as the column names specified
+     * at cursor construction time.
+     */
+    public class RowBuilder {
+
+        private int index;
+        private final int endIndex;
+
+        RowBuilder(int index, int endIndex) {
+            this.index = index;
+            this.endIndex = endIndex;
+        }
+
+        /**
+         * Sets the next column value in this row.
+         *
+         * @throws CursorIndexOutOfBoundsException if you try to add too many
+         *  values
+         * @return this builder to support chaining
+         */
+        public RowBuilder add(Object columnValue) {
+            if (index == endIndex) {
+                throw new CursorIndexOutOfBoundsException(
+                        "No more columns left.");
+            }
+
+            data[index++] = columnValue;
+            return this;
+        }
+    }
+
+    // AbstractCursor implementation.
+
+    @Override
+    public int getCount() {
+        return rowCount;
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return columnNames;
+    }
+
+    @Override
+    public String getString(int column) {
+        Object value = get(column);
+        if (value == null) return null;
+        return value.toString();
+    }
+
+    @Override
+    public short getShort(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).shortValue();
+        return Short.parseShort(value.toString());
+    }
+
+    @Override
+    public int getInt(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).intValue();
+        return Integer.parseInt(value.toString());
+    }
+
+    @Override
+    public long getLong(int column) {
+        Object value = get(column);
+        if (value == null) return 0;
+        if (value instanceof Number) return ((Number) value).longValue();
+        return Long.parseLong(value.toString());
+    }
+
+    @Override
+    public float getFloat(int column) {
+        Object value = get(column);
+        if (value == null) return 0.0f;
+        if (value instanceof Number) return ((Number) value).floatValue();
+        return Float.parseFloat(value.toString());
+    }
+
+    @Override
+    public double getDouble(int column) {
+        Object value = get(column);
+        if (value == null) return 0.0d;
+        if (value instanceof Number) return ((Number) value).doubleValue();
+        return Double.parseDouble(value.toString());
+    }
+
+    @Override
+    public byte[] getBlob(int column) {
+        Object value = get(column);
+        if (value == null) return null;
+        if (value instanceof byte[]) {
+            return (byte[]) value;
+        }
+        if (value instanceof ByteBuffer) {
+            ByteBuffer data = (ByteBuffer)value;
+            byte[] byteArray = new byte[data.remaining()];
+            data.get(byteArray);
+            return byteArray;
+        }
+        throw new UnsupportedOperationException("BLOB Object not of known type");
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return get(column) == null;
+    }
+}
--- a/mobile/android/base/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/sqlite/SQLiteBridge.java
@@ -1,18 +1,18 @@
 /* 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.sqlite;
 
 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
+import org.mozilla.gecko.sqlite.MatrixBlobCursor;
 import android.content.ContentValues;
 import android.database.Cursor;
-import android.database.MatrixCursor;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -49,35 +49,35 @@ public class SQLiteBridge {
     // Takes the path to the database we want to access.
     public SQLiteBridge(String aDb) throws SQLiteBridgeException {
         mDb = aDb;
     }
 
     // Executes a simple line of sql.
     public void execSQL(String sql)
                 throws SQLiteBridgeException {
-        query(sql, null);
+        internalQuery(sql, null);
     }
 
     // Executes a simple line of sql. Allow you to bind arguments
     public void execSQL(String sql, String[] bindArgs)
                 throws SQLiteBridgeException {
-        query(sql, bindArgs);
+        internalQuery(sql, bindArgs);
     }
 
     // Executes a DELETE statement on the database
     public int delete(String table, String whereClause, String[] whereArgs)
                throws SQLiteBridgeException {
         StringBuilder sb = new StringBuilder("DELETE from ");
         sb.append(table);
         if (whereClause != null) {
             sb.append(" WHERE " + whereClause);
         }
 
-        query(sb.toString(), whereArgs);
+        internalQuery(sb.toString(), whereArgs);
         return mQueryResults[kResultRowsChanged].intValue();
     }
 
     public Cursor query(String table,
                         String[] columns,
                         String selection,
                         String[] selectionArgs,
                         String groupBy,
@@ -109,20 +109,26 @@ public class SQLiteBridge {
         if (orderBy != null) {
             sb.append(" ORDER BY " + orderBy);
         }
 
         if (limit != null) {
             sb.append(" " + limit);
         }
 
+        return rawQuery(sb.toString(), selectionArgs);
+    }
+
+    public Cursor rawQuery(String sql, String[] selectionArgs)
+        throws SQLiteBridgeException {
         ArrayList<Object[]> results;
-        results = query(sb.toString(), selectionArgs);
+        results = internalQuery(sql, selectionArgs);
 
-        MatrixCursor cursor = new MatrixCursor(mColumns.toArray(new String[0]));
+        MatrixBlobCursor cursor =
+            new MatrixBlobCursor(mColumns.toArray(new String[0]));
         try {
             for (Object resultRow: results) {
                 Object[] resultColumns = (Object[])resultRow;
                 if (resultColumns.length == mColumns.size())
                     cursor.addRow(resultColumns);
             }
         } catch(IllegalArgumentException ex) {
             Log.e(LOGTAG, "Error getting rows", ex);
@@ -155,17 +161,17 @@ public class SQLiteBridge {
 
         // XXX - Do we need to bind these values?
         sb.append(" VALUES (");
         sb.append(TextUtils.join(", ", valueNames));
         sb.append(") ");
 
         String[] binds = new String[valueBinds.size()];
         valueBinds.toArray(binds);
-        query(sb.toString(), binds);
+        internalQuery(sb.toString(), binds);
         return mQueryResults[kResultInsertRowId];
     }
 
     public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
                throws SQLiteBridgeException {
         Set<Entry<String, Object>> valueSet = values.valueSet();
         Iterator<Entry<String, Object>> valueIterator = valueSet.iterator();
         ArrayList<String> valueNames = new ArrayList<String>();
@@ -190,59 +196,48 @@ public class SQLiteBridge {
             for (int i = 0; i < whereArgs.length; i++) {
                 valueNames.add(whereArgs[i]);
             }
         }
 
         String[] binds = new String[valueNames.size()];
         valueNames.toArray(binds);
 
-        query(sb.toString(), binds);
+        internalQuery(sb.toString(), binds);
         return mQueryResults[kResultRowsChanged].intValue();
     }
 
     public int getVersion()
                throws SQLiteBridgeException {
         ArrayList<Object[]> results = null;
-        results = query("PRAGMA user_version");
+        results = internalQuery("PRAGMA user_version", null);
         int ret = -1;
         if (results != null) {
             for (Object resultRow: results) {
                 Object[] resultColumns = (Object[])resultRow;
                 String version = (String)resultColumns[0];
                 ret = Integer.parseInt(version);
             }
         }
         return ret;
     }
 
-    // Do an SQL query without parameters
-    public ArrayList<Object[]> query(String aQuery) throws SQLiteBridgeException {
-        return query(aQuery, null);
-    }
-
     // Do an SQL query, substituting the parameters in the query with the passed
     // parameters. The parameters are subsituded in order, so named parameters
     // are not supported.
     // The result is returned as an ArrayList<Object[]>, with each
     // row being an entry in the ArrayList, and each column being one Object
     // in the Object[] array. The columns are of type null,
     // direct ByteBuffer (BLOB), or String (everything else).
-    public ArrayList<Object[]> query(String aQuery, String[] aParams)
+    private ArrayList<Object[]> internalQuery(String aQuery, String[] aParams)
         throws SQLiteBridgeException {
         ArrayList<Object[]> result = new ArrayList<Object[]>();
         mQueryResults = new Long[2];
         mColumns = new ArrayList<String>();
 
         sqliteCall(mDb, aQuery, aParams, mColumns, mQueryResults, result);
 
         return result;
     }
 
-    // Gets the index in the row Object[] for the given column name.
-    // Returns -1 if not found.
-    public int getColumnIndex(String aColumnName) {
-        return mColumns.lastIndexOf(aColumnName);
-    }
-
     // nop, provided for API compatibility with SQLiteDatabase.
     public void close() { }
-}
\ No newline at end of file
+}