am f095e9df: Merge change Ib53e4411 into eclair-mr2

Merge commit 'f095e9df5a8dbd97dcefecb479fbcce05ddb4d87' into eclair-mr2-plus-aosp

* commit 'f095e9df5a8dbd97dcefecb479fbcce05ddb4d87':
  Clear out orphaned messages in updates/deletes tables
This commit is contained in:
Marc Blank 2009-10-15 12:09:46 -07:00 committed by Android Git Automerger
commit bdaf801ea2
3 changed files with 272 additions and 14 deletions

View File

@ -54,15 +54,25 @@ public class EmailProvider extends ContentProvider {
private static final String TAG = "EmailProvider"; private static final String TAG = "EmailProvider";
static final String DATABASE_NAME = "EmailProvider.db"; private static final String DATABASE_NAME = "EmailProvider.db";
static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; private static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
// Definitions for our queries looking for orphaned messages
private static final String[] ORPHANS_PROJECTION
= new String[] {MessageColumns.ID, MessageColumns.MAILBOX_KEY};
private static final int ORPHANS_ID = 0;
private static final int ORPHANS_MAILBOX_KEY = 1;
private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
// Any changes to the database format *must* include update-in-place code. // Any changes to the database format *must* include update-in-place code.
// Original version: 3 // Original version: 3
// Version 4: Database wipe required; changing AccountManager interface w/Exchange // Version 4: Database wipe required; changing AccountManager interface w/Exchange
// Version 5: Database wipe required; changing AccountManager interface w/Exchange // Version 5: Database wipe required; changing AccountManager interface w/Exchange
// Version 6: Adding Message.mServerTimeStamp column // Version 6: Adding Message.mServerTimeStamp column
public static final int DATABASE_VERSION = 6; // Version 7: Replace the mailbox_delete trigger with a version that removes orphaned messages
// from the Message_Deletes and Message_Updates tables
public static final int DATABASE_VERSION = 7;
// Any changes to the database format *must* include update-in-place code. // Any changes to the database format *must* include update-in-place code.
// Original version: 2 // Original version: 2
@ -162,6 +172,17 @@ public class EmailProvider extends ContentProvider {
private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?"; private static final String ID_EQUALS = EmailContent.RECORD_ID + "=?";
private static final String TRIGGER_MAILBOX_DELETE =
"create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME +
" begin" +
" delete from " + Message.TABLE_NAME +
" where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
"; delete from " + Message.UPDATED_TABLE_NAME +
" where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
"; delete from " + Message.DELETED_TABLE_NAME +
" where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
"; end";
static { static {
// Email URI matching table // Email URI matching table
UriMatcher matcher = sURIMatcher; UriMatcher matcher = sURIMatcher;
@ -450,11 +471,8 @@ public class EmailProvider extends ContentProvider {
+ " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")"); + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.SERVER_ID + ")");
db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY db.execSQL("create index mailbox_" + MailboxColumns.ACCOUNT_KEY
+ " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")"); + " on " + Mailbox.TABLE_NAME + " (" + MailboxColumns.ACCOUNT_KEY + ")");
// Deleting a Mailbox deletes associated Messages // Deleting a Mailbox deletes associated Messages in all three tables
db.execSQL("create trigger mailbox_delete before delete on " + Mailbox.TABLE_NAME + db.execSQL(TRIGGER_MAILBOX_DELETE);
" begin delete from " + Message.TABLE_NAME +
" where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID +
"; end");
} }
static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) { static void resetMailboxTable(SQLiteDatabase db, int oldVersion, int newVersion) {
@ -540,9 +558,67 @@ public class EmailProvider extends ContentProvider {
mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase");
} }
} }
// Check for any orphaned Messages in the updated/deleted tables
deleteOrphans(mDatabase, Message.UPDATED_TABLE_NAME);
deleteOrphans(mDatabase, Message.DELETED_TABLE_NAME);
return mDatabase; return mDatabase;
} }
/*package*/ static SQLiteDatabase getReadableDatabase(Context context) {
DatabaseHelper helper = new EmailProvider().new DatabaseHelper(context, DATABASE_NAME);
return helper.getReadableDatabase();
}
/*package*/ static void deleteOrphans(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);
// Usually, there will be nothing in these tables, so make a quick check
try {
if (c.getCount() == 0) return;
ArrayList<Long> foundMailboxes = new ArrayList<Long>();
ArrayList<Long> notFoundMailboxes = new ArrayList<Long>();
ArrayList<Long> deleteList = new ArrayList<Long>();
String[] bindArray = new String[1];
while (c.moveToNext()) {
// Get the mailbox key and see if we've already found this mailbox
// If so, we're fine
long mailboxId = c.getLong(ORPHANS_MAILBOX_KEY);
// If we already know this mailbox doesn't exist, mark the message for deletion
if (notFoundMailboxes.contains(mailboxId)) {
deleteList.add(c.getLong(ORPHANS_ID));
// If we don't know about this mailbox, we'll try to find it
} else if (!foundMailboxes.contains(mailboxId)) {
bindArray[0] = Long.toString(mailboxId);
Cursor boxCursor = database.query(Mailbox.TABLE_NAME,
Mailbox.ID_PROJECTION, WHERE_ID, bindArray, null, null, null);
try {
// If it exists, we'll add it to the "found" mailboxes
if (boxCursor.moveToFirst()) {
foundMailboxes.add(mailboxId);
// Otherwise, we'll add to "not found" and mark the message for deletion
} else {
notFoundMailboxes.add(mailboxId);
deleteList.add(c.getLong(ORPHANS_ID));
}
} finally {
boxCursor.close();
}
}
}
// Now, delete the orphan messages
for (long messageId: deleteList) {
bindArray[0] = Long.toString(messageId);
database.delete(tableName, WHERE_ID, bindArray);
}
} finally {
c.close();
}
}
}
private class BodyDatabaseHelper extends SQLiteOpenHelper { private class BodyDatabaseHelper extends SQLiteOpenHelper {
BodyDatabaseHelper(Context context, String name) { BodyDatabaseHelper(Context context, String name) {
super(context, name, null, BODY_DATABASE_VERSION); super(context, name, null, BODY_DATABASE_VERSION);
@ -614,6 +690,12 @@ public class EmailProvider extends ContentProvider {
} }
oldVersion = 6; oldVersion = 6;
} }
if (oldVersion == 6) {
// Use the newer mailbox_delete trigger
db.execSQL("delete trigger mailbox_delete;");
db.execSQL(TRIGGER_MAILBOX_DELETE);
oldVersion = 7;
}
} }
@Override @Override
@ -779,6 +861,8 @@ public class EmailProvider extends ContentProvider {
Uri resultUri = null; Uri resultUri = null;
switch (match) { switch (match) {
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case BODY: case BODY:
case MESSAGE: case MESSAGE:
case ATTACHMENT: case ATTACHMENT:
@ -787,6 +871,13 @@ public class EmailProvider extends ContentProvider {
case HOSTAUTH: case HOSTAUTH:
id = db.insert(TABLE_NAMES[table], "foo", values); id = db.insert(TABLE_NAMES[table], "foo", values);
resultUri = ContentUris.withAppendedId(uri, id); resultUri = ContentUris.withAppendedId(uri, id);
// Clients shouldn't normally be adding rows to these tables, as they are
// maintained by triggers. However, we need to be able to do this for unit
// testing, so we allow the insert and then throw the same exception that we
// would if this weren't allowed.
if (match == UPDATED_MESSAGE || match == DELETED_MESSAGE) {
throw new IllegalArgumentException("Unknown URL " + uri);
}
break; break;
case MAILBOX_ID: case MAILBOX_ID:
// This implies adding a message to a mailbox // This implies adding a message to a mailbox

View File

@ -142,7 +142,7 @@ public class SyncManager extends Service implements Runnable {
protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE = protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ',' MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ')'; + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ')';
private static final String WHERE_MAILBOX_KEY = EmailContent.RECORD_ID + "=?"; private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" + private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
AbstractSyncService.EAS_PROTOCOL + "\""; AbstractSyncService.EAS_PROTOCOL + "\"";
private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN = private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
@ -1147,6 +1147,10 @@ public class SyncManager extends Service implements Runnable {
// We ignore drafts completely (doesn't sync). Changes in Outbox are handled // We ignore drafts completely (doesn't sync). Changes in Outbox are handled
// in the checkMailboxes loop, so we can ignore these pings. // in the checkMailboxes loop, so we can ignore these pings.
if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) { if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
String[] args = new String[] {Long.toString(m.mId)};
ContentResolver resolver = INSTANCE.mResolver;
resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
return; return;
} }
service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey); service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);

View File

@ -169,6 +169,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
/** /**
* Test the various combinations of SSL, TLS, and trust-certificates encoded as Uris * Test the various combinations of SSL, TLS, and trust-certificates encoded as Uris
*/ */
@SuppressWarnings("deprecation")
public void testHostAuthSecurityUri() { public void testHostAuthSecurityUri() {
HostAuth ha = ProviderTestUtils.setupHostAuth("uri-security", 1, false, mMockContext); HostAuth ha = ProviderTestUtils.setupHostAuth("uri-security", 1, false, mMockContext);
@ -209,6 +210,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
/** /**
* Test port assignments made from Uris * Test port assignments made from Uris
*/ */
@SuppressWarnings("deprecation")
public void testHostAuthPortAssignments() { public void testHostAuthPortAssignments() {
HostAuth ha = ProviderTestUtils.setupHostAuth("uri-port", 1, false, mMockContext); HostAuth ha = ProviderTestUtils.setupHostAuth("uri-port", 1, false, mMockContext);
@ -656,7 +658,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
public void testDeleteOrphanBodies() { public void testDeleteOrphanBodies() {
final ContentResolver resolver = mMockContext.getContentResolver(); final ContentResolver resolver = mMockContext.getContentResolver();
// Create account and twa mailboxes // Create account and two mailboxes
Account account1 = ProviderTestUtils.setupAccount("orphaned body", true, mMockContext); Account account1 = ProviderTestUtils.setupAccount("orphaned body", true, mMockContext);
long account1Id = account1.mId; long account1Id = account1.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext); Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext);
@ -686,6 +688,130 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertNotNull(loadBodyForMessageId(message2Id)); assertNotNull(loadBodyForMessageId(message2Id));
} }
/**
* Test delete orphan messages
* 1. create message without body (message id 1)
* 2. create message with body (message id 2. Body has _id 1 and messageKey 2).
* 3. delete first message.
* 4. delete some other mailbox -- this triggers delete orphan bodies.
* 5. verify that body for message 2 has not been deleted.
*/
public void testDeleteOrphanMessages() {
final ContentResolver resolver = mMockContext.getContentResolver();
final Context context = mMockContext;
// Create account and two mailboxes
Account acct = ProviderTestUtils.setupAccount("orphaned body", true, context);
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct.mId, true, context);
// Create 4 messages in box1
Message msg1_1 =
ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, false, true, context);
Message msg1_2 =
ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, false, true, context);
Message msg1_3 =
ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, false, true, context);
Message msg1_4 =
ProviderTestUtils.setupMessage("message4", acct.mId, box1.mId, false, true, context);
// Create 4 messages in box2
Message msg2_1 =
ProviderTestUtils.setupMessage("message1", acct.mId, box2.mId, false, true, context);
Message msg2_2 =
ProviderTestUtils.setupMessage("message2", acct.mId, box2.mId, false, true, context);
Message msg2_3 =
ProviderTestUtils.setupMessage("message3", acct.mId, box2.mId, false, true, context);
Message msg2_4 =
ProviderTestUtils.setupMessage("message4", acct.mId, box2.mId, false, true, context);
// Delete 2 from each mailbox
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg1_1.mId),
null, null);
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg1_2.mId),
null, null);
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg2_1.mId),
null, null);
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg2_2.mId),
null, null);
// There should be 4 items in the deleted item table
assertEquals(4, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
// Update 2 from each mailbox
ContentValues v = new ContentValues();
v.put(MessageColumns.DISPLAY_NAME, "--updated--");
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg1_3.mId),
v, null, null);
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg1_4.mId),
v, null, null);
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg2_3.mId),
v, null, null);
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, msg2_4.mId),
v, null, null);
// There should be 4 items in the updated item table
assertEquals(4, EmailContent.count(context, Message.UPDATED_CONTENT_URI, null, null));
// Manually add 2 messages from a "deleted" mailbox to deleted and updated tables
// Use a value > 2 for the deleted box id
long delBoxId = 10;
// Create 4 messages in the "deleted" mailbox
Message msgX_A =
ProviderTestUtils.setupMessage("messageA", acct.mId, delBoxId, false, false, context);
Message msgX_B =
ProviderTestUtils.setupMessage("messageB", acct.mId, delBoxId, false, false, context);
Message msgX_C =
ProviderTestUtils.setupMessage("messageC", acct.mId, delBoxId, false, false, context);
Message msgX_D =
ProviderTestUtils.setupMessage("messageD", acct.mId, delBoxId, false, false, context);
ContentValues cv;
// We have to assign id's manually because there are no autoincrement id's for these tables
// Start with an id that won't exist, since id's in these tables must be unique
long msgId = 10;
// It's illegal to manually insert these, so we need to catch the exception
// NOTE: The insert succeeds, and then throws the exception
try {
cv = msgX_A.toContentValues();
cv.put(EmailContent.RECORD_ID, msgId++);
resolver.insert(Message.DELETED_CONTENT_URI, cv);
} catch (IllegalArgumentException e) {
}
try {
cv = msgX_B.toContentValues();
cv.put(EmailContent.RECORD_ID, msgId++);
resolver.insert(Message.DELETED_CONTENT_URI, cv);
} catch (IllegalArgumentException e) {
}
try {
cv = msgX_C.toContentValues();
cv.put(EmailContent.RECORD_ID, msgId++);
resolver.insert(Message.UPDATED_CONTENT_URI, cv);
} catch (IllegalArgumentException e) {
}
try {
cv = msgX_D.toContentValues();
cv.put(EmailContent.RECORD_ID, msgId++);
resolver.insert(Message.UPDATED_CONTENT_URI, cv);
} catch (IllegalArgumentException e) {
}
// There should be 6 items in the deleted and updated tables
assertEquals(6, EmailContent.count(context, Message.UPDATED_CONTENT_URI, null, null));
assertEquals(6, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
// Delete the orphans
EmailProvider.deleteOrphans(EmailProvider.getReadableDatabase(context),
Message.DELETED_TABLE_NAME);
EmailProvider.deleteOrphans(EmailProvider.getReadableDatabase(context),
Message.UPDATED_TABLE_NAME);
// There should now be 4 messages in each of the deleted and updated tables again
assertEquals(4, EmailContent.count(context, Message.UPDATED_CONTENT_URI, null, null));
assertEquals(4, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
}
/** /**
* Test delete mailbox * Test delete mailbox
*/ */
@ -961,10 +1087,16 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
long account1Id = account1.mId; long account1Id = account1.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext); Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext);
long box1Id = box1.mId; long box1Id = box1.mId;
/* Message message1 = */ ProviderTestUtils.setupMessage("message1", account1Id, box1Id, Message message1 = ProviderTestUtils.setupMessage("message1", account1Id, box1Id,
false, true, mMockContext); false, true, mMockContext);
/* Message message2 = */ ProviderTestUtils.setupMessage("message2", account1Id, box1Id, Message message2 = ProviderTestUtils.setupMessage("message2", account1Id, box1Id,
false, true, mMockContext); false, true, mMockContext);
Message message3 = ProviderTestUtils.setupMessage("message3", account1Id, box1Id,
false, true, mMockContext);
Message message4 = ProviderTestUtils.setupMessage("message4", account1Id, box1Id,
false, true, mMockContext);
ProviderTestUtils.setupMessage("message5", account1Id, box1Id, false, true, mMockContext);
ProviderTestUtils.setupMessage("message6", account1Id, box1Id, false, true, mMockContext);
String selection = EmailContent.MessageColumns.ACCOUNT_KEY + "=? AND " + String selection = EmailContent.MessageColumns.ACCOUNT_KEY + "=? AND " +
EmailContent.MessageColumns.MAILBOX_KEY + "=?"; EmailContent.MessageColumns.MAILBOX_KEY + "=?";
@ -972,15 +1104,46 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
// make sure there are two messages // make sure there are two messages
int numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs); int numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs);
assertEquals(6, numMessages);
ContentValues cv = new ContentValues();
cv.put(Message.SERVER_ID, "SERVER_ID");
ContentResolver resolver = mMockContext.getContentResolver();
// Update two messages
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, message1.mId),
cv, null, null);
resolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, message2.mId),
cv, null, null);
// Delete two messages
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, message3.mId),
null, null);
resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, message4.mId),
null, null);
// There should now be two messages in updated/deleted, and 4 in messages
numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs);
assertEquals(4, numMessages);
numMessages = EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, selection,
selArgs);
assertEquals(2, numMessages);
numMessages = EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, selection,
selArgs);
assertEquals(2, numMessages); assertEquals(2, numMessages);
// now delete the mailbox // now delete the mailbox
Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, box1Id); Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, box1Id);
mMockContext.getContentResolver().delete(uri, null, null); resolver.delete(uri, null, null);
// there should now be zero messages // there should now be zero messages in all three tables
numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs); numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs);
assertEquals(0, numMessages); assertEquals(0, numMessages);
numMessages = EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, selection,
selArgs);
assertEquals(0, numMessages);
numMessages = EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, selection,
selArgs);
assertEquals(0, numMessages);
} }
/** /**