From 2bdf7ee0f0f4a2b11b5f7c0f8b193080600fefd8 Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Thu, 30 Jun 2011 16:03:36 -0700 Subject: [PATCH] Delete orphaned mailboxes/messages/policies at provider startup Bug: 4972132 Change-Id: Icc756a9e28b77de052261903089b040d229e7b5d --- .../android/emailcommon/provider/Policy.java | 2 +- .../android/email/provider/EmailProvider.java | 49 +++++++++---- .../android/email/provider/ProviderTests.java | 70 ++++++++++++++++++- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/emailcommon/src/com/android/emailcommon/provider/Policy.java b/emailcommon/src/com/android/emailcommon/provider/Policy.java index 1747c3f47..89933462b 100644 --- a/emailcommon/src/com/android/emailcommon/provider/Policy.java +++ b/emailcommon/src/com/android/emailcommon/provider/Policy.java @@ -40,7 +40,7 @@ import java.util.ArrayList; */ public final class Policy extends EmailContent implements EmailContent.PolicyColumns, Parcelable { // STOPSHIP Change to false after a few days of debugging - public static final boolean DEBUG_POLICY = true; // DO NOT SUBMIT WITH THIS SET TO FALSE + public static final boolean DEBUG_POLICY = false; // DO NOT SUBMIT WITH THIS SET TO TRUE public static final String TAG = "Email/Policy"; public static final String TABLE_NAME = "Policy"; diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index eb148961c..5a8963de0 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -786,6 +786,25 @@ public class EmailProvider extends ContentProvider { private SQLiteDatabase mDatabase; private SQLiteDatabase mBodyDatabase; + /** + * Orphan record deletion utility. Generates a sqlite statement like: + * delete from where not in (select from ) + * @param db the EmailProvider database + * @param table the table whose orphans are to be removed + * @param column the column deletion will be based on + * @param foreignColumn the column in the foreign table whose absence will trigger the deletion + * @param foreignTable the foreign table + */ + @VisibleForTesting + void deleteUnlinked(SQLiteDatabase db, String table, String column, String foreignColumn, + String foreignTable) { + int count = db.delete(table, column + " not in (select " + foreignColumn + " from " + + foreignTable + ")", null); + if (count > 0) { + Log.w(TAG, "Found " + count + " orphaned row(s) in " + table); + } + } + @VisibleForTesting synchronized SQLiteDatabase getDatabase(Context context) { // Always return the cached database, if we've got one @@ -811,15 +830,26 @@ public class EmailProvider extends ContentProvider { // Restore accounts if the database is corrupted... restoreIfNeeded(context, mDatabase); + if (Email.DEBUG) { + Log.d(TAG, "Deleting orphans..."); + } // Check for any orphaned Messages in the updated/deleted tables - deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME); - deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME); + deleteMessageOrphans(mDatabase, Message.UPDATED_TABLE_NAME); + deleteMessageOrphans(mDatabase, Message.DELETED_TABLE_NAME); + // Delete orphaned mailboxes/messages/policies (account no longer exists) + deleteUnlinked(mDatabase, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, + Account.TABLE_NAME); + deleteUnlinked(mDatabase, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, + Account.TABLE_NAME); + deleteUnlinked(mDatabase, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, + Account.TABLE_NAME); + if (Email.DEBUG) { Log.d(TAG, "EmailProvider pre-caching..."); } preCacheData(); if (Email.DEBUG) { - Log.d(TAG, "Pre-caching finished."); + Log.d(TAG, "EmailProvider ready."); } return mDatabase; } @@ -959,7 +989,7 @@ public class EmailProvider extends ContentProvider { } } - /*package*/ static void deleteOrphans(SQLiteDatabase database, String tableName) { + /*package*/ static void deleteMessageOrphans(SQLiteDatabase database, String tableName) { if (database != null) { // We'll look at all of the items in the table; there won't be many typically Cursor c = database.query(tableName, ORPHANS_PROJECTION, null, null, null, null, null); @@ -1808,17 +1838,6 @@ public class EmailProvider extends ContentProvider { } c = db.query(tableName, projection, whereWithId(id, selection), selectionArgs, null, null, sortOrder, limit); - if (Email.DEBUG) { - switch(match) { - case ACCOUNT_ID: - case HOSTAUTH_ID: - case POLICY_ID: - case MAILBOX_ID: - Log.w(Logging.LOG_TAG, - "==== UNCACHED read of " + tableName + ", id = " + id); - break; - } - } if (cache != null) { c = cache.putCursor(c, id, projection, token); } diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 4b4ac963c..1c1abf746 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -43,6 +43,7 @@ import com.android.emailcommon.provider.EmailContent.BodyColumns; import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; +import com.android.emailcommon.provider.EmailContent.PolicyColumns; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Policy; @@ -953,9 +954,9 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(6, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null)); // Delete the orphans - EmailProvider.deleteOrphans(EmailProvider.getReadableDatabase(context), + EmailProvider.deleteMessageOrphans(EmailProvider.getReadableDatabase(context), Message.DELETED_TABLE_NAME); - EmailProvider.deleteOrphans(EmailProvider.getReadableDatabase(context), + EmailProvider.deleteMessageOrphans(EmailProvider.getReadableDatabase(context), Message.UPDATED_TABLE_NAME); // There should now be 4 messages in each of the deleted and updated tables again @@ -2459,4 +2460,69 @@ public class ProviderTests extends ProviderTestCase2 { cleanupTestAccountManagerAccounts(accountManager); } } + + public void testCleanupOrphans() { + EmailProvider ep = getProvider(); + SQLiteDatabase db = ep.getDatabase(mMockContext); + + Account a = ProviderTestUtils.setupAccount("account1", true, mMockContext); + // Mailbox a1 and a3 won't have a valid account + Mailbox a1 = createTypeMailbox(mMockContext, -1, Mailbox.TYPE_INBOX); + Mailbox a2 = createTypeMailbox(mMockContext, a.mId, Mailbox.TYPE_MAIL); + Mailbox a3 = createTypeMailbox(mMockContext, -1, Mailbox.TYPE_DRAFTS); + Mailbox a4 = createTypeMailbox(mMockContext, a.mId, Mailbox.TYPE_SENT); + Mailbox a5 = createTypeMailbox(mMockContext, a.mId, Mailbox.TYPE_TRASH); + // Mailbox ax isn't even saved; use an obviously invalid id + Mailbox ax = new Mailbox(); + ax.mId = 69105; + + // Message mt2 is an orphan, as is mt4 + Message m1 = createMessage(mMockContext, a1, true, false, Message.FLAG_LOADED_COMPLETE); + Message m2 = createMessage(mMockContext, a2, true, false, Message.FLAG_LOADED_COMPLETE); + Message m3 = createMessage(mMockContext, a3, true, false, Message.FLAG_LOADED_COMPLETE); + Message m4 = createMessage(mMockContext, a4, true, false, Message.FLAG_LOADED_COMPLETE); + Message m5 = createMessage(mMockContext, a5, true, false, Message.FLAG_LOADED_COMPLETE); + Message mx = createMessage(mMockContext, ax, true, false, Message.FLAG_LOADED_COMPLETE); + + // Two orphan policies + Policy p1 = new Policy(); + p1.save(mMockContext); + Policy p2 = new Policy(); + p2.save(mMockContext); + Policy p3 = new Policy(); + Policy.setAccountPolicy(mMockContext, a.mId, p3, "0"); + + // We don't want anything cached or the tests below won't work. Note that + // deleteUnlinked is only called by EmailProvider when the caches are empty + ContentCache.invalidateAllCaches(); + // Delete orphaned mailboxes/messages/policies + ep.deleteUnlinked(db, Mailbox.TABLE_NAME, MailboxColumns.ACCOUNT_KEY, AccountColumns.ID, + Account.TABLE_NAME); + ep.deleteUnlinked(db, Message.TABLE_NAME, MessageColumns.ACCOUNT_KEY, AccountColumns.ID, + Account.TABLE_NAME); + ep.deleteUnlinked(db, Policy.TABLE_NAME, PolicyColumns.ID, AccountColumns.POLICY_KEY, + Account.TABLE_NAME); + + // Make sure the orphaned mailboxes are gone + assertNull(Mailbox.restoreMailboxWithId(mMockContext, a1.mId)); + assertNotNull(Mailbox.restoreMailboxWithId(mMockContext, a2.mId)); + assertNull(Mailbox.restoreMailboxWithId(mMockContext, a3.mId)); + assertNotNull(Mailbox.restoreMailboxWithId(mMockContext, a4.mId)); + assertNotNull(Mailbox.restoreMailboxWithId(mMockContext, a5.mId)); + assertNull(Mailbox.restoreMailboxWithId(mMockContext, ax.mId)); + + // Make sure orphaned messages are gone + assertNull(Message.restoreMessageWithId(mMockContext, m1.mId)); + assertNotNull(Message.restoreMessageWithId(mMockContext, m2.mId)); + assertNull(Message.restoreMessageWithId(mMockContext, m3.mId)); + assertNotNull(Message.restoreMessageWithId(mMockContext, m4.mId)); + assertNotNull(Message.restoreMessageWithId(mMockContext, m5.mId)); + assertNull(Message.restoreMessageWithId(mMockContext, mx.mId)); + + // Make sure orphaned policies are gone + assertNull(Policy.restorePolicyWithId(mMockContext, p1.mId)); + assertNull(Policy.restorePolicyWithId(mMockContext, p2.mId)); + a = Account.restoreAccountWithId(mMockContext, a.mId); + assertNotNull(Policy.restorePolicyWithId(mMockContext, a.mPolicyKey)); + } }