Bug 986114 - Part 1: ReadingListProvider and BrowserProvider should share DB accessors. r=nalexander

* * *
Bug 986114 - Follow-up: Fix bustage on a CLOSED TREE.
This commit is contained in:
Richard Newman 2014-03-21 16:00:38 -07:00
parent b8943468be
commit 3dabb9ceb8
7 changed files with 360 additions and 302 deletions

View File

@ -0,0 +1,79 @@
/* 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

@ -1,199 +1,66 @@
/* 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/. */
* 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;
/*
* 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.
/**
* 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.
*/
public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
protected Context mContext;
protected PerProfileDatabases<T> mDatabases;
/*
* 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();
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
/*
* 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);
protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
protected abstract SQLiteDatabase getWritableDatabase(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);
public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
/*
* 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;
}
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);
/**
* Track whether we're in a batch operation.
@ -222,6 +89,29 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
*/
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) {
@ -265,7 +155,7 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
* @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
@ -301,23 +191,6 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
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
@ -385,10 +258,8 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
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);
@ -438,71 +309,53 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
/**
* 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.
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, 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);
protected 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
*/
protected 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
*/
protected static boolean isTest(Uri uri) {
if (uri == null) {
return false;
}
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);
}
// 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);
@ -514,4 +367,4 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
Log.d(LOGTAG, message);
}
}
}
}

View File

@ -19,7 +19,6 @@ 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;
@ -27,7 +26,6 @@ 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;
@ -40,7 +38,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
public class BrowserProvider extends SharedBrowserDatabaseProvider {
private static final String LOGTAG = "GeckoBrowserProvider";
// How many records to reposition in a single query.
@ -815,21 +813,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
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.
*
@ -1305,7 +1288,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// it if we can.
final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
@ -1334,7 +1317,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// require the transaction to be upgraded from a reader to a writer.
final int updated = updateBookmarks(uri, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
@ -1461,15 +1444,4 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
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

@ -0,0 +1,50 @@
/* 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,7 +9,6 @@ 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;
@ -17,9 +16,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
private static final String LOGTAG = "GeckoReadingListProv";
public class ReadingListProvider extends SharedBrowserDatabaseProvider {
static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
static final int ITEMS = 101;
@ -103,7 +100,7 @@ public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHe
ContentValues values = new ContentValues();
values.put(ReadingListItems.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
return updateItems(uri, values, selection, selectionArgs);
}
@ -247,15 +244,4 @@ public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHe
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

@ -0,0 +1,115 @@
/* 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

@ -117,6 +117,8 @@ gbjar.sources += [
'ContextGetter.java',
'CustomEditText.java',
'DataReportingNotification.java',
'db/AbstractPerProfileDatabaseProvider.java',
'db/AbstractTransactionalProvider.java',
'db/BrowserContract.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
@ -126,11 +128,12 @@ 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',