Delete orphaned mailboxes/messages/policies at provider startup

Bug: 4972132

Change-Id: Icc756a9e28b77de052261903089b040d229e7b5d
This commit is contained in:
Marc Blank 2011-06-30 16:03:36 -07:00
parent 09e7fffb2a
commit 2bdf7ee0f0
3 changed files with 103 additions and 18 deletions

View File

@ -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";

View File

@ -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 <table> where <column> not in (select <foreignColumn> from <foreignTable>)
* @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);
}

View File

@ -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<EmailProvider> {
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<EmailProvider> {
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));
}
}