Back out 4 changesets (bug 986114) for 50% roboprovider bustage

CLOSED TREE

Backed out changeset cc87c22aab07 (bug 986114)
Backed out changeset c75b0b78ebbf (bug 986114)
Backed out changeset d9e1c755f554 (bug 986114)
Backed out changeset f1d1a28b0f5a (bug 986114)
This commit is contained in:
Phil Ringnalda 2014-03-22 21:34:30 -07:00
parent 08f929b520
commit 666e28991c
9 changed files with 555 additions and 402 deletions

View File

@ -1,79 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
/**
* The base class for ContentProviders that wish to use a different DB
* for each profile.
*
* This class has logic shared between ordinary per-profile CPs and
* those that wish to share DB connections between CPs.
*/
public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
/**
* Extend this to provide access to your own map of shared databases. This
* is a method so that your subclass doesn't collide with others!
*/
protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
/**
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writable SQLiteDatabase
*/
@Override
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
}

View File

@ -19,6 +19,7 @@ import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.sync.Utils;
import android.app.SearchManager;
@ -26,6 +27,7 @@ import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
@ -38,7 +40,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class BrowserProvider extends SharedBrowserDatabaseProvider {
public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoBrowserProvider";
// How many records to reposition in a single query.
@ -813,6 +815,21 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
return cursor;
}
private static int getUrlCount(SQLiteDatabase db, String table, String url) {
final Cursor c = db.query(table, new String[] { "COUNT(*)" },
URLColumns.URL + " = ?", new String[] { url },
null, null, null);
try {
if (c.moveToFirst()) {
return c.getInt(0);
}
} finally {
c.close();
}
return 0;
}
/**
* Update the positions of bookmarks in batches.
*
@ -1288,7 +1305,7 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
// it if we can.
final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
try {
cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
@ -1317,7 +1334,7 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
// require the transaction to be upgraded from a reader to a writer.
final int updated = updateBookmarks(uri, values, selection, selectionArgs);
try {
cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
@ -1444,4 +1461,15 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
return results;
}
@Override
protected BrowserDatabaseHelper createDatabaseHelper(
Context context, String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
@Override
protected String getDatabaseName() {
return BrowserDatabaseHelper.DATABASE_NAME;
}
}

View File

@ -1,50 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Abstract class containing methods needed to make a SQLite-based content
* provider with a database helper of type T, where one database helper is
* held per profile.
*/
public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
private PerProfileDatabases<T> databases;
@Override
protected PerProfileDatabases<T> getDatabases() {
return databases;
}
protected abstract String getDatabaseName();
/**
* Creates and returns an instance of the appropriate DB helper.
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
protected abstract T createDatabaseHelper(Context context, String databasePath);
@Override
public boolean onCreate() {
synchronized (this) {
databases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
return createDatabaseHelper(context, databasePath);
}
});
}
return true;
}
}

View File

@ -9,6 +9,7 @@ import org.mozilla.gecko.sync.Utils;
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;
@ -16,7 +17,9 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class ReadingListProvider extends SharedBrowserDatabaseProvider {
public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoReadingListProv";
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
static final int ITEMS = 101;
@ -100,7 +103,7 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
ContentValues values = new ContentValues();
values.put(ReadingListItems.IS_DELETED, 1);
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
return updateItems(uri, values, selection, selectionArgs);
}
@ -244,4 +247,15 @@ public class ReadingListProvider extends SharedBrowserDatabaseProvider {
debug("URI has unrecognized type: " + uri);
return null;
}
@Override
protected BrowserDatabaseHelper createDatabaseHelper(Context context,
String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
@Override
protected String getDatabaseName() {
return BrowserDatabaseHelper.DATABASE_NAME;
}
}

View File

@ -1,115 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
/**
* A ContentProvider subclass that provides per-profile browser.db access
* that can be safely shared between multiple providers.
*
* If multiple ContentProvider classes wish to share a database, it's
* vitally important that they use the same SQLiteOpenHelpers for access.
*
* Failure to do so can cause accidental concurrent writes, with the result
* being unexpected SQLITE_BUSY errors.
*
* This class provides a static {@link PerProfileDatabases} instance, lazily
* initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
*/
public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
private static PerProfileDatabases<BrowserDatabaseHelper> databases;
@Override
protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
return databases;
}
@Override
public boolean onCreate() {
// If necessary, do the shared DB work.
synchronized (SharedBrowserDatabaseProvider.class) {
if (databases != null) {
return true;
}
final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
@Override
public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new BrowserDatabaseHelper(context, databasePath);
}
};
databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
}
return true;
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
}

View File

@ -4,25 +4,37 @@
package org.mozilla.gecko.db;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract.Clients;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
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.SQLException;
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 TabsProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTabsProvider";
private Context mContext;
private PerProfileDatabases<TabsDatabaseHelper> mDatabases;
public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDatabaseHelper> {
static final String DATABASE_NAME = "tabs.db";
static final int DATABASE_VERSION = 2;
@ -75,10 +87,35 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private static final String selectColumn(String table, String column) {
static final String selectColumn(String table, String column) {
return table + "." + column + " = ?";
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
final class TabsDatabaseHelper extends SQLiteOpenHelper {
public TabsDatabaseHelper(Context context, String databasePath) {
super(context, databasePath, null, DATABASE_VERSION);
@ -91,34 +128,35 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
// Table for each tab on any client.
db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Tabs.CLIENT_GUID + " TEXT," +
Tabs.TITLE + " TEXT," +
Tabs.URL + " TEXT," +
Tabs.HISTORY + " TEXT," +
Tabs.FAVICON + " TEXT," +
Tabs.LAST_USED + " INTEGER," +
Tabs.POSITION + " INTEGER" +
");");
Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Tabs.CLIENT_GUID + " TEXT," +
Tabs.TITLE + " TEXT," +
Tabs.URL + " TEXT," +
Tabs.HISTORY + " TEXT," +
Tabs.FAVICON + " TEXT," +
Tabs.LAST_USED + " INTEGER," +
Tabs.POSITION + " INTEGER" +
");");
// Indices on CLIENT_GUID and POSITION.
db.execSQL("CREATE INDEX " + INDEX_TABS_GUID +
" ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION +
" ON " + TABLE_TABS + "(" + Tabs.POSITION + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_GUID + " ON " + TABLE_TABS + "("
+ Tabs.CLIENT_GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION + " ON " + TABLE_TABS + "("
+ Tabs.POSITION + ")");
debug("Creating " + TABLE_CLIENTS + " table");
// Table for client's name-guid mapping.
db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
Clients.GUID + " TEXT PRIMARY KEY," +
Clients.NAME + " TEXT," +
Clients.LAST_MODIFIED + " INTEGER" +
");");
Clients.GUID + " TEXT PRIMARY KEY," +
Clients.NAME + " TEXT," +
Clients.LAST_MODIFIED + " INTEGER" +
");");
// Index on GUID.
db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID +
" ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")");
db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID + " ON " + TABLE_CLIENTS + "("
+ Clients.GUID + ")");
createLocalClient(db);
}
@ -135,7 +173,7 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
debug("Upgrading tabs.db: " + db.getPath() + " from " +
oldVersion + " to " + newVersion);
oldVersion + " to " + newVersion);
// We have to do incremental upgrades until we reach the current
// database schema version.
@ -151,20 +189,73 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
@Override
public void onOpen(SQLiteDatabase db) {
debug("Opening tabs.db: " + db.getPath());
db.rawQuery("PRAGMA synchronous=OFF", null).close();
if (shouldUseTransactions()) {
db.enableWriteAheadLogging();
db.setLockingEnabled(false);
return;
Cursor cursor = null;
try {
cursor = db.rawQuery("PRAGMA synchronous=OFF", null);
} finally {
if (cursor != null)
cursor.close();
}
// If we're not using transactions (in particular, prior to
// Honeycomb), then we can do some lesser optimizations.
db.rawQuery("PRAGMA journal_mode=PERSIST", null).close();
// From Honeycomb on, it's possible to run several db
// commands in parallel using multiple connections.
if (Build.VERSION.SDK_INT >= 11) {
db.enableWriteAheadLogging();
db.setLockingEnabled(false);
} else {
// Pre-Honeycomb, we can do some lesser optimizations.
cursor = null;
try {
cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
} finally {
if (cursor != null)
cursor.close();
}
}
}
}
private SQLiteDatabase getReadableDatabase(Uri uri) {
trace("Getting readable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return mDatabases.getDatabaseHelperForProfile(profile).getReadableDatabase();
}
private SQLiteDatabase getWritableDatabase(Uri uri) {
trace("Getting writable database for URI: " + uri);
String profile = null;
if (uri != null)
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
return mDatabases.getDatabaseHelperForProfile(profile).getWritableDatabase();
}
@Override
public boolean onCreate() {
debug("Creating TabsProvider");
synchronized (this) {
mContext = getContext();
mDatabases = new PerProfileDatabases<TabsDatabaseHelper>(
getContext(), DATABASE_NAME, new DatabaseHelperFactory<TabsDatabaseHelper>() {
@Override
public TabsDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
return new TabsDatabaseHelper(context, databasePath);
}
});
}
return true;
}
@Override
public String getType(Uri uri) {
final int match = URI_MATCHER.match(uri);
@ -194,6 +285,35 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning delete transaction: " + uri);
db.beginTransaction();
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful delete transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
deleted = deleteInTransaction(uri, selection, selectionArgs);
}
if (deleted > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return deleted;
}
@SuppressWarnings("fallthrough")
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
@ -235,6 +355,35 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning insert transaction: " + uri);
db.beginTransaction();
try {
result = insertInTransaction(uri, values);
db.setTransactionSuccessful();
trace("Successful insert transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
result = insertInTransaction(uri, values);
}
if (result != null) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return result;
}
public Uri insertInTransaction(Uri uri, ContentValues values) {
trace("Calling insert in transaction on URI: " + uri);
@ -267,7 +416,38 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
return null;
}
public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning update transaction: " + uri);
db.beginTransaction();
try {
updated = updateInTransaction(uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful update transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
updated = updateInTransaction(uri, values, selection, selectionArgs);
}
if (updated > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return updated;
}
public int updateInTransaction(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update in transaction on URI: " + uri);
int match = URI_MATCHER.match(uri);
@ -357,8 +537,10 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
}
trace("Running built query.");
final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(),
BrowserContract.TABS_AUTHORITY_URI);
return cursor;
}
@ -367,7 +549,7 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
trace("Updating tabs on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.update(table, values, selection, selectionArgs);
}
@ -375,17 +557,46 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
debug("Deleting tabs for URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.delete(table, selection, selectionArgs);
}
@Override
protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) {
return new TabsDatabaseHelper(context, databasePath);
}
public int bulkInsert(Uri uri, ContentValues[] values) {
if (values == null)
return 0;
@Override
protected String getDatabaseName() {
return DATABASE_NAME;
int numValues = values.length;
int successes = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
db.beginTransaction();
try {
for (int i = 0; i < numValues; i++) {
try {
insertInTransaction(uri, values[i]);
successes++;
} catch (SQLException e) {
Log.e(LOGTAG, "SQLException in bulkInsert", e);
// Restart the transaction to continue insertions.
db.setTransactionSuccessful();
db.endTransaction();
db.beginTransaction();
}
}
trace("Flushing DB bulkinsert...");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
}

View File

@ -1,62 +1,199 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
/**
* This abstract class exists to capture some of the transaction-handling
* commonalities in Fennec's DB layer.
*
* In particular, this abstracts DB access, batching, and a particular
* transaction approach.
*
* That approach is: subclasses implement the abstract methods
* {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
* {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
* {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
*
* These are all called expecting a transaction to be established, so failed
* modifications can be rolled-back, and work batched.
*
* If no transaction is established, that's not a problem. Transaction nesting
* can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
*
* The decision of when to begin a transaction is left to the subclasses,
* primarily to avoid the pattern of a transaction being begun, a read occurring,
* and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
* which we don't handle well. Better to avoid starting a transaction too soon!
*
* You are probably interested in some subclasses:
*
* * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
* querying databases that are stored in the user's profile directory.
* * {@link PerProfileDatabaseProvider} is a simple version that only allows a
* single ContentProvider to access each per-profile database.
* * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
* that allows for multiple providers to safely work with the same databases.
/*
* Abstract class containing methods needed to make a SQLite-based content provider with a
* database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
* updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
*/
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
protected Context mContext;
protected PerProfileDatabases<T> mDatabases;
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
/*
* Returns the name of the database file. Used to get a path
* to the DB file.
*
* @return name of the database file
*/
abstract protected String getDatabaseName();
protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
/*
* Creates and returns an instance of a DB helper. Given a
* context and a path to the DB file
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
abstract protected T createDatabaseHelper(Context context, String databasePath);
public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
/*
* Inserts an item into the database within a DB transaction.
*
* @param uri query URI
* @param values column values to be inserted
* @return a URI for the newly inserted item
*/
abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/*
* Deletes items from the database within a DB transaction.
*
* @param uri Query URI.
* @param selection An optional filter to match rows to delete.
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the deletion.
*/
abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
/*
* Updates the database within a DB transaction.
*
* @param uri Query URI.
* @param values A set of column_name/value pairs to add to the database.
* @param selection An optional filter to match rows to update.
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the update.
*/
abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writeable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writeable SQLiteDatabase
*/
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
/**
* Public version of {@link #getWritableDatabase(Uri) getWritableDatabase}.
* This method should ONLY be used for testing purposes.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writeable SQLiteDatabase
*/
@RobocopTarget
public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
return getWritableDatabase(uri);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
public static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
public static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
public static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
public static boolean isTest(Uri uri) {
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
@Override
public boolean onCreate() {
synchronized (this) {
mContext = getContext();
mDatabases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
return createDatabaseHelper(context, databasePath);
}
});
}
return true;
}
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
/**
* Track whether we're in a batch operation.
@ -85,29 +222,6 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
*/
final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
private boolean isInBatch() {
final Boolean isInBatch = isInBatchOperation.get();
if (isInBatch == null) {
@ -151,7 +265,7 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
* @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
@ -187,6 +301,23 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
isInBatchOperation.set(Boolean.FALSE);
}
/*
* This utility is replicated from RepoUtils, which is managed by android-sync.
*/
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
@ -254,8 +385,10 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
@ -305,53 +438,71 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
protected static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
protected static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
protected static boolean isTest(Uri uri) {
if (uri == null) {
return false;
// we cleanup records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// Maximum age of deleted records to be cleaned up (20 days in ms)
final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
// Number of records marked as deleted to be removed
final long DELETED_RECORDS_PURGE_LIMIT = 5;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
cursor.close();
}
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
protected static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
@ -363,4 +514,4 @@ public abstract class AbstractTransactionalProvider extends ContentProvider {
Log.d(LOGTAG, message);
}
}
}
}

View File

@ -117,8 +117,6 @@ gbjar.sources += [
'ContextGetter.java',
'CustomEditText.java',
'DataReportingNotification.java',
'db/AbstractPerProfileDatabaseProvider.java',
'db/AbstractTransactionalProvider.java',
'db/BrowserContract.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
@ -128,12 +126,11 @@ gbjar.sources += [
'db/HomeProvider.java',
'db/LocalBrowserDB.java',
'db/PasswordsProvider.java',
'db/PerProfileDatabaseProvider.java',
'db/PerProfileDatabases.java',
'db/ReadingListProvider.java',
'db/SharedBrowserDatabaseProvider.java',
'db/SQLiteBridgeContentProvider.java',
'db/TabsProvider.java',
'db/TransactionalProvider.java',
'Distribution.java',
'DoorHangerPopup.java',
'DynamicToolbar.java',

View File

@ -1,7 +1,3 @@
/* 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.tests;
import java.util.HashSet;
@ -11,6 +7,7 @@ import java.util.concurrent.Callable;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.ReadingListProvider;
import org.mozilla.gecko.db.TransactionalProvider;
import android.content.ContentProvider;
import android.content.ContentUris;
@ -71,13 +68,12 @@ public class testReadingListProvider extends ContentProviderTest {
}
}
public void testReadingListProviderTests() throws Exception {
public void testReadingListProvider() throws Exception {
for (Runnable test : mTests) {
setTestName(test.getClass().getSimpleName());
ensureEmptyDatabase();
test.run();
}
// Ensure browser initialization is complete before completing test,
// so that the minidumps directory is consistently created.
blockForGeckoReady();