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));
+ }
}