Proper transaction handling in ContactsProvider2
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 2be5073..8a22e22 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -284,24 +284,6 @@
     }
 
     /**
-     * Synchronously aggregate the specified contact.
-     */
-    public void aggregateContact(long rawContactId) {
-        if (!mEnabled) {
-            return;
-        }
-
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            aggregateContact(db, rawContactId);
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    /**
      * Synchronously aggregate the specified contact assuming an open transaction.
      */
     public void aggregateContact(SQLiteDatabase db, long rawContactId) {
@@ -324,13 +306,11 @@
      *         {@link RawContacts#AGGREGATION_MODE_IMMEDIATE} or
      *         {@link RawContacts#AGGREGATION_MODE_DISABLED}.
      */
-    public int markContactForAggregation(long rawContactId) {
+    public int markContactForAggregation(SQLiteDatabase db, long rawContactId) {
         if (!mEnabled) {
             return RawContacts.AGGREGATION_MODE_DISABLED;
         }
 
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         int aggregationMode = mOpenHelper.getAggregationMode(rawContactId);
         if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
             return aggregationMode;
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 9a9e9e2..85767bc 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -33,15 +33,11 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.SearchManager;
-import android.content.ContentProvider;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Entity;
 import android.content.EntityIterator;
-import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.UriMatcher;
 import android.content.SharedPreferences.Editor;
@@ -84,7 +80,7 @@
  * Contacts content provider. The contract between this provider and applications
  * is defined in {@link ContactsContract}.
  */
-public class ContactsProvider2 extends ContentProvider {
+public class ContactsProvider2 extends SQLiteContentProvider {
 
     // TODO: clean up debug tag and rename this class
     private static final String TAG = "ContactsProvider ~~~~";
@@ -1089,6 +1085,8 @@
 
     private boolean mImportMode;
 
+    private boolean mScheduleAggregation;
+
     public ContactsProvider2() {
         this(new ContactAggregationScheduler());
     }
@@ -1102,9 +1100,10 @@
 
     @Override
     public boolean onCreate() {
-        final Context context = getContext();
+        super.onCreate();
 
-        mOpenHelper = getOpenHelper(context);
+        final Context context = getContext();
+        mOpenHelper = (OpenHelper)getOpenHelper();
         mGlobalSearchSupport = new GlobalSearchSupport(this);
         mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this, mGlobalSearchSupport);
         mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
@@ -1155,6 +1154,7 @@
     }
 
     /* Visible for testing */
+    @Override
     protected OpenHelper getOpenHelper(final Context context) {
         return OpenHelper.getInstance(context);
     }
@@ -1213,18 +1213,13 @@
         mOpenHelper.wipeData();
     }
 
-    /**
-     * Called when a change has been made.
-     *
-     * @param uri the uri that the change was made to
-     */
-    private void onChange(Uri uri) {
-        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
-    }
-
     @Override
-    public boolean isTemporary() {
-        return false;
+    protected void onTransactionComplete() {
+        if (mScheduleAggregation) {
+            mScheduleAggregation = false;
+            mContactAggregator.schedule();
+        }
+        super.onTransactionComplete();
     }
 
     private DataRowHandler getDataRowHandler(final String mimeType) {
@@ -1237,13 +1232,13 @@
     }
 
     @Override
-    public Uri insert(Uri uri, ContentValues values) {
+    protected Uri insertInTransaction(Uri uri, ContentValues values) {
         final int match = sUriMatcher.match(uri);
         long id = 0;
 
         switch (match) {
             case SYNCSTATE:
-                id = mOpenHelper.getSyncState().insert(mOpenHelper.getWritableDatabase(), values);
+                id = mOpenHelper.getSyncState().insert(mDb, values);
                 break;
 
             case CONTACTS: {
@@ -1287,9 +1282,7 @@
             return null;
         }
 
-        final Uri result = ContentUris.withAppendedId(uri, id);
-        onChange(result);
-        return result;
+        return ContentUris.withAppendedId(uri, id);
     }
 
     /**
@@ -1324,7 +1317,7 @@
      * @return the row ID of the newly created row
      */
     private long insertContact(ContentValues values) {
-        throw new UnsupportedOperationException("Aggregates are created automatically");
+        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
     }
 
     /**
@@ -1340,8 +1333,6 @@
          * be processed by the aggregator before it will be returned by the
          * "aggregates" queries.
          */
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         ContentValues overriddenValues = new ContentValues(values);
         overriddenValues.putNull(RawContacts.CONTACT_ID);
         if (!resolveAccount(overriddenValues, account)) {
@@ -1354,7 +1345,7 @@
                     RawContacts.AGGREGATION_MODE_DISABLED);
         }
 
-        return db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
+        return mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
     }
 
     /**
@@ -1365,46 +1356,37 @@
      */
     private long insertData(ContentValues values, boolean markRawContactAsDirty) {
         int aggregationMode = RawContacts.AGGREGATION_MODE_DISABLED;
-
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         long id = 0;
-        db.beginTransaction();
-        try {
-            mValues.clear();
-            mValues.putAll(values);
+        mValues.clear();
+        mValues.putAll(values);
 
-            long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
+        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
 
-            // Replace package with internal mapping
-            final String packageName = mValues.getAsString(Data.RES_PACKAGE);
-            if (packageName != null) {
-                mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
-            }
-            mValues.remove(Data.RES_PACKAGE);
-
-            // Replace mimetype with internal mapping
-            final String mimeType = mValues.getAsString(Data.MIMETYPE);
-            if (TextUtils.isEmpty(mimeType)) {
-                throw new IllegalArgumentException(Data.MIMETYPE + " is required");
-            }
-
-            mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
-            mValues.remove(Data.MIMETYPE);
-
-            // TODO create GroupMembershipRowHandler and move this code there
-            resolveGroupSourceIdInValues(rawContactId, mimeType, db, mValues, true /* isInsert */);
-
-            id = getDataRowHandler(mimeType).insert(db, rawContactId, mValues);
-            if (markRawContactAsDirty) {
-                setRawContactDirty(rawContactId);
-            }
-
-            aggregationMode = mContactAggregator.markContactForAggregation(rawContactId);
-
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
+        // Replace package with internal mapping
+        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
+        if (packageName != null) {
+            mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
         }
+        mValues.remove(Data.RES_PACKAGE);
+
+        // Replace mimetype with internal mapping
+        final String mimeType = mValues.getAsString(Data.MIMETYPE);
+        if (TextUtils.isEmpty(mimeType)) {
+            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
+        }
+
+        mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
+        mValues.remove(Data.MIMETYPE);
+
+        // TODO create GroupMembershipRowHandler and move this code there
+        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, true /* isInsert */);
+
+        id = getDataRowHandler(mimeType).insert(mDb, rawContactId, mValues);
+        if (markRawContactAsDirty) {
+            setRawContactDirty(rawContactId);
+        }
+
+        aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
 
         triggerAggregation(id, aggregationMode);
         return id;
@@ -1413,11 +1395,11 @@
     private void triggerAggregation(long rawContactId, int aggregationMode) {
         switch (aggregationMode) {
             case RawContacts.AGGREGATION_MODE_DEFAULT:
-                mContactAggregator.schedule();
+                mScheduleAggregation = true;
                 break;
 
             case RawContacts.AGGREGATION_MODE_IMMEDITATE:
-                mContactAggregator.aggregateContact(rawContactId);
+                mContactAggregator.aggregateContact(mDb, rawContactId);
                 break;
 
             case RawContacts.AGGREGATION_MODE_DISABLED:
@@ -1487,25 +1469,18 @@
      */
     private int deleteData(String selection, String[] selectionArgs,
             boolean markRawContactAsDirty) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count = 0;
-        db.beginTransaction();
-        try {
 
-            // Note that the query will return data according to the access restrictions,
-            // so we don't need to worry about deleting data we don't have permission to read.
-            Cursor c = query(Data.CONTENT_URI, DataIdQuery.COLUMNS, selection, selectionArgs, null);
-            try {
-                while(c.moveToNext()) {
-                    long dataId = c.getLong(DataIdQuery._ID);
-                    count += deleteData(dataId, markRawContactAsDirty);
-                }
-            } finally {
-                c.close();
+        // Note that the query will return data according to the access restrictions,
+        // so we don't need to worry about deleting data we don't have permission to read.
+        Cursor c = query(Data.CONTENT_URI, DataIdQuery.COLUMNS, selection, selectionArgs, null);
+        try {
+            while(c.moveToNext()) {
+                long dataId = c.getLong(DataIdQuery._ID);
+                count += deleteData(dataId, markRawContactAsDirty);
             }
-            db.setTransactionSuccessful();
         } finally {
-            db.endTransaction();
+            c.close();
         }
 
         return count;
@@ -1549,7 +1524,6 @@
     private int deleteData(long dataId, boolean markRawContactAsDirty) {
 
         // TODO redo this method with the use of DataRowHandlers
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
         final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
@@ -1566,7 +1540,7 @@
         // TODO check security
         Cursor cursor = null;
         try {
-            cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
+            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
                     DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
             if (!cursor.moveToFirst()) {
                 return 0;
@@ -1592,7 +1566,7 @@
         }
 
         // Delete the requested data item.
-        int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
+        int dataDeleted = mDb.delete(Tables.DATA, Data._ID + "=" + dataId, null);
         if (markRawContactAsDirty) {
             setRawContactDirty(rawContactId);
         }
@@ -1627,7 +1601,7 @@
             final int COL_IS_RESTRICTED = 1;
             final int COL_SCORE = 2;
 
-            cursor = db.query(Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS, PROJ_PRIMARY,
+            cursor = mDb.query(Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS, PROJ_PRIMARY,
                     ContactsColumns.CONCRETE_ID + "=" + contactId + " AND " + DataColumns.MIMETYPE_ID
                             + "=" + mimeId, null, null, null, SCORE);
 
@@ -1688,7 +1662,7 @@
 
             // Push through any contact updates we have
             if (values.size() > 0) {
-                db.update(Tables.CONTACTS, values, ContactsColumns.CONCRETE_ID + "=" + contactId,
+                mDb.update(Tables.CONTACTS, values, ContactsColumns.CONCRETE_ID + "=" + contactId,
                         null);
             }
         }
@@ -1700,8 +1674,6 @@
      * Inserts an item in the groups table
      */
     private long insertGroup(ContentValues values, Account account, boolean markAsDirty) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         ContentValues overriddenValues = new ContentValues(values);
         if (!resolveAccount(overriddenValues, account)) {
             return -1;
@@ -1718,14 +1690,13 @@
             overriddenValues.put(Groups.DIRTY, 1);
         }
 
-        return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
+        return mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
     }
 
     /**
      * Inserts a presence update.
      */
     public long insertPresence(ContentValues values) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         final String handle = values.getAsString(Presence.IM_HANDLE);
         final String protocol = values.getAsString(Presence.IM_PROTOCOL);
         if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
@@ -1763,7 +1734,7 @@
 
         Cursor cursor = null;
         try {
-            cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
+            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
                     selection.toString(), selectionArgs, null, null, null);
             if (cursor.moveToFirst()) {
                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
@@ -1782,17 +1753,16 @@
         values.put(Presence.RAW_CONTACT_ID, rawContactId);
 
         // Insert the presence update
-        long presenceId = db.replace(Tables.PRESENCE, null, values);
+        long presenceId = mDb.replace(Tables.PRESENCE, null, values);
         return presenceId;
     }
 
     @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
         final int match = sUriMatcher.match(uri);
         switch (match) {
             case SYNCSTATE:
-                return mOpenHelper.getSyncState().delete(db, selection, selectionArgs);
+                return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs);
 
             case CONTACTS_ID: {
                 long contactId = ContentUris.parseId(uri);
@@ -1800,10 +1770,10 @@
                 // Remove references to the contact first
                 ContentValues values = new ContentValues();
                 values.putNull(RawContacts.CONTACT_ID);
-                db.update(Tables.RAW_CONTACTS, values,
+                mDb.update(Tables.RAW_CONTACTS, values,
                         RawContacts.CONTACT_ID + "=" + contactId, null);
 
-                return db.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null);
+                return mDb.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null);
             }
 
             case RAW_CONTACTS_ID: {
@@ -1824,7 +1794,7 @@
             }
 
             case PRESENCE: {
-                return db.delete(Tables.PRESENCE, null, null);
+                return mDb.delete(Tables.PRESENCE, null, null);
             }
 
             default:
@@ -1840,23 +1810,22 @@
     }
 
     private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         final long groupMembershipMimetypeId = mOpenHelper
                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-        db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
+        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
                 + groupId, null);
 
         try {
             if (permanently) {
-                return db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
+                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
             } else {
                 mValues.clear();
                 mValues.put(Groups.DELETED, 1);
                 if (markAsDirty) {
                     mValues.put(Groups.DIRTY, 1);
                 }
-                return db.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
+                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
             }
         } finally {
             mOpenHelper.updateAllVisible();
@@ -1871,13 +1840,11 @@
     }
 
     public int deleteRawContact(long rawContactId, boolean permanently) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         // TODO delete aggregation exceptions
         mOpenHelper.removeContactIfSingleton(rawContactId);
         if (permanently) {
-            db.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
-            return db.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+            mDb.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
+            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
         } else {
             mValues.clear();
             mValues.put(RawContacts.DELETED, 1);
@@ -1897,25 +1864,24 @@
         return new Account(name, type);
     }
 
-
     @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
         int count = 0;
 
         final int match = sUriMatcher.match(uri);
         switch(match) {
             case SYNCSTATE:
-                return mOpenHelper.getSyncState().update(db, values, selection, selectionArgs);
+                return mOpenHelper.getSyncState().update(mDb, values, selection, selectionArgs);
 
             // TODO(emillar): We will want to disallow editing the contacts table at some point.
             case CONTACTS: {
-                count = db.update(Tables.CONTACTS, values, selection, selectionArgs);
+                count = mDb.update(Tables.CONTACTS, values, selection, selectionArgs);
                 break;
             }
 
             case CONTACTS_ID: {
-                count = updateContactData(db, ContentUris.parseId(uri), values);
+                count = updateContactData(ContentUris.parseId(uri), values);
                 break;
             }
 
@@ -1934,7 +1900,7 @@
             case RAW_CONTACTS: {
 
                 // TODO: security checks
-                count = db.update(Tables.RAW_CONTACTS, values, selection, selectionArgs);
+                count = mDb.update(Tables.RAW_CONTACTS, values, selection, selectionArgs);
                 break;
             }
 
@@ -1945,7 +1911,7 @@
             }
 
             case GROUPS: {
-                count = updateGroups(db, values, selection, selectionArgs,
+                count = updateGroups(values, selection, selectionArgs,
                         shouldMarkGroupAsDirty(uri));
                 break;
             }
@@ -1954,13 +1920,13 @@
                 long groupId = ContentUris.parseId(uri);
                 String selectionWithId = (Groups._ID + "=" + groupId + " ")
                         + (selection == null ? "" : " AND " + selection);
-                count = updateGroups(db, values, selectionWithId, selectionArgs,
+                count = updateGroups(values, selectionWithId, selectionArgs,
                         shouldMarkGroupAsDirty(uri));
                 break;
             }
 
             case AGGREGATION_EXCEPTIONS: {
-                count = updateAggregationException(db, values);
+                count = updateAggregationException(mDb, values);
                 break;
             }
 
@@ -1968,13 +1934,10 @@
                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
         }
 
-        if (count > 0) {
-            getContext().getContentResolver().notifyChange(uri, null);
-        }
         return count;
     }
 
-    private int updateGroups(SQLiteDatabase db, ContentValues values, String selectionWithId,
+    private int updateGroups(ContentValues values, String selectionWithId,
             String[] selectionArgs, boolean markAsDirty) {
 
         ContentValues updatedValues;
@@ -1987,7 +1950,7 @@
             updatedValues = values;
         }
 
-        int count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
+        int count = mDb.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
 
         // If changing visibility, then update contacts
         if (values.containsKey(Groups.GROUP_VISIBLE)) {
@@ -2000,35 +1963,28 @@
             String[] selectionArgs) {
 
         // TODO: security checks
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         String selectionWithId = (RawContacts._ID + " = " + rawContactId + " ")
                 + (selection == null ? "" : " AND " + selection);
-        return db.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs);
+        return mDb.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs);
     }
 
     private int updateData(Uri uri, ContentValues values, String selection,
             String[] selectionArgs, boolean markRawContactAsDirty) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count = 0;
-        db.beginTransaction();
+
+        // Note that the query will return data according to the access restrictions,
+        // so we don't need to worry about updating data we don't have permission to read.
+        Cursor c = query(uri, DataIdQuery.COLUMNS, selection, selectionArgs, null);
         try {
-            // Note that the query will return data according to the access restrictions,
-            // so we don't need to worry about updating data we don't have permission to read.
-            Cursor c = query(uri, DataIdQuery.COLUMNS, selection, selectionArgs, null);
-            try {
-                while(c.moveToNext()) {
-                    final long dataId = c.getLong(DataIdQuery._ID);
-                    final long rawContactId = c.getLong(DataIdQuery.RAW_CONTACT_ID);
-                    final String mimetype = c.getString(DataIdQuery.MIMETYPE);
-                    count += updateData(dataId, rawContactId, mimetype, values,
-                            markRawContactAsDirty);
-                }
-            } finally {
-                c.close();
+            while(c.moveToNext()) {
+                final long dataId = c.getLong(DataIdQuery._ID);
+                final long rawContactId = c.getLong(DataIdQuery.RAW_CONTACT_ID);
+                final String mimetype = c.getString(DataIdQuery.MIMETYPE);
+                count += updateData(dataId, rawContactId, mimetype, values,
+                        markRawContactAsDirty);
             }
-            db.setTransactionSuccessful();
         } finally {
-            db.endTransaction();
+            c.close();
         }
 
         return count;
@@ -2036,8 +1992,6 @@
 
     private int updateData(long dataId, long rawContactId, String mimeType, ContentValues values,
             boolean markRawContactAsDirty) {
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         mValues.clear();
         mValues.putAll(values);
         mValues.remove(Data._ID);
@@ -2081,10 +2035,10 @@
         }
 
         // TODO create GroupMembershipRowHandler and move this code there
-        resolveGroupSourceIdInValues(rawContactId, mimeType, db, mValues, false /* isInsert */);
+        resolveGroupSourceIdInValues(rawContactId, mimeType, mDb, mValues, false /* isInsert */);
 
         if (mValues.size() > 0) {
-            db.update(Tables.DATA, mValues, Data._ID + " = " + dataId, null);
+            mDb.update(Tables.DATA, mValues, Data._ID + " = " + dataId, null);
             if (markRawContactAsDirty) {
                 setRawContactDirty(rawContactId);
             }
@@ -2124,7 +2078,7 @@
         }
     }
 
-    private int updateContactData(SQLiteDatabase db, long contactId, ContentValues values) {
+    private int updateContactData(long contactId, ContentValues values) {
 
         // First update all constituent contacts
         ContentValues optionValues = new ContentValues(5);
@@ -2144,9 +2098,9 @@
             return 0;
         }
 
-        db.update(Tables.RAW_CONTACTS, optionValues,
+        mDb.update(Tables.RAW_CONTACTS, optionValues,
                 RawContacts.CONTACT_ID + "=" + contactId, null);
-        return db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+        return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
     }
 
     public void updateContactTime(long contactId, long lastTimeContacted) {
@@ -2211,7 +2165,7 @@
             }
         }
 
-        int aggregationMode = mContactAggregator.markContactForAggregation(rawContactId);
+        int aggregationMode = mContactAggregator.markContactForAggregation(mDb, rawContactId);
         if (aggregationMode != RawContacts.AGGREGATION_MODE_DISABLED) {
             mContactAggregator.aggregateContact(db, rawContactId);
             if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
@@ -2985,21 +2939,6 @@
         throw new UnsupportedOperationException("Unknown uri: " + uri);
     }
 
-    @Override
-    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
-            throws OperationApplicationException {
-
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            ContentProviderResult[] results = super.applyBatch(operations);
-            db.setTransactionSuccessful();
-            return results;
-        } finally {
-            db.endTransaction();
-        }
-    }
-
     private void setDisplayName(long rawContactId, String displayName) {
         if (displayName != null) {
             mContactDisplayNameUpdate.bindString(1, displayName);
@@ -3072,15 +3011,13 @@
         mSetSuperPrimaryStatement.execute();
 
         // Find the parent aggregate and package for this new primary
-        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-
         long aggId = -1;
         boolean isRestricted = false;
         String mimeType = null;
 
         Cursor cursor = null;
         try {
-            cursor = db.query(DataRawContactsQuery.TABLE, DataRawContactsQuery.PROJECTION,
+            cursor = mDb.query(DataRawContactsQuery.TABLE, DataRawContactsQuery.PROJECTION,
                     DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
             if (cursor.moveToFirst()) {
                 aggId = cursor.getLong(DataRawContactsQuery.CONTACT_ID);
@@ -3121,7 +3058,7 @@
 
         // Push update into contacts table, if needed
         if (values.size() > 0) {
-            db.update(Tables.CONTACTS, values, Contacts._ID + "=" + aggId, null);
+            mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + aggId, null);
         }
     }
 
diff --git a/src/com/android/providers/contacts/SQLiteContentProvider.java b/src/com/android/providers/contacts/SQLiteContentProvider.java
new file mode 100644
index 0000000..32c95df
--- /dev/null
+++ b/src/com/android/providers/contacts/SQLiteContentProvider.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2009 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.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import java.util.ArrayList;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider {
+
+    private static final String TAG = "SQLiteContentProvider";
+
+    private SQLiteOpenHelper mOpenHelper;
+    private volatile boolean mNotifyChange;
+    protected SQLiteDatabase mDb;
+
+    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+
+    @Override
+    public boolean onCreate() {
+        Context context = getContext();
+        mOpenHelper = getOpenHelper(context);
+        return true;
+    }
+
+    protected abstract SQLiteOpenHelper getOpenHelper(Context context);
+
+    /**
+     * The equivalent of the {@link #insert} method, but invoked within a transaction.
+     */
+    protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
+
+    /**
+     * The equivalent of the {@link #update} method, but invoked within a transaction.
+     */
+    protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs);
+
+    /**
+     * The equivalent of the {@link #delete} method, but invoked within a transaction.
+     */
+    protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+
+    protected SQLiteOpenHelper getOpenHelper() {
+        return mOpenHelper;
+    }
+
+    private boolean applyingBatch() {
+        return mApplyingBatch.get() != null && mApplyingBatch.get();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        Uri result = null;
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                result = insertInTransaction(uri, values);
+                if (result != null) {
+                    mNotifyChange = true;
+                }
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onTransactionComplete();
+        } else {
+            result = insertInTransaction(uri, values);
+            if (result != null) {
+                mNotifyChange = true;
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        int numValues = values.length;
+        mDb = mOpenHelper.getWritableDatabase();
+        mDb.beginTransaction();
+        try {
+            for (int i = 0; i < numValues; i++) {
+                Uri result = insertInTransaction(uri, values[i]);
+                if (result != null) {
+                    mNotifyChange = true;
+                }
+            }
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+
+        onTransactionComplete();
+        return numValues;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                count = updateInTransaction(uri, values, selection, selectionArgs);
+                if (count > 0) {
+                    mNotifyChange = true;
+                }
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onTransactionComplete();
+        } else {
+            count = updateInTransaction(uri, values, selection, selectionArgs);
+            if (count > 0) {
+                mNotifyChange = true;
+            }
+        }
+
+        return count;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                count = deleteInTransaction(uri, selection, selectionArgs);
+                if (count > 0) {
+                    mNotifyChange = true;
+                }
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onTransactionComplete();
+        } else {
+            count = deleteInTransaction(uri, selection, selectionArgs);
+            if (count > 0) {
+                mNotifyChange = true;
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        ContentProviderResult[] results = null;
+        mDb = mOpenHelper.getWritableDatabase();
+        mDb.beginTransaction();
+        try {
+            mApplyingBatch.set(true);
+            results = super.applyBatch(operations);
+            mDb.setTransactionSuccessful();
+        } finally {
+            mApplyingBatch.set(false);
+            mDb.endTransaction();
+        }
+
+        onTransactionComplete();
+        return results;
+    }
+
+    protected void onTransactionComplete() {
+        if (mNotifyChange) {
+            mNotifyChange = false;
+            getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
+        }
+    }
+}