Bug 727264 - Update Profile Migration to use Cursors. r=blassey

This commit is contained in:
Gian-Carlo Pascutto 2012-02-27 12:28:22 +01:00
parent 8fc091bd34
commit 9cdd43ae4f
4 changed files with 403 additions and 86 deletions

View File

@ -96,6 +96,7 @@ FENNEC_JAVA_FILES = \
ProfileMigrator.java \
PromptService.java \
sqlite/ByteBufferInputStream.java \
sqlite/MatrixBlobCursor.java \
sqlite/SQLiteBridge.java \
sqlite/SQLiteBridgeException.java \
SetupScreen.java \

View File

@ -45,7 +45,6 @@ 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;
@ -62,10 +61,11 @@ 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;
@ -220,17 +220,20 @@ public class ProfileMigrator {
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;
@ -337,15 +340,15 @@ public class ProfileMigrator {
}
}
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) {
@ -373,9 +376,7 @@ public class ProfileMigrator {
} 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);
@ -412,26 +413,26 @@ public class ProfileMigrator {
*/
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);
@ -441,7 +442,9 @@ public class ProfileMigrator {
} 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;
@ -499,20 +502,20 @@ public class ProfileMigrator {
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>();
@ -535,33 +538,38 @@ public class ProfileMigrator {
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.
@ -590,6 +598,7 @@ public class ProfileMigrator {
openFolders.add(parent);
skipped++;
}
cursor.moveToNext();
}
// Now check if any of the new folders we added was a folder
@ -608,6 +617,8 @@ public class ProfileMigrator {
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;

View File

@ -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;
}
}

View File

@ -5,9 +5,9 @@
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;
@ -54,13 +54,13 @@ public class SQLiteBridge {
// 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
@ -72,7 +72,7 @@ public class SQLiteBridge {
sb.append(" WHERE " + whereClause);
}
query(sb.toString(), whereArgs);
internalQuery(sb.toString(), whereArgs);
return mQueryResults[kResultRowsChanged].intValue();
}
@ -114,10 +114,16 @@ public class SQLiteBridge {
sb.append(" " + limit);
}
ArrayList<Object[]> results;
results = query(sb.toString(), selectionArgs);
return rawQuery(sb.toString(), selectionArgs);
}
MatrixCursor cursor = new MatrixCursor(mColumns.toArray(new String[0]));
public Cursor rawQuery(String sql, String[] selectionArgs)
throws SQLiteBridgeException {
ArrayList<Object[]> results;
results = internalQuery(sql, selectionArgs);
MatrixBlobCursor cursor =
new MatrixBlobCursor(mColumns.toArray(new String[0]));
try {
for (Object resultRow: results) {
Object[] resultColumns = (Object[])resultRow;
@ -160,7 +166,7 @@ public class SQLiteBridge {
String[] binds = new String[valueBinds.size()];
valueBinds.toArray(binds);
query(sb.toString(), binds);
internalQuery(sb.toString(), binds);
return mQueryResults[kResultInsertRowId];
}
@ -195,14 +201,14 @@ public class SQLiteBridge {
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) {
@ -214,11 +220,6 @@ public class SQLiteBridge {
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.
@ -226,7 +227,7 @@ public class SQLiteBridge {
// 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];
@ -237,12 +238,6 @@ public class SQLiteBridge {
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() { }
}
}