blob: 6f84df5e363f58e84a38a0b798425276cf0fcbf5 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.contacts;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteTransactionListener;
import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
/**
* A common base class for the contacts and profile providers. This handles much of the same
* logic that SQLiteContentProvider does (i.e. starting transactions on the appropriate database),
* but exposes awareness of batch operations to the subclass so that cross-database operations
* can be supported.
*/
public abstract class AbstractContactsProvider extends ContentProvider
implements SQLiteTransactionListener {
public static final String TAG = "ContactsProvider";
public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
/** Set true to enable detailed transaction logging. */
public static final boolean ENABLE_TRANSACTION_LOG = false; // Don't submit with true.
/**
* Duration in ms to sleep after successfully yielding the lock during a batch operation.
*/
protected static final int SLEEP_AFTER_YIELD_DELAY = 4000;
/**
* Maximum number of operations allowed in a batch between yield points.
*/
private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
/**
* Number of inserts performed in bulk to allow before yielding the transaction.
*/
private static final int BULK_INSERTS_PER_YIELD_POINT = 50;
/**
* The contacts transaction that is active in this thread.
*/
private ThreadLocal<ContactsTransaction> mTransactionHolder;
/**
* The DB helper to use for this content provider.
*/
private SQLiteOpenHelper mDbHelper;
/**
* The database helper to serialize all transactions on. If non-null, any new transaction
* created by this provider will automatically retrieve a writable database from this helper
* and initiate a transaction on that database. This should be used to ensure that operations
* across multiple databases are all blocked on a single DB lock (to prevent deadlock cases).
*
* Hint: It's always {@link ContactsDatabaseHelper}.
*
* TODO Change the structure to make it obvious that it's actually always set, and is the
* {@link ContactsDatabaseHelper}.
*/
private SQLiteOpenHelper mSerializeOnDbHelper;
/**
* The tag corresponding to the database used for serializing transactions.
*
* Hint: It's always the contacts db helper tag.
*
* See also the TODO on {@link #mSerializeOnDbHelper}.
*/
private String mSerializeDbTag;
/**
* The transaction listener used with {@link #mSerializeOnDbHelper}.
*
* Hint: It's always {@link ContactsProvider2}.
*
* See also the TODO on {@link #mSerializeOnDbHelper}.
*/
private SQLiteTransactionListener mSerializedDbTransactionListener;
@Override
public boolean onCreate() {
Context context = getContext();
mDbHelper = getDatabaseHelper(context);
mTransactionHolder = getTransactionHolder();
return true;
}
public SQLiteOpenHelper getDatabaseHelper() {
return mDbHelper;
}
/**
* Specifies a database helper (and corresponding tag) to serialize all transactions on.
*
* See also the TODO on {@link #mSerializeOnDbHelper}.
*/
public void setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag,
SQLiteTransactionListener listener) {
mSerializeOnDbHelper = serializeOnDbHelper;
mSerializeDbTag = tag;
mSerializedDbTransactionListener = listener;
}
public ContactsTransaction getCurrentTransaction() {
return mTransactionHolder.get();
}
@Override
public Uri insert(Uri uri, ContentValues values) {
ContactsTransaction transaction = startTransaction(false);
try {
Uri result = insertInTransaction(uri, values);
if (result != null) {
transaction.markDirty();
}
transaction.markSuccessful(false);
return result;
} finally {
endTransaction(false);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
ContactsTransaction transaction = startTransaction(false);
try {
int deleted = deleteInTransaction(uri, selection, selectionArgs);
if (deleted > 0) {
transaction.markDirty();
}
transaction.markSuccessful(false);
return deleted;
} finally {
endTransaction(false);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
ContactsTransaction transaction = startTransaction(false);
try {
int updated = updateInTransaction(uri, values, selection, selectionArgs);
if (updated > 0) {
transaction.markDirty();
}
transaction.markSuccessful(false);
return updated;
} finally {
endTransaction(false);
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
ContactsTransaction transaction = startTransaction(true);
int numValues = values.length;
int opCount = 0;
try {
for (int i = 0; i < numValues; i++) {
insert(uri, values[i]);
if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
opCount = 0;
try {
yield(transaction);
} catch (RuntimeException re) {
transaction.markYieldFailed();
throw re;
}
}
}
transaction.markSuccessful(true);
} finally {
endTransaction(true);
}
return numValues;
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
if (VERBOSE_LOGGING) {
Log.v(TAG, "applyBatch: " + operations.size() + " ops");
}
int ypCount = 0;
int opCount = 0;
ContactsTransaction transaction = startTransaction(true);
try {
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
throw new OperationApplicationException(
"Too many content provider operations between yield points. "
+ "The maximum number of operations per yield point is "
+ MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
}
final ContentProviderOperation operation = operations.get(i);
if (i > 0 && operation.isYieldAllowed()) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
}
opCount = 0;
try {
if (yield(transaction)) {
ypCount++;
}
} catch (RuntimeException re) {
transaction.markYieldFailed();
throw re;
}
}
results[i] = operation.apply(this, results, i);
}
transaction.markSuccessful(true);
return results;
} finally {
endTransaction(true);
}
}
/**
* If we are not yet already in a transaction, this starts one (on the DB to serialize on, if
* present) and sets the thread-local transaction variable for tracking. If we are already in
* a transaction, this returns that transaction, and the batch parameter is ignored.
* @param callerIsBatch Whether the caller is operating in batch mode.
*/
private ContactsTransaction startTransaction(boolean callerIsBatch) {
if (ENABLE_TRANSACTION_LOG) {
Log.i(TAG, "startTransaction " + getClass().getSimpleName() +
" callerIsBatch=" + callerIsBatch, new RuntimeException("startTransaction"));
}
ContactsTransaction transaction = mTransactionHolder.get();
if (transaction == null) {
transaction = new ContactsTransaction(callerIsBatch);
if (mSerializeOnDbHelper != null) {
transaction.startTransactionForDb(mSerializeOnDbHelper.getWritableDatabase(),
mSerializeDbTag, mSerializedDbTransactionListener);
}
mTransactionHolder.set(transaction);
}
return transaction;
}
/**
* Ends the current transaction and clears out the member variable. This does not set the
* transaction as being successful.
* @param callerIsBatch Whether the caller is operating in batch mode.
*/
private void endTransaction(boolean callerIsBatch) {
if (ENABLE_TRANSACTION_LOG) {
Log.i(TAG, "endTransaction " + getClass().getSimpleName() +
" callerIsBatch=" + callerIsBatch, new RuntimeException("endTransaction"));
}
ContactsTransaction transaction = mTransactionHolder.get();
if (transaction != null && (!transaction.isBatch() || callerIsBatch)) {
boolean notify = false;
try {
if (transaction.isDirty()) {
notify = true;
}
transaction.finish(callerIsBatch);
if (notify) {
notifyChange();
}
} finally {
// No matter what, make sure we clear out the thread-local transaction reference.
mTransactionHolder.set(null);
}
}
}
/**
* Gets the database helper for this contacts provider. This is called once, during onCreate().
*/
protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
/**
* Gets the thread-local transaction holder to use for keeping track of the transaction. This
* is called once, in onCreate(). If multiple classes are inheriting from this class that need
* to be kept in sync on the same transaction, they must all return the same thread-local.
*/
protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder();
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);
protected abstract boolean yield(ContactsTransaction transaction);
protected abstract void notifyChange();
}