blob: e00bd862f924c8ca36c895dc9e2393e63fcdc3ad [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.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteTransactionListener;
import android.net.Uri;
import android.os.Binder;
import android.os.SystemClock;
import android.provider.BaseColumns;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import com.android.internal.util.ProviderAccessStats;
import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import java.io.PrintWriter;
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 ContactsDatabaseHelper 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;
protected final ProviderAccessStats mStats = new ProviderAccessStats();
@Override
public boolean onCreate() {
Context context = getContext();
mDbHelper = newDatabaseHelper(context);
mTransactionHolder = getTransactionHolder();
return true;
}
public ContactsDatabaseHelper 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();
}
private boolean isInBatch() {
final ContactsTransaction t = mTransactionHolder.get();
return t != null && t.isBatch();
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final int callingUid = Binder.getCallingUid();
mStats.incrementInsertStats(callingUid, isInBatch());
try {
ContactsTransaction transaction = startTransaction(false);
try {
Uri result = insertInTransaction(uri, values);
if (result != null) {
transaction.markDirty();
}
transaction.markSuccessful(false);
return result;
} finally {
endTransaction(false);
}
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final int callingUid = Binder.getCallingUid();
mStats.incrementDeleteStats(callingUid, isInBatch());
try {
ContactsTransaction transaction = startTransaction(false);
try {
int deleted = deleteInTransaction(uri, selection, selectionArgs);
if (deleted > 0) {
transaction.markDirty();
}
transaction.markSuccessful(false);
return deleted;
} finally {
endTransaction(false);
}
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final int callingUid = Binder.getCallingUid();
mStats.incrementUpdateStats(callingUid, isInBatch());
try {
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);
}
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
final int callingUid = Binder.getCallingUid();
mStats.incrementBatchStats(callingUid);
try {
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;
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
final int callingUid = Binder.getCallingUid();
mStats.incrementBatchStats(callingUid);
try {
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);
}
} finally {
mStats.finishOperation(callingUid);
}
}
/**
* 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().
* Do not call in other places.
*/
protected abstract ContactsDatabaseHelper newDatabaseHelper(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();
private static final String ACCOUNTS_QUERY =
"SELECT * FROM " + Tables.ACCOUNTS + " ORDER BY " + BaseColumns._ID;
private static final String NUM_INVISIBLE_CONTACTS_QUERY =
"SELECT count(*) FROM " + Tables.CONTACTS;
private static final String NUM_VISIBLE_CONTACTS_QUERY =
"SELECT count(*) FROM " + Tables.DEFAULT_DIRECTORY;
private static final String NUM_RAW_CONTACTS_PER_CONTACT =
"SELECT _id, count(*) as c FROM " + Tables.RAW_CONTACTS
+ " GROUP BY " + RawContacts.CONTACT_ID;
private static final String MAX_RAW_CONTACTS_PER_CONTACT =
"SELECT max(c) FROM (" + NUM_RAW_CONTACTS_PER_CONTACT + ")";
private static final String AVG_RAW_CONTACTS_PER_CONTACT =
"SELECT avg(c) FROM (" + NUM_RAW_CONTACTS_PER_CONTACT + ")";
private static final String NUM_RAW_CONTACT_PER_ACCOUNT_PER_CONTACT =
"SELECT " + RawContactsColumns.ACCOUNT_ID + " AS aid"
+ ", " + RawContacts.CONTACT_ID + " AS cid"
+ ", count(*) AS c"
+ " FROM " + Tables.RAW_CONTACTS
+ " GROUP BY aid, cid";
private static final String RAW_CONTACTS_PER_ACCOUNT_PER_CONTACT =
"SELECT aid, sum(c) AS s, max(c) AS m, avg(c) AS a"
+ " FROM (" + NUM_RAW_CONTACT_PER_ACCOUNT_PER_CONTACT + ")"
+ " GROUP BY aid";
private static final String DATA_WITH_ACCOUNT =
"SELECT d._id AS did"
+ ", d." + Data.RAW_CONTACT_ID + " AS rid"
+ ", r." + RawContactsColumns.ACCOUNT_ID + " AS aid"
+ " FROM " + Tables.DATA + " AS d JOIN " + Tables.RAW_CONTACTS + " AS r"
+ " ON d." + Data.RAW_CONTACT_ID + "=r._id";
private static final String NUM_DATA_PER_ACCOUNT_PER_RAW_CONTACT =
"SELECT aid, rid, count(*) AS c"
+ " FROM (" + DATA_WITH_ACCOUNT + ")"
+ " GROUP BY aid, rid";
private static final String DATA_PER_ACCOUNT_PER_RAW_CONTACT =
"SELECT aid, sum(c) AS s, max(c) AS m, avg(c) AS a"
+ " FROM (" + NUM_DATA_PER_ACCOUNT_PER_RAW_CONTACT + ")"
+ " GROUP BY aid";
protected void dump(PrintWriter pw, String dbName) {
pw.print("Database: ");
pw.println(dbName);
mStats.dump(pw, " ");
if (mDbHelper == null) {
pw.println("mDbHelper is null");
return;
}
try {
pw.println();
pw.println(" Accounts:");
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
try (Cursor c = db.rawQuery(ACCOUNTS_QUERY, null)) {
c.moveToPosition(-1);
while (c.moveToNext()) {
pw.print(" ");
dumpLongColumn(pw, c, BaseColumns._ID);
pw.print(" ");
dumpStringColumn(pw, c, AccountsColumns.ACCOUNT_NAME);
pw.print(" ");
dumpStringColumn(pw, c, AccountsColumns.ACCOUNT_TYPE);
pw.print(" ");
dumpStringColumn(pw, c, AccountsColumns.DATA_SET);
pw.println();
}
}
pw.println();
pw.println(" Contacts:");
pw.print(" # of visible: ");
pw.print(longForQuery(db, NUM_VISIBLE_CONTACTS_QUERY));
pw.println();
pw.print(" # of invisible: ");
pw.print(longForQuery(db, NUM_INVISIBLE_CONTACTS_QUERY));
pw.println();
pw.print(" Max # of raw contacts: ");
pw.print(longForQuery(db, MAX_RAW_CONTACTS_PER_CONTACT));
pw.println();
pw.print(" Avg # of raw contacts: ");
pw.print(doubleForQuery(db, AVG_RAW_CONTACTS_PER_CONTACT));
pw.println();
pw.println();
pw.println(" Raw contacts (per account):");
try (Cursor c = db.rawQuery(RAW_CONTACTS_PER_ACCOUNT_PER_CONTACT, null)) {
c.moveToPosition(-1);
while (c.moveToNext()) {
pw.print(" ");
dumpLongColumn(pw, c, "aid");
pw.print(" total # of raw contacts: ");
dumpStringColumn(pw, c, "s");
pw.print(", max # per contact: ");
dumpLongColumn(pw, c, "m");
pw.print(", avg # per contact: ");
dumpDoubleColumn(pw, c, "a");
pw.println();
}
}
pw.println();
pw.println(" Data (per account):");
try (Cursor c = db.rawQuery(DATA_PER_ACCOUNT_PER_RAW_CONTACT, null)) {
c.moveToPosition(-1);
while (c.moveToNext()) {
pw.print(" ");
dumpLongColumn(pw, c, "aid");
pw.print(" total # of data:");
dumpLongColumn(pw, c, "s");
pw.print(", max # per raw contact: ");
dumpLongColumn(pw, c, "m");
pw.print(", avg # per raw contact: ");
dumpDoubleColumn(pw, c, "a");
pw.println();
}
}
} catch (Exception e) {
pw.println("Error: " + e);
}
}
private static void dumpStringColumn(PrintWriter pw, Cursor c, String column) {
final int index = c.getColumnIndex(column);
if (index == -1) {
pw.println("Column not found: " + column);
return;
}
final String value = c.getString(index);
if (value == null) {
pw.print("(null)");
} else if (value.length() == 0) {
pw.print("\"\"");
} else {
pw.print(value);
}
}
private static void dumpLongColumn(PrintWriter pw, Cursor c, String column) {
final int index = c.getColumnIndex(column);
if (index == -1) {
pw.println("Column not found: " + column);
return;
}
if (c.isNull(index)) {
pw.print("(null)");
} else {
pw.print(c.getLong(index));
}
}
private static void dumpDoubleColumn(PrintWriter pw, Cursor c, String column) {
final int index = c.getColumnIndex(column);
if (index == -1) {
pw.println("Column not found: " + column);
return;
}
if (c.isNull(index)) {
pw.print("(null)");
} else {
pw.print(c.getDouble(index));
}
}
private static long longForQuery(SQLiteDatabase db, String query) {
return DatabaseUtils.longForQuery(db, query, null);
}
private static double doubleForQuery(SQLiteDatabase db, String query) {
try (final Cursor c = db.rawQuery(query, null)) {
if (!c.moveToFirst()) {
return -1;
}
return c.getDouble(0);
}
}
}