Aggregation optimization: updating all Contacts attributes with a single pass over raw contacts.
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 47ed279..fd25406 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -113,20 +113,6 @@
 
     private static final String[] CONTACT_ID_COLUMN = new String[] { RawContacts._ID };
 
-    private static final String[] CONTACT_OPTIONS_COLUMNS = new String[] {
-            RawContacts.CUSTOM_RINGTONE,
-            RawContacts.SEND_TO_VOICEMAIL,
-            RawContacts.LAST_TIME_CONTACTED,
-            RawContacts.TIMES_CONTACTED,
-            RawContacts.STARRED,
-    };
-
-    private static final int COL_CUSTOM_RINGTONE = 0;
-    private static final int COL_SEND_TO_VOICEMAIL = 1;
-    private static final int COL_LAST_TIME_CONTACTED = 2;
-    private static final int COL_TIMES_CONTACTED = 3;
-    private static final int COL_STARRED = 4;
-
     private static final String[] CONTACT_ID_COLUMNS = new String[]{ RawContacts.CONTACT_ID };
     private static final int COL_CONTACT_ID = 0;
 
@@ -346,7 +332,9 @@
 
         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         final ContentValues values = new ContentValues();
-        updateAggregateData(db, contactId, values);
+        computeAggregateData(db, RawContacts.CONTACT_ID + "=" + contactId, values);
+        db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+
     }
 
     /**
@@ -365,17 +353,19 @@
 
         if (contactId == -1) {
             ContentValues contactValues = new ContentValues();
-            contactValues.put(Contacts.DISPLAY_NAME, "");
+            contactValues.put(RawContactsColumns.DISPLAY_NAME, "");
+            computeAggregateData(db,
+                    RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
             contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+            mOpenHelper.setContactId(rawContactId, contactId);
+        } else {
+            mOpenHelper.setContactId(rawContactId, contactId);
+            computeAggregateData(db, RawContacts.CONTACT_ID + "=" + contactId, values);
+            db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+            mOpenHelper.updateContactVisible(contactId);
         }
 
         updateContactAggregationData(db, rawContactId, candidates, values);
-        mOpenHelper.setContactId(rawContactId, contactId);
-
-        updateAggregateData(db, contactId, values);
-        updateContactValues(db, contactId);
-        mOpenHelper.updateContactVisible(contactId);
-
     }
 
     /**
@@ -840,192 +830,170 @@
         }
     }
 
+    private interface RawContactsQuery {
+        String[] COLUMNS = new String[] {
+            RawContactsColumns.CONCRETE_ID,
+            RawContactsColumns.DISPLAY_NAME,
+            RawContacts.ACCOUNT_NAME,
+            RawContacts.CUSTOM_RINGTONE,
+            RawContacts.SEND_TO_VOICEMAIL,
+            RawContacts.LAST_TIME_CONTACTED,
+            RawContacts.TIMES_CONTACTED,
+            RawContacts.STARRED,
+            RawContacts.IS_RESTRICTED,
+            DataColumns.CONCRETE_ID,
+            DataColumns.CONCRETE_MIMETYPE_ID,
+        };
+
+        int RAW_CONTACT_ID = 0;
+        int DISPLAY_NAME = 1;
+        int ACCOUNT_NAME = 2;
+        int CUSTOM_RINGTONE = 3;
+        int SEND_TO_VOICEMAIL = 4;
+        int LAST_TIME_CONTACTED = 5;
+        int TIMES_CONTACTED = 6;
+        int STARRED = 7;
+        int IS_RESTRICTED = 8;
+        int DATA_ID = 9;
+        int MIMETYPE_ID = 10;
+    }
+
     /**
-     * Updates aggregate-level data from constituent contacts.
+     * Computes aggregate-level data from constituent raw contacts.
      */
-    private void updateAggregateData(final SQLiteDatabase db, long contactId,
+    private void computeAggregateData(final SQLiteDatabase db, String selection,
             final ContentValues values) {
-        updateDisplayName(db, contactId, values);
-        updateSendToVoicemailAndRingtone(db, contactId);
-        updatePhotoId(db, contactId, values);
-    }
 
-    /**
-     * Updates the contact record's {@link Contacts#DISPLAY_NAME} field. If none of the
-     * constituent raw contacts has a suitable name, leaves the aggregate contact record unchanged.
-     */
-    private void updateDisplayName(SQLiteDatabase db, long contactId, ContentValues values) {
-        String displayName = getBestDisplayName(db, contactId);
-
-        // If don't have anything to base the display name on, let's just leave what was in
-        // that field hoping that there was something there before and it is still valid.
-        if (displayName == null) {
-            return;
-        }
-
-        values.clear();
-        values.put(Contacts.DISPLAY_NAME, displayName);
-        db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
-    }
-
-    private void updatePhotoId(SQLiteDatabase db, long contactId, ContentValues values) {
-        int photoId = choosePhotoId(db, contactId);
-
-        if (photoId == -1) {
-            return;
-        }
-
-        values.clear();
-        values.put(Contacts.PHOTO_ID, photoId);
-        db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
-    }
-
-    /**
-     * Updates various {@link ContactsColumns} values based on the newly joined
-     * {@link RawContacts} entry.
-     */
-    private void updateContactValues(SQLiteDatabase db, long contactId) {
-
-        // TODO compiled statement
-        String countRestrictedSql = "SELECT COUNT(_id) FROM " + Tables.RAW_CONTACTS + " WHERE "
-                + RawContacts.CONTACT_ID + "=" + contactId + " AND " + RawContacts.IS_RESTRICTED + "=1";
-        String countUnrestrictedSql = "SELECT COUNT(_id) FROM " + Tables.RAW_CONTACTS + " WHERE "
-                + RawContacts.CONTACT_ID + "=" + contactId + " AND " + RawContacts.IS_RESTRICTED + "=0";
-
-        String singleRestrictedSql = "(CASE WHEN (" + countRestrictedSql + ")=1"
-                + " AND (" + countUnrestrictedSql + ")=0 THEN 1 ELSE 0 END)";
-
-        String hasPhoneNumberSql = "(CASE WHEN EXISTS (SELECT " + DataColumns.CONCRETE_ID
-                + " FROM " + Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS
-                + " WHERE " + MimetypesColumns.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
-                + " AND " + Phone.NUMBER + " NOT NULL"
-                + " AND " + RawContacts.CONTACT_ID + "=" + contactId + ") THEN 1 ELSE 0 END)";
-
-        String updateSql = "UPDATE " + Tables.CONTACTS + " SET "
-                + ContactsColumns.SINGLE_IS_RESTRICTED + "=" + singleRestrictedSql + ","
-                + Contacts.HAS_PHONE_NUMBER + "=" + hasPhoneNumberSql
-                + " WHERE " + ContactsColumns.CONCRETE_ID + "=" + contactId;
-
-        db.execSQL(updateSql);
-    }
-
-    /**
-     * Computes display name for the given contact.  Chooses a longer name over a shorter name
-     * and a mixed-case name over an all lowercase or uppercase name.
-     */
-    private String getBestDisplayName(SQLiteDatabase db, long contactId) {
+        long currentRawContactId = -1;
         String bestDisplayName = null;
-
-        final Cursor c = db.query(Tables.RAW_CONTACTS, new String[] {RawContactsColumns.DISPLAY_NAME},
-                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
-
-        try {
-            while (c.moveToNext()) {
-                String displayName = c.getString(0);
-                if (!TextUtils.isEmpty(displayName)) {
-                    if (bestDisplayName == null) {
-                        bestDisplayName = displayName;
-                    } else {
-                        if (NameNormalizer.compareComplexity(displayName, bestDisplayName) > 0) {
-                            bestDisplayName = displayName;
-                        }
-                    }
-                }
-            }
-        } finally {
-            c.close();
-        }
-        return bestDisplayName;
-    }
-
-    /**
-     * Iterates over the photos associated with contact defined by contactId, and chooses one
-     * to be associated with the contact. Initially this just chooses the first photo in a list
-     * sorted by account name.
-     */
-    private int choosePhotoId(SQLiteDatabase db, long contactId) {
-        int chosenPhotoId = -1;
-        String chosenAccount = null;
-
-        final Cursor c = db.query(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS,
-                new String[] {"data._id AS _id", RawContacts.ACCOUNT_NAME},
-                DatabaseUtils.concatenateWhere(RawContacts.CONTACT_ID + "=" + contactId,
-                        Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'"),
-                null, null, null, null);
-
-        try {
-            while (c.moveToNext()) {
-                int photoId = c.getInt(0);
-                String account = c.getString(1);
-                if (chosenAccount == null) {
-                    chosenAccount = account;
-                    chosenPhotoId = photoId;
-                } else {
-                    if (account.compareToIgnoreCase(chosenAccount) < 0 ) {
-                        chosenAccount = account;
-                        chosenPhotoId = photoId;
-                    }
-                }
-            }
-        } finally {
-            c.close();
-        }
-        return chosenPhotoId;
-    }
-
-    /**
-     * Updates the contact's send-to-voicemail and custom-ringtone options based on
-     * constituent contacts' options.
-     */
-    private void updateSendToVoicemailAndRingtone(SQLiteDatabase db, long contactId) {
-        int totalContactCount = 0;
+        long bestPhotoId = -1;
+        String photoAccount = null;
+        int totalRowCount = 0;
         int contactSendToVoicemail = 0;
         String contactCustomRingtone = null;
         long contactLastTimeContacted = 0;
         int contactTimesContacted = 0;
         boolean contactStarred = false;
+        int singleIsRestricted = 1;
+        int hasPhoneNumber = 0;
 
-        final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_OPTIONS_COLUMNS,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+        long photoMimeType = mOpenHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+        long phoneMimeType = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
 
+        String isPhotoSql = "(" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
+                + Photo.PHOTO + " NOT NULL)";
+        String isPhoneSql = "(" + DataColumns.MIMETYPE_ID + "=" + phoneMimeType + " AND "
+                + Phone.NUMBER + " NOT NULL)";
+
+        String tables = Tables.RAW_CONTACTS + " LEFT OUTER JOIN " + Tables.DATA + " ON("
+                + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+                + " AND (" + isPhotoSql + " OR " + isPhoneSql + "))";
+
+        final Cursor c = db.query(tables, RawContactsQuery.COLUMNS, selection, null, null, null,
+                null);
         try {
             while (c.moveToNext()) {
-                totalContactCount++;
-                if (!c.isNull(COL_SEND_TO_VOICEMAIL)) {
-                    boolean sendToVoicemail = (c.getInt(COL_SEND_TO_VOICEMAIL) != 0);
-                    if (sendToVoicemail) {
-                        contactSendToVoicemail++;
+                long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
+                if (rawContactId != currentRawContactId) {
+                    currentRawContactId = rawContactId;
+                    totalRowCount++;
+
+                    // Display name
+                    String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
+                    if (!TextUtils.isEmpty(displayName)) {
+                        if (bestDisplayName == null) {
+                            bestDisplayName = displayName;
+                        } else if (NameNormalizer.compareComplexity(displayName,
+                                bestDisplayName) > 0) {
+                            bestDisplayName = displayName;
+                        }
+                    }
+
+                    // Contact options
+                    if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
+                        boolean sendToVoicemail =
+                                (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
+                        if (sendToVoicemail) {
+                            contactSendToVoicemail++;
+                        }
+                    }
+
+                    if (contactCustomRingtone == null
+                            && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
+                        contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
+                    }
+
+                    long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
+                    if (lastTimeContacted > contactLastTimeContacted) {
+                        contactLastTimeContacted = lastTimeContacted;
+                    }
+
+                    int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
+                    if (timesContacted > contactTimesContacted) {
+                        contactTimesContacted = timesContacted;
+                    }
+
+                    contactStarred |= (c.getInt(RawContactsQuery.STARRED) != 0);
+
+                    // Single restricted
+                    if (totalRowCount > 1) {
+                        // Not single
+                        singleIsRestricted = 0;
+                    } else {
+                        int isRestricted = c.getInt(RawContactsQuery.IS_RESTRICTED);
+
+                        if (isRestricted == 0) {
+                            // Not restricted
+                            singleIsRestricted = 0;
+                        }
                     }
                 }
 
-                if (contactCustomRingtone == null && !c.isNull(COL_CUSTOM_RINGTONE)) {
-                    contactCustomRingtone = c.getString(COL_CUSTOM_RINGTONE);
+                if (!c.isNull(RawContactsQuery.DATA_ID)) {
+                    long dataId = c.getLong(RawContactsQuery.DATA_ID);
+                    int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
+                    if (mimetypeId == photoMimeType) {
+
+                        // For now, just choose the first photo in a list sorted by account name.
+                        String account = c.getString(RawContactsQuery.ACCOUNT_NAME);
+                        if (photoAccount == null) {
+                            photoAccount = account;
+                            bestPhotoId = dataId;
+                        } else {
+                            if (account.compareToIgnoreCase(photoAccount) < 0) {
+                                photoAccount = account;
+                                bestPhotoId = dataId;
+                            }
+                        }
+                    } else if (mimetypeId == phoneMimeType) {
+                        hasPhoneNumber = 1;
+                    }
                 }
 
-                long lastTimeContacted = c.getLong(COL_LAST_TIME_CONTACTED);
-                if (lastTimeContacted > contactLastTimeContacted) {
-                    contactLastTimeContacted = lastTimeContacted;
-                }
-
-                int timesContacted = c.getInt(COL_TIMES_CONTACTED);
-                if (timesContacted > contactTimesContacted) {
-                    contactTimesContacted = timesContacted;
-                }
-
-                contactStarred |= (c.getInt(COL_STARRED) != 0);
             }
         } finally {
             c.close();
         }
 
-        ContentValues values = new ContentValues(2);
-        values.put(Contacts.SEND_TO_VOICEMAIL, totalContactCount == contactSendToVoicemail);
+        values.clear();
+
+        // If don't have anything to base the display name on, let's just leave what was in
+        // that field hoping that there was something there before and it is still valid.
+        if (bestDisplayName != null) {
+            values.put(Contacts.DISPLAY_NAME, bestDisplayName);
+        }
+
+        if (bestPhotoId != -1) {
+            values.put(Contacts.PHOTO_ID, bestPhotoId);
+        }
+
+        values.put(Contacts.SEND_TO_VOICEMAIL, totalRowCount == contactSendToVoicemail);
         values.put(Contacts.CUSTOM_RINGTONE, contactCustomRingtone);
         values.put(Contacts.LAST_TIME_CONTACTED, contactLastTimeContacted);
         values.put(Contacts.TIMES_CONTACTED, contactTimesContacted);
         values.put(Contacts.STARRED, contactStarred);
-
-        db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+        values.put(Contacts.HAS_PHONE_NUMBER, hasPhoneNumber);
+        values.put(ContactsColumns.SINGLE_IS_RESTRICTED, singleIsRestricted);
     }
 
     /**
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index e5b826b..04eb949 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -66,6 +66,8 @@
     protected MockContentResolver mResolver;
     protected Account mAccount = new Account("account1", "account type1");
 
+    private byte[] mTestPhoto;
+
     protected final static Long NO_LONG = new Long(0);
     protected final static String NO_STRING = new String("");
     protected final static Account NO_ACCOUNT = new Account("a", "b");
@@ -224,7 +226,7 @@
         ContentValues values = new ContentValues();
         values.put(Data.RAW_CONTACT_ID, rawContactId);
         values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
-
+        values.put(Photo.PHOTO, loadTestPhoto());
         Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
         return resultUri;
     }
@@ -606,16 +608,23 @@
         }
     }
 
-    protected byte[] loadTestPhoto() throws IOException {
-        final Resources resources = getContext().getResources();
-        InputStream is =
-                resources.openRawResource(com.android.internal.R.drawable.ic_contact_picture);
-        ByteArrayOutputStream os = new ByteArrayOutputStream();
-        byte[] buffer = new byte[1000];
-        int count;
-        while((count = is.read(buffer)) != -1) {
-            os.write(buffer, 0, count);
+    protected byte[] loadTestPhoto() {
+        if (mTestPhoto == null) {
+            final Resources resources = getContext().getResources();
+            InputStream is = resources
+                    .openRawResource(com.android.internal.R.drawable.ic_contact_picture);
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            byte[] buffer = new byte[1000];
+            int count;
+            try {
+                while ((count = is.read(buffer)) != -1) {
+                    os.write(buffer, 0, count);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            mTestPhoto = os.toByteArray();
         }
-        return os.toByteArray();
+        return mTestPhoto;
     }
 }
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
new file mode 100644
index 0000000..7e382fd
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.os.Debug;
+import android.provider.ContactsContract;
+import android.test.AndroidTestCase;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+/**
+ * Performance test for {@link ContactAggregator}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb push <large contacts2.db> \
+ *         data/data/com.android.providers.contacts/databases/perf.contacts2.db
+ * adb shell am instrument -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactAggregatorPerformanceTest extends AndroidTestCase {
+
+    private static final String TAG = "ContactAggregatorPerformanceTest";
+    private static final boolean TRACE = false;
+
+    public void testPerformance() {
+        final Context targetContext = getContext();
+        MockContentResolver resolver = new MockContentResolver();
+        MockContext context = new MockContext() {
+            @Override
+            public Resources getResources() {
+                return targetContext.getResources();
+            }
+        };
+        RenamingDelegatingContext targetContextWrapper =
+                new RenamingDelegatingContext(context, targetContext, "perf.");
+        targetContextWrapper.makeExistingFilesAndDbsAccessible();
+        IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper);
+        TestAggregationScheduler scheduler = new TestAggregationScheduler();
+        SynchronousContactsProvider2 provider = new SynchronousContactsProvider2(scheduler);
+        provider.setDataWipeEnabled(false);
+        provider.attachInfo(providerContext, null);
+        resolver.addProvider(ContactsContract.AUTHORITY, provider);
+
+        long rawContactCount = provider.getRawContactCount();
+        if (rawContactCount == 0) {
+            Log.w(TAG, "The test has not been set up. Use this command to copy a contact db"
+                    + " to the device:\nadb push <large contacts2.db> "
+                    + "data/data/com.android.providers.contacts/databases/perf.contacts2.db");
+            return;
+        }
+
+        provider.prepareForFullAggregation(50);
+        rawContactCount = provider.getRawContactCount();
+        long start = System.currentTimeMillis();
+        if (TRACE) {
+            Debug.startMethodTracing("aggregation");
+        }
+        scheduler.trigger();
+        if (TRACE) {
+            Debug.stopMethodTracing();
+        }
+        long end = System.currentTimeMillis();
+        long contactCount = provider.getContactCount();
+
+        Log.i(TAG, String.format("Aggregated contacts in %d ms.\n" +
+                "Raw contacts: %d\n" +
+                "Aggregated contacts: %d\n" +
+                "Per raw contact: %.3f",
+                end-start,
+                rawContactCount,
+                contactCount,
+                ((double)(end-start)/rawContactCount)));
+    }
+
+    private static class TestAggregationScheduler extends ContactAggregationScheduler {
+
+        @Override
+        public void start() {
+        }
+
+        @Override
+        public void stop() {
+        }
+
+        @Override
+        long currentTime() {
+            return 0;
+        }
+
+        @Override
+        void runDelayed() {
+        }
+
+        void trigger() {
+            run();
+        }
+    }
+}
+
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 79e7bcb..f5b42c0 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -18,6 +18,7 @@
 
 import android.accounts.Account;
 import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
 
 /**
  * A version of {@link ContactsProvider2} class that performs aggregation
@@ -26,10 +27,15 @@
 public class SynchronousContactsProvider2 extends ContactsProvider2 {
     private static Boolean sDataWiped = false;
     private static OpenHelper mOpenHelper;
+    private boolean mDataWipeEnabled = true;
     private Account mAccount;
 
     public SynchronousContactsProvider2() {
-        super(new SynchronousAggregationScheduler());
+        this(new SynchronousAggregationScheduler());
+    }
+
+    public SynchronousContactsProvider2(ContactAggregationScheduler scheduler) {
+        super(scheduler);
     }
 
     @Override
@@ -44,13 +50,19 @@
         mOpenHelper = null;
     }
 
+    public void setDataWipeEnabled(boolean flag) {
+        mDataWipeEnabled = flag;
+    }
+
     @Override
     public boolean onCreate() {
         boolean created = super.onCreate();
-        synchronized (sDataWiped) {
-            if (!sDataWiped) {
-                sDataWiped = true;
-                wipeData();
+        if (mDataWipeEnabled) {
+            synchronized (sDataWiped) {
+                if (!sDataWiped) {
+                    sDataWiped = true;
+                    wipeData();
+                }
             }
         }
         return created;
@@ -64,6 +76,26 @@
         return mAccount;
     }
 
+    public void prepareForFullAggregation(int maxContact) {
+        SQLiteDatabase db = getOpenHelper().getWritableDatabase();
+        db.execSQL("UPDATE raw_contacts SET contact_id = NULL, aggregation_mode=0;");
+        db.execSQL("DELETE FROM contacts;");
+        long rowId =
+            db.compileStatement("SELECT _id FROM raw_contacts LIMIT 1 OFFSET " + maxContact)
+                .simpleQueryForLong();
+        db.execSQL("DELETE FROM raw_contacts WHERE _id > " + rowId + ";");
+    }
+
+    public long getRawContactCount() {
+        SQLiteDatabase db = getOpenHelper().getReadableDatabase();
+        return db.compileStatement("SELECT COUNT(*) FROM raw_contacts").simpleQueryForLong();
+    }
+
+    public long getContactCount() {
+        SQLiteDatabase db = getOpenHelper().getReadableDatabase();
+        return db.compileStatement("SELECT COUNT(*) FROM contacts").simpleQueryForLong();
+    }
+
     @Override
     protected boolean isLegacyContactImportNeeded() {