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,