From bd578a748ab5bd74aa63511cce8769d5882f4651 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Fri, 12 Mar 2010 19:29:10 -0800 Subject: Implementing legacy contact upgrade under low storage conditions Bug: 2498528 Change-Id: Ibd7aa458f665fea71192ce7ff1743f064acb3858 --- res/values/strings.xml | 9 ++ .../providers/contacts/ContactsProvider2.java | 113 ++++++++++++++++----- .../providers/contacts/LegacyContactImporter.java | 106 +++++++++++++------ 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 @@ Contacts + + + Contact upgrade needs more memory + + + Upgrading contact storage + + + Select to complete the upgrade. 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); -- cgit v1.2.3