aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitri Plotnikov <dplotnikov@google.com>2010-03-12 19:29:10 -0800
committerDmitri Plotnikov <dplotnikov@google.com>2010-03-15 18:05:42 -0700
commitbd578a748ab5bd74aa63511cce8769d5882f4651 (patch)
tree60fa36a299196d96ddef3bd8694a2eb0ae1c5a5b
parent51698db0c97b20e95421bc42b243e4ea315137be (diff)
downloadContactsProvider-bd578a748ab5bd74aa63511cce8769d5882f4651.tar.gz
Implementing legacy contact upgrade under low storage conditions
Bug: 2498528 Change-Id: Ibd7aa458f665fea71192ce7ff1743f064acb3858
-rw-r--r--res/values/strings.xml9
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java113
-rw-r--r--src/com/android/providers/contacts/LegacyContactImporter.java106
3 files changed, 168 insertions, 60 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bbf4ade7..ba8a7de7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -22,5 +22,14 @@
<!-- What to show in messaging that refers to this provider, e.g. AccountSyncSettings -->
<string name="provider_label">Contacts</string>
+
+ <!-- Ticker for the notification shown when updating contacts fails because of memory shortage -->
+ <string name="upgrade_out_of_memory_notification_ticker">Contact upgrade needs more memory</string>
+
+ <!-- Title for the notification shown when updating contacts fails because of memory shortage -->
+ <string name="upgrade_out_of_memory_notification_title">Upgrading contact storage</string>
+
+ <!-- Text for the notification shown when updating contacts fails because of memory shortage -->
+ <string name="upgrade_out_of_memory_notification_text">Select to complete the upgrade.</string>
</resources>
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index b2823e15..4750bc89 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -42,6 +42,9 @@ import com.google.android.collect.Sets;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.SearchManager;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
@@ -50,6 +53,7 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.IContentService;
+import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncAdapterType;
@@ -89,6 +93,7 @@ import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.ProviderStatus;
@@ -383,6 +388,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ /**
+ * Notification ID for failure to import contacts.
+ */
+ private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
/** Precompiled sql statement for setting a data record to the primary. */
private SQLiteStatement mSetPrimaryStatement;
@@ -1924,13 +1933,13 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ verifyAccounts();
+ verifyLocale();
+
if (isLegacyContactImportNeeded()) {
importLegacyContactsAsync();
}
- verifyAccounts();
- verifyLocale();
-
return (db != null);
}
@@ -2043,20 +2052,20 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
* all other access to the contacts is blocked.
*/
private void importLegacyContactsAsync() {
- mAccessLatch = new CountDownLatch(1);
+ Log.v(TAG, "Importing legacy contacts");
+ setProviderStatus(ProviderStatus.STATUS_UPGRADING);
+ if (mAccessLatch == null) {
+ mAccessLatch = new CountDownLatch(1);
+ }
Thread importThread = new Thread("LegacyContactImport") {
@Override
public void run() {
- if (importLegacyContacts()) {
- // TODO aggregate all newly added raw contacts
-
- /*
- * When the import process is done, we can unlock the provider and
- * start aggregating the imported contacts asynchronously.
- */
- mAccessLatch.countDown();
- mAccessLatch = null;
+ LegacyContactImporter importer = getLegacyContactImporter();
+ if (importLegacyContacts(importer)) {
+ onLegacyContactImportSuccess();
+ } else {
+ onLegacyContactImportFailure();
}
}
};
@@ -2064,17 +2073,46 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
importThread.start();
}
- private boolean importLegacyContacts() {
- LegacyContactImporter importer = getLegacyContactImporter();
- if (importLegacyContacts(importer)) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
- Editor editor = prefs.edit();
- editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
- editor.commit();
- return true;
- } else {
- return false;
- }
+ /**
+ * Unlocks the provider and declares that the import process is complete.
+ */
+ private void onLegacyContactImportSuccess() {
+ NotificationManager nm =
+ (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ Editor editor = prefs.edit();
+ editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
+ editor.commit();
+ setProviderStatus(ProviderStatus.STATUS_NORMAL);
+ mAccessLatch.countDown();
+ mAccessLatch = null;
+ Log.v(TAG, "Completed import of legacy contacts");
+ }
+
+ /**
+ * Announces the provider status and keeps the provider locked.
+ */
+ private void onLegacyContactImportFailure() {
+ Context context = getContext();
+ NotificationManager nm =
+ (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ // Show a notification
+ Notification n = new Notification(android.R.drawable.stat_notify_error,
+ context.getString(R.string.upgrade_out_of_memory_notification_ticker),
+ System.currentTimeMillis());
+ n.setLatestEventInfo(context,
+ context.getString(R.string.upgrade_out_of_memory_notification_title),
+ context.getString(R.string.upgrade_out_of_memory_notification_text),
+ PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
+ n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+
+ nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
+
+ setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
+ Log.v(TAG, "Failed to import legacy contacts");
}
/* Visible for testing */
@@ -2082,13 +2120,17 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
boolean aggregatorEnabled = mContactAggregator.isEnabled();
mContactAggregator.setEnabled(false);
try {
- importer.importContacts();
- mContactAggregator.setEnabled(aggregatorEnabled);
- return true;
+ if (importer.importContacts()) {
+
+ // TODO aggregate all newly added raw contacts
+ mContactAggregator.setEnabled(aggregatorEnabled);
+ return true;
+ }
} catch (Throwable e) {
Log.e(TAG, "Legacy contact import failed", e);
- return false;
}
+ mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
+ return false;
}
/**
@@ -2128,6 +2170,21 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ if (mAccessLatch != null) {
+ // We are stuck trying to upgrade contacts db. The only update request
+ // allowed in this case is an update of provider status, which will trigger
+ // an attempt to upgrade contacts again.
+ int match = sUriMatcher.match(uri);
+ if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) {
+ Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
+ if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
+ importLegacyContactsAsync();
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
waitForAccess();
return super.update(uri, values, selection, selectionArgs);
}
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 201a3b42..391ff3e9 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -26,6 +26,7 @@ import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
@@ -61,6 +62,16 @@ public class LegacyContactImporter {
private static final int INSERT_BATCH_SIZE = 200;
+ /**
+ * Estimated increase in database size after import.
+ */
+ private static final long DATABASE_SIZE_MULTIPLIER = 4;
+
+ /**
+ * Estimated minimum database size in megabytes.
+ */
+ private static final long DATABASE_MIN_SIZE = 5;
+
private final Context mContext;
private final ContactsProvider2 mContactsProvider;
private ContactsDatabaseHelper mDbHelper;
@@ -86,6 +97,7 @@ public class LegacyContactImporter {
private long mPhotoMimetypeId;
private long mGroupMembershipMimetypeId;
+ private long mEstimatedStorageRequirement;
public LegacyContactImporter(Context context, ContactsProvider2 contactsProvider) {
mContext = context;
@@ -93,22 +105,23 @@ public class LegacyContactImporter {
mResolver = mContactsProvider.getContext().getContentResolver();
}
- public void importContacts() throws Exception {
+ public boolean importContacts() throws Exception {
String path = mContext.getDatabasePath(DATABASE_NAME).getPath();
- Log.w(TAG, "Importing contacts from " + path);
-
- if (!new File(path).exists()) {
- Log.i(TAG, "Legacy contacts database does not exist");
- return;
+ File file = new File(path);
+ if (!file.exists()) {
+ Log.i(TAG, "Legacy contacts database does not exist at " + path);
+ return true;
}
+ Log.w(TAG, "Importing contacts from " + path);
+
for (int i = 0; i < MAX_ATTEMPTS; i++) {
try {
mSourceDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
importContactsFromLegacyDb();
Log.i(TAG, "Imported legacy contacts: " + mContactCount);
mContactsProvider.notifyChange();
- return;
+ return true;
} catch (SQLiteException e) {
Log.e(TAG, "Database import exception. Will retry in " + DELAY_BETWEEN_ATTEMPTS
@@ -124,6 +137,18 @@ public class LegacyContactImporter {
}
}
}
+
+ long oldDatabaseSize = file.length();
+ mEstimatedStorageRequirement = oldDatabaseSize * DATABASE_SIZE_MULTIPLIER / 1024 / 1024;
+ if (mEstimatedStorageRequirement < DATABASE_MIN_SIZE) {
+ mEstimatedStorageRequirement = DATABASE_MIN_SIZE;
+ }
+
+ return false;
+ }
+
+ public long getEstimatedStorageRequirement() {
+ return mEstimatedStorageRequirement;
}
private void importContactsFromLegacyDb() {
@@ -142,14 +167,6 @@ public class LegacyContactImporter {
mDbHelper = (ContactsDatabaseHelper)mContactsProvider.getDatabaseHelper();
mTargetDb = mDbHelper.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 = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
mNoteMimetypeId = mDbHelper.getMimeTypeId(Note.CONTENT_ITEM_TYPE);
mOrganizationMimetypeId = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
@@ -164,27 +181,53 @@ public class LegacyContactImporter {
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();
-
- mDbHelper.updateAllVisible();
+ try {
+ checkForImportFailureTest();
+
+ /*
+ * 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();
+
+ 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();
+
+ mDbHelper.updateAllVisible();
- mTargetDb.setTransactionSuccessful();
- mTargetDb.endTransaction();
+ mTargetDb.setTransactionSuccessful();
+ } finally {
+ mTargetDb.endTransaction();
+ }
importCalls();
}
+ /**
+ * This is used for simulating an import failure. Insert a row into the "settings"
+ * table with key='TEST' and then proceed with the upgrade. Remove the record
+ * after verifying the failure handling.
+ */
+ private void checkForImportFailureTest() {
+ long isTest = DatabaseUtils.longForQuery(mSourceDb,
+ "SELECT COUNT(*) FROM settings WHERE key='TEST'", null);
+ if (isTest != 0) {
+ throw new SQLiteException("Testing import failure.");
+ }
+ }
+
private interface GroupsQuery {
String TABLE = "groups";
@@ -677,7 +720,6 @@ public class LegacyContactImporter {
}
private void insertOrganization(Cursor c, SQLiteStatement insert) {
-
long id = c.getLong(OrganizationsQuery.PERSON);
insert.bindLong(OrganizationInsert.RAW_CONTACT_ID, id);
insert.bindLong(OrganizationInsert.MIMETYPE_ID, mOrganizationMimetypeId);