mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-15 14:25:52 +00:00
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:
parent
b8943468be
commit
3dabb9ceb8
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
50
mobile/android/base/db/PerProfileDatabaseProvider.java
Normal file
50
mobile/android/base/db/PerProfileDatabaseProvider.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
115
mobile/android/base/db/SharedBrowserDatabaseProvider.java
Normal file
115
mobile/android/base/db/SharedBrowserDatabaseProvider.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user