do not merge: cherry-picked d326974ca339cef284cc045c61d340ddb60d08da from master branch
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index c142e83..6e19f79 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -34,11 +34,14 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.SearchManager;
+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;
@@ -49,6 +52,7 @@
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
@@ -76,6 +80,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.concurrent.Semaphore;
 
 /**
  * Contacts content provider. The contract between this provider and applications
@@ -1035,6 +1040,7 @@
 
     private ContentValues mValues = new ContentValues();
 
+    private volatile Semaphore mAccessSemaphore;
     private boolean mImportMode;
 
     private boolean mScheduleAggregation;
@@ -1098,10 +1104,9 @@
                 new StructuredNameRowHandler(mNameSplitter));
 
         if (isLegacyContactImportNeeded()) {
-            if (!importLegacyContacts()) {
-                return false;
-            }
+            importLegacyContactsAsync();
         }
+
         return (db != null);
     }
 
@@ -1120,8 +1125,47 @@
         return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
     }
 
+    protected LegacyContactImporter getLegacyContactImporter() {
+        return new LegacyContactImporter(getContext(), this);
+    }
+
+    /**
+     * Imports legacy contacts in a separate thread.  As long as the import process is running
+     * all other access to the contacts is blocked.
+     */
+    private void importLegacyContactsAsync() {
+        mAccessSemaphore = new Semaphore(1);
+
+        // Parameter (0) indicates that release must be called before acquire is granted
+        final Semaphore importThreadStarted = new Semaphore(0);
+
+        Thread importThread = new Thread("LegacyContactImport") {
+            @Override
+            public void run() {
+                mAccessSemaphore.acquireUninterruptibly();
+                importThreadStarted.release();
+                if (importLegacyContacts()) {
+
+                    /*
+                     * When the import process is done, we can unlock the provider and
+                     * start aggregating the imported contacts asynchronously.
+                     */
+                    mAccessSemaphore.release();
+                    mAccessSemaphore = null;
+                    scheduleContactAggregation();
+                }
+            }
+        };
+
+        importThread.start();
+
+        // Wait for the import thread to start
+        importThreadStarted.acquireUninterruptibly();
+    }
+
     private boolean importLegacyContacts() {
-        if (importLegacyContacts(getLegacyContactImporter(), true)) {
+        LegacyContactImporter importer = getLegacyContactImporter();
+        if (importLegacyContacts(importer)) {
             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
             Editor editor = prefs.edit();
             editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
@@ -1132,20 +1176,13 @@
         }
     }
 
-    protected LegacyContactImporter getLegacyContactImporter() {
-        return new LegacyContactImporter(getContext(), this);
-    }
-
     /* Visible for testing */
-    /* package */ boolean importLegacyContacts(LegacyContactImporter importer, boolean aggregate) {
+    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
         mContactAggregator.setEnabled(false);
         mImportMode = true;
         try {
             importer.importContacts();
             mContactAggregator.setEnabled(true);
-            if (aggregate) {
-                mContactAggregator.run();
-            }
             return true;
         } catch (Throwable e) {
            Log.e(TAG, "Legacy contact import failed", e);
@@ -1171,15 +1208,63 @@
         mOpenHelper.wipeData();
     }
 
+    /**
+     * While importing and aggregating contacts, this content provider will
+     * block all attempts to change contacts data. In particular, it will hold
+     * up all contact syncs. As soon as the import process is complete, all
+     * processes waiting to write to the provider are unblocked and can proceed
+     * to compete for the database transaction monitor.
+     */
+    private void waitForAccess() {
+        Semaphore semaphore = mAccessSemaphore;
+        if (semaphore != null) {
+            semaphore.acquireUninterruptibly();
+
+            // We don't need to hold this semaphore, the database lock will later ensure
+            // exclusive access to the database.
+            semaphore.release();
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        waitForAccess();
+        return super.insert(uri, values);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        waitForAccess();
+        return super.update(uri, values, selection, selectionArgs);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        waitForAccess();
+        return super.delete(uri, selection, selectionArgs);
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        waitForAccess();
+        return super.applyBatch(operations);
+    }
+
     @Override
     protected void onTransactionComplete() {
         if (mScheduleAggregation) {
             mScheduleAggregation = false;
-            mContactAggregator.schedule();
+            scheduleContactAggregation();
         }
         super.onTransactionComplete();
     }
 
+
+    protected void scheduleContactAggregation() {
+        mContactAggregator.schedule();
+    }
+
     private DataRowHandler getDataRowHandler(final String mimeType) {
         DataRowHandler handler = mDataRowHandlers.get(mimeType);
         if (handler == null) {
@@ -2708,6 +2793,8 @@
     @Override
     public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
             String sortOrder) {
+        waitForAccess();
+
         final int match = sUriMatcher.match(uri);
         switch (match) {
             case RAW_CONTACTS:
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index bdead14..712bcf3 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -48,10 +48,15 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.io.File;
+
 public class LegacyContactImporter {
 
     public static final String TAG = "LegacyContactImporter";
 
+    private static final int MAX_ATTEMPTS = 5;
+    private static final int DELAY_BETWEEN_ATTEMPTS = 2000;
+
     public static final String DEFAULT_ACCOUNT_TYPE = "com.google.GAIA";
     private static final String DATABASE_NAME = "contacts.db";
 
@@ -69,6 +74,8 @@
     private NameSplitter mNameSplitter;
     private int mBatchCounter;
 
+    private int mContactCount;
+
     private long mStructuredNameMimetypeId;
     private long mNoteMimetypeId;
     private long mOrganizationMimetypeId;
@@ -86,81 +93,94 @@
     }
 
     public void importContacts() throws Exception {
+        String path = mContext.getDatabasePath(DATABASE_NAME).getPath();
+        Log.w(TAG, "Importing contacts from " + path);
 
-        try {
-            String path = mContext.getDatabasePath(DATABASE_NAME).getPath();
+        if (!new File(path).exists()) {
+            Log.i(TAG, "Legacy contacts database does not exist");
+            return;
+        }
+
+        for (int i = 0; i < MAX_ATTEMPTS; i++) {
             try {
                 mSourceDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
-            } catch(SQLiteException e) {
-
-                // If we cannot open the original database, it is either non-existent or corrupt;
-                // in both bases - just bail.
+                importContactsFromLegacyDb();
+                Log.i(TAG, "Imported legacy contacts: " + mContactCount);
                 return;
+
+            } catch (SQLiteException e) {
+                Log.e(TAG, "Database import exception. Will retry in " + DELAY_BETWEEN_ATTEMPTS
+                        + "ms", e);
+
+                // We could get a "database locked" exception here, in which
+                // case we should retry
+                Thread.sleep(DELAY_BETWEEN_ATTEMPTS);
+
+            } finally {
+                if (mSourceDb != null) {
+                    mSourceDb.close();
+                }
             }
-
-            int version = mSourceDb.getVersion();
-
-            // Upgrade to version 78 was the latest that wiped out data.  Might as well follow suit
-            // and ignore earlier versions.
-            if (version < 78) {
-                return;
-            }
-
-            Log.w(TAG, "Importing contacts from " + path);
-
-            if (version < 80) {
-                mPhoneticNameAvailable = false;
-            }
-
-            OpenHelper openHelper = (OpenHelper)mContactsProvider.getOpenHelper();
-            mTargetDb = openHelper.getWritableDatabase();
-
-            /*
-             * At this point there should be no data in the contacts provider, but in case
-             * some was inserted by mistake, we should remove it.  The main reason for this
-             * is that we will be preserving original contact IDs and don't want to run into
-             * any collisions.
-             */
-            mContactsProvider.wipeData();
-
-            mStructuredNameMimetypeId = openHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
-            mNoteMimetypeId = openHelper.getMimeTypeId(Note.CONTENT_ITEM_TYPE);
-            mOrganizationMimetypeId = openHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
-            mPhoneMimetypeId = openHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-            mEmailMimetypeId = openHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
-            mImMimetypeId = openHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
-            mPostalMimetypeId = openHelper.getMimeTypeId(StructuredPostal.CONTENT_ITEM_TYPE);
-            mPhotoMimetypeId = openHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
-            mGroupMembershipMimetypeId =
-                    openHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-
-            mNameSplitter = mContactsProvider.getNameSplitter();
-
-            mTargetDb.beginTransaction();
-            importGroups();
-            importPeople();
-            importOrganizations();
-            importPhones();
-            importContactMethods();
-            importPhotos();
-            importGroupMemberships();
-
-            // Deleted contacts should be inserted after everything else, because
-            // the legacy table does not provide an _ID field - the _ID field
-            // will be autoincremented
-            importDeletedPeople();
-
-            mTargetDb.setTransactionSuccessful();
-            mTargetDb.endTransaction();
-
-            importCalls();
-
-            Log.w(TAG, "Contact import completed");
-        } finally {
-            if (mSourceDb != null) mSourceDb.close();
         }
     }
 
+    private void importContactsFromLegacyDb() {
+        int version = mSourceDb.getVersion();
+
+        // Upgrade to version 78 was the latest that wiped out data.  Might as well follow suit
+        // and ignore earlier versions.
+        if (version < 78) {
+            return;
+        }
+
+        if (version < 80) {
+            mPhoneticNameAvailable = false;
+        }
+
+        OpenHelper openHelper = (OpenHelper)mContactsProvider.getOpenHelper();
+        mTargetDb = openHelper.getWritableDatabase();
+
+        /*
+         * At this point there should be no data in the contacts provider, but in case
+         * some was inserted by mistake, we should remove it.  The main reason for this
+         * is that we will be preserving original contact IDs and don't want to run into
+         * any collisions.
+         */
+        mContactsProvider.wipeData();
+
+        mStructuredNameMimetypeId = openHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+        mNoteMimetypeId = openHelper.getMimeTypeId(Note.CONTENT_ITEM_TYPE);
+        mOrganizationMimetypeId = openHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+        mPhoneMimetypeId = openHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+        mEmailMimetypeId = openHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+        mImMimetypeId = openHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
+        mPostalMimetypeId = openHelper.getMimeTypeId(StructuredPostal.CONTENT_ITEM_TYPE);
+        mPhotoMimetypeId = openHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+        mGroupMembershipMimetypeId =
+                openHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+
+        mNameSplitter = mContactsProvider.getNameSplitter();
+
+        mTargetDb.beginTransaction();
+        importGroups();
+        importPeople();
+        importOrganizations();
+        importPhones();
+        importContactMethods();
+        importPhotos();
+        importGroupMemberships();
+
+        // Deleted contacts should be inserted after everything else, because
+        // the legacy table does not provide an _ID field - the _ID field
+        // will be autoincremented
+        importDeletedPeople();
+
+        mTargetDb.setTransactionSuccessful();
+        mTargetDb.endTransaction();
+
+        importCalls();
+    }
+
     private interface GroupsQuery {
         String TABLE = "groups";
 
@@ -410,14 +430,14 @@
                 insertRawContact(c, rawContactInsert);
                 insertStructuredName(c, structuredNameInsert);
                 insertNote(c, noteInsert);
+                mContactCount++;
             }
         } finally {
             c.close();
         }
     }
 
-    private void insertRawContact(Cursor c, SQLiteStatement insert)
-            {
+    private void insertRawContact(Cursor c, SQLiteStatement insert) {
         long id = c.getLong(PeopleQuery._ID);
         insert.bindLong(RawContactsInsert.ID, id);
         bindString(insert, RawContactsInsert.CUSTOM_RINGTONE,
@@ -452,8 +472,7 @@
         insert(insert);
     }
 
-    private void insertStructuredName(Cursor c, SQLiteStatement insert)
-            {
+    private void insertStructuredName(Cursor c, SQLiteStatement insert) {
         String name = c.getString(PeopleQuery.NAME);
         if (TextUtils.isEmpty(name)) {
             return;
@@ -544,8 +563,7 @@
         }
     }
 
-    private void insertOrganization(Cursor c, SQLiteStatement insert)
-            {
+    private void insertOrganization(Cursor c, SQLiteStatement insert) {
 
         long id = c.getLong(OrganizationsQuery.PERSON);
         insert.bindLong(OrganizationInsert.RAW_CONTACT_ID, id);
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
index 9951f51..4cc6911 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
@@ -77,36 +77,12 @@
         if (TRACE) {
             Debug.startMethodTracing("import");
         }
-        provider.importLegacyContacts(importer, false);
+        provider.importLegacyContacts(importer);
         if (TRACE) {
             Debug.stopMethodTracing();
         }
         long end = System.currentTimeMillis();
         long contactCount = provider.getRawContactCount();
-
-        // long rawContactCount = provider.getRawContactCount();
-        // if (rawContactCount == 0) {
-        // Log.w(TAG,
-        // "The test has not been set up. Use this command to copy a legacy contact db"
-        // + " to the device:\nadb push <large contacts.db> "
-        // +
-        // "data/data/com.android.providers.contacts/databases/perf_imp.contacts.db");
-        // return;
-        // }
-
-        // provider.prepareForFullAggregation(100);
-        // 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("Imported contacts in %d ms.\n"
                 + "Contacts: %d\n"
                 + "Per contact: %.3f",
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
index 17f01d7..bf5fde8 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
@@ -89,7 +89,8 @@
         ContactsProvider2 provider = (ContactsProvider2)getProvider();
         LegacyContactImporter importer =
                 new LegacyContactImporter(createLegacyMockContext(), provider);
-        provider.importLegacyContacts(importer, true);
+        provider.importLegacyContacts(importer);
+        provider.scheduleContactAggregation();
 
         assertQueryResults("expected_groups.txt", Groups.CONTENT_URI, new String[]{
                 Groups._ID,