am 0e1595c1: Handle unexpected deletion of EmailProvider database

Merge commit '0e1595c177e40428b267a8696dfc05d015ce6a2f' into eclair-mr2-plus-aosp

* commit '0e1595c177e40428b267a8696dfc05d015ce6a2f':
  Handle unexpected deletion of EmailProvider database
This commit is contained in:
Marc Blank 2009-12-09 15:42:33 -08:00 committed by Android Git Automerger
commit 9dc9f44ba3
2 changed files with 330 additions and 143 deletions

View File

@ -44,18 +44,23 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
public class EmailProvider extends ContentProvider {
private static final String TAG = "EmailProvider";
private static final String DATABASE_NAME = "EmailProvider.db";
private static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
protected static final String DATABASE_NAME = "EmailProvider.db";
protected static final String BODY_DATABASE_NAME = "EmailProviderBody.db";
public static final Uri INTEGRITY_CHECK_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/integrityCheck");
// Definitions for our queries looking for orphaned messages
private static final String[] ORPHANS_PROJECTION
@ -65,6 +70,8 @@ public class EmailProvider extends ContentProvider {
private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
private static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
// Any changes to the database format *must* include update-in-place code.
// Original version: 3
// Version 4: Database wipe required; changing AccountManager interface w/Exchange
@ -543,9 +550,15 @@ public class EmailProvider extends ContentProvider {
private SQLiteDatabase mBodyDatabase;
public synchronized SQLiteDatabase getDatabase(Context context) {
if (mDatabase != null) {
// Always return the cached database, if we've got one
if (mDatabase != null) {
return mDatabase;
}
// Whenever we create or re-cache the databases, make sure that we haven't lost one
// to corruption
checkDatabases();
DatabaseHelper helper = new DatabaseHelper(context, DATABASE_NAME);
mDatabase = helper.getWritableDatabase();
if (mDatabase != null) {
@ -626,7 +639,7 @@ public class EmailProvider extends ContentProvider {
@Override
public void onCreate(SQLiteDatabase db) {
// Create all tables here; each class has its own method
Log.d(TAG, "Creating EmailProviderBody database");
createBodyTable(db);
}
@ -650,6 +663,7 @@ public class EmailProvider extends ContentProvider {
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(TAG, "Creating EmailProvider database");
// Create all tables here; each class has its own method
createMessageTable(db);
createAttachmentTable(db);
@ -797,6 +811,9 @@ public class EmailProvider extends ContentProvider {
}
db.setTransactionSuccessful();
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
} finally {
if (messageDeletion) {
db.endTransaction();
@ -860,51 +877,56 @@ public class EmailProvider extends ContentProvider {
Uri resultUri = null;
switch (match) {
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case BODY:
case MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
id = db.insert(TABLE_NAMES[table], "foo", values);
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) {
try {
switch (match) {
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case BODY:
case MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
id = db.insert(TABLE_NAMES[table], "foo", values);
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;
case MAILBOX_ID:
// This implies adding a message to a mailbox
// Hmm, a problem here is that we can't link the account as well, so it must be
// already in the values...
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(MessageColumns.MAILBOX_KEY, id);
resultUri = insert(Message.CONTENT_URI, values);
break;
case MESSAGE_ID:
// This implies adding an attachment to a message.
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(AttachmentColumns.MESSAGE_KEY, id);
resultUri = insert(Attachment.CONTENT_URI, values);
break;
case ACCOUNT_ID:
// This implies adding a mailbox to an account.
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(MailboxColumns.ACCOUNT_KEY, id);
resultUri = insert(Mailbox.CONTENT_URI, values);
break;
case ATTACHMENTS_MESSAGE_ID:
id = db.insert(TABLE_NAMES[table], "foo", values);
resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
break;
case MAILBOX_ID:
// This implies adding a message to a mailbox
// Hmm, one problem here is that we can't link the account as well, so it must be
// already in the values...
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(MessageColumns.MAILBOX_KEY, id);
resultUri = insert(Message.CONTENT_URI, values);
break;
case MESSAGE_ID:
// This implies adding an attachment to a message.
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(AttachmentColumns.MESSAGE_KEY, id);
resultUri = insert(Attachment.CONTENT_URI, values);
break;
case ACCOUNT_ID:
// This implies adding a mailbox to an account.
id = Long.parseLong(uri.getPathSegments().get(1));
values.put(MailboxColumns.ACCOUNT_KEY, id);
resultUri = insert(Mailbox.CONTENT_URI, values);
break;
case ATTACHMENTS_MESSAGE_ID:
id = db.insert(TABLE_NAMES[table], "foo", values);
resultUri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
}
// Notify with the base uri, not the new uri (nobody is watching a new record)
@ -914,10 +936,38 @@ public class EmailProvider extends ContentProvider {
@Override
public boolean onCreate() {
// TODO Auto-generated method stub
checkDatabases();
return false;
}
/**
* The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must
* always be in sync (i.e. there are two database or NO databases). This code will delete
* any "orphan" database, so that both will be created together. Note that an "orphan" database
* will exist after either of the individual databases is deleted due to data corruption.
*/
public void checkDatabases() {
// Uncache the databases
if (mDatabase != null) {
mDatabase = null;
}
if (mBodyDatabase != null) {
mBodyDatabase = null;
}
// Look for orphans, and delete as necessary; these must always be in sync
File databaseFile = getContext().getDatabasePath(DATABASE_NAME);
File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME);
// TODO Make sure attachments are deleted
if (databaseFile.exists() && !bodyFile.exists()) {
Log.w(TAG, "Deleting orphaned EmailProvider database...");
databaseFile.delete();
} else if (bodyFile.exists() && !databaseFile.exists()) {
Log.w(TAG, "Deleting orphaned EmailProviderBody database...");
bodyFile.delete();
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
@ -934,39 +984,44 @@ public class EmailProvider extends ContentProvider {
Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match);
}
switch (match) {
case BODY:
case MESSAGE:
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
c = db.query(TABLE_NAMES[table], projection,
selection, selectionArgs, null, null, sortOrder);
break;
case BODY_ID:
case MESSAGE_ID:
case DELETED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
case HOSTAUTH_ID:
id = uri.getPathSegments().get(1);
c = db.query(TABLE_NAMES[table], projection,
whereWithId(id, selection), selectionArgs, null, null, sortOrder);
break;
case ATTACHMENTS_MESSAGE_ID:
// All attachments for the given message
id = uri.getPathSegments().get(2);
c = db.query(Attachment.TABLE_NAME, projection,
whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
try {
switch (match) {
case BODY:
case MESSAGE:
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
c = db.query(TABLE_NAMES[table], projection,
selection, selectionArgs, null, null, sortOrder);
break;
case BODY_ID:
case MESSAGE_ID:
case DELETED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
case HOSTAUTH_ID:
id = uri.getPathSegments().get(1);
c = db.query(TABLE_NAMES[table], projection,
whereWithId(id, selection), selectionArgs, null, null, sortOrder);
break;
case ATTACHMENTS_MESSAGE_ID:
// All attachments for the given message
id = uri.getPathSegments().get(2);
c = db.query(Attachment.TABLE_NAME, projection,
whereWith(Attachment.MESSAGE_KEY + "=" + id, selection),
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
}
if ((c != null) && !isTemporary()) {
@ -1028,68 +1083,80 @@ public class EmailProvider extends ContentProvider {
values.remove(MailboxColumns.UNREAD_COUNT);
}
// Handle this special case the fastest possible way
if (uri == INTEGRITY_CHECK_URI) {
checkDatabases();
return 0;
}
String id;
switch (match) {
case MAILBOX_ID_ADD_TO_FIELD:
case ACCOUNT_ID_ADD_TO_FIELD:
db.beginTransaction();
id = uri.getPathSegments().get(1);
String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME);
Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME);
if (field == null || add == null) {
throw new IllegalArgumentException("No field/add specified " + uri);
}
Cursor c = db.query(TABLE_NAMES[table],
new String[] {EmailContent.RECORD_ID, field}, whereWithId(id, selection),
selectionArgs, null, null, null);
try {
result = 0;
ContentValues cv = new ContentValues();
String[] bind = new String[1];
while (c.moveToNext()) {
bind[0] = c.getString(0);
long value = c.getLong(1) + add;
cv.put(field, value);
result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind);
try {
switch (match) {
case MAILBOX_ID_ADD_TO_FIELD:
case ACCOUNT_ID_ADD_TO_FIELD:
db.beginTransaction();
id = uri.getPathSegments().get(1);
String field = values.getAsString(EmailContent.FIELD_COLUMN_NAME);
Long add = values.getAsLong(EmailContent.ADD_COLUMN_NAME);
if (field == null || add == null) {
throw new IllegalArgumentException("No field/add specified " + uri);
}
} finally {
c.close();
}
db.setTransactionSuccessful();
db.endTransaction();
break;
case BODY_ID:
case MESSAGE_ID:
case SYNCED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
case HOSTAUTH_ID:
id = uri.getPathSegments().get(1);
if (match == SYNCED_MESSAGE_ID) {
// For synced messages, first copy the old message to the updated table
// Note the insert or ignore semantics, guaranteeing that only the first
// update will be reflected in the updated message table; therefore this row
// will always have the "original" data
db.execSQL(UPDATED_MESSAGE_INSERT + id);
} else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);
}
result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection),
selectionArgs);
break;
case BODY:
case MESSAGE:
case UPDATED_MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
result = db.update(TABLE_NAMES[table], values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
Cursor c = db.query(TABLE_NAMES[table],
new String[] {EmailContent.RECORD_ID, field},
whereWithId(id, selection),
selectionArgs, null, null, null);
try {
result = 0;
ContentValues cv = new ContentValues();
String[] bind = new String[1];
while (c.moveToNext()) {
bind[0] = c.getString(0);
long value = c.getLong(1) + add;
cv.put(field, value);
result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind);
}
} finally {
c.close();
}
db.setTransactionSuccessful();
db.endTransaction();
break;
case BODY_ID:
case MESSAGE_ID:
case SYNCED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
case HOSTAUTH_ID:
id = uri.getPathSegments().get(1);
if (match == SYNCED_MESSAGE_ID) {
// For synced messages, first copy the old message to the updated table
// Note the insert or ignore semantics, guaranteeing that only the first
// update will be reflected in the updated message table; therefore this row
// will always have the "original" data
db.execSQL(UPDATED_MESSAGE_INSERT + id);
} else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);
}
result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection),
selectionArgs);
break;
case BODY:
case MESSAGE:
case UPDATED_MESSAGE:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
result = db.update(TABLE_NAMES[table], values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
}
getContext().getContentResolver().notifyChange(uri, null);

View File

@ -1102,7 +1102,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
EmailContent.MessageColumns.MAILBOX_KEY + "=?";
String[] selArgs = new String[] { String.valueOf(account1Id), String.valueOf(box1Id) };
// make sure there are two messages
// make sure there are six messages
int numMessages = EmailContent.count(mMockContext, Message.CONTENT_URI, selection, selArgs);
assertEquals(6, numMessages);
@ -1515,4 +1515,124 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
Mailbox restoredBoxA = Mailbox.restoreMailboxWithId(mMockContext, boxA.mId);
assertEquals(11, restoredBoxA.mUnreadCount);
}
public void testDatabaseCorruptionRecovery() {
final ContentResolver resolver = mMockContext.getContentResolver();
final Context context = mMockContext;
// Create account and two mailboxes
Account acct = ProviderTestUtils.setupAccount("acct1", true, context);
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
// Create 4 messages in box1 with bodies
ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message4", acct.mId, box1.mId, true, true, context);
// Confirm there are four messages
int count = EmailContent.count(mMockContext, Message.CONTENT_URI, null, null);
assertEquals(4, count);
// Confirm there are four bodies
count = EmailContent.count(mMockContext, Body.CONTENT_URI, null, null);
assertEquals(4, count);
// Find the EmailProvider.db file
File dbFile = mMockContext.getDatabasePath(EmailProvider.DATABASE_NAME);
// The EmailProvider.db database should exist (the provider creates it automatically)
assertTrue(dbFile != null);
assertTrue(dbFile.exists());
// Delete it, and confirm it is gone
assertTrue(dbFile.delete());
assertFalse(dbFile.exists());
// Find the EmailProviderBody.db file
dbFile = mMockContext.getDatabasePath(EmailProvider.BODY_DATABASE_NAME);
// The EmailProviderBody.db database should still exist
assertTrue(dbFile != null);
assertTrue(dbFile.exists());
// URI to uncache the databases
// This simulates the Provider starting up again (otherwise, it will still be pointing to
// the already opened files)
// Note that we only have access to the EmailProvider via the ContentResolver; therefore,
// we cannot directly call into the provider and use a URI for this
resolver.update(EmailProvider.INTEGRITY_CHECK_URI, null, null, null);
// TODO We should check for the deletion of attachment files once this is implemented in
// the provider
// Explanation for what happens below...
// The next time the database is created by the provider, it will notice that there's
// already a EmailProviderBody.db file. In this case, it will delete that database to
// ensure that both are in sync (and empty)
// Confirm there are no bodies
count = EmailContent.count(mMockContext, Body.CONTENT_URI, null, null);
assertEquals(0, count);
// Confirm there are no messages
count = EmailContent.count(mMockContext, Message.CONTENT_URI, null, null);
assertEquals(0, count);
}
public void testBodyDatabaseCorruptionRecovery() {
final ContentResolver resolver = mMockContext.getContentResolver();
final Context context = mMockContext;
// Create account and two mailboxes
Account acct = ProviderTestUtils.setupAccount("acct1", true, context);
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
// Create 4 messages in box1 with bodies
ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
ProviderTestUtils.setupMessage("message4", acct.mId, box1.mId, true, true, context);
// Confirm there are four messages
int count = EmailContent.count(mMockContext, Message.CONTENT_URI, null, null);
assertEquals(4, count);
// Confirm there are four bodies
count = EmailContent.count(mMockContext, Body.CONTENT_URI, null, null);
assertEquals(4, count);
// Find the EmailProviderBody.db file
File dbFile = mMockContext.getDatabasePath(EmailProvider.BODY_DATABASE_NAME);
// The EmailProviderBody.db database should exist (the provider creates it automatically)
assertTrue(dbFile != null);
assertTrue(dbFile.exists());
// Delete it, and confirm it is gone
assertTrue(dbFile.delete());
assertFalse(dbFile.exists());
// Find the EmailProvider.db file
dbFile = mMockContext.getDatabasePath(EmailProvider.DATABASE_NAME);
// The EmailProviderBody.db database should still exist
assertTrue(dbFile != null);
assertTrue(dbFile.exists());
// URI to uncache the databases
// This simulates the Provider starting up again (otherwise, it will still be pointing to
// the already opened files)
// Note that we only have access to the EmailProvider via the ContentResolver; therefore,
// we cannot directly call into the provider and use a URI for this
resolver.update(EmailProvider.INTEGRITY_CHECK_URI, null, null, null);
// TODO We should check for the deletion of attachment files once this is implemented in
// the provider
// Explanation for what happens below...
// The next time the body database is created by the provider, it will notice that there's
// already a populated EmailProvider.db file. In this case, it will delete that database to
// ensure that both are in sync (and empty)
// Confirm there are no messages
count = EmailContent.count(mMockContext, Message.CONTENT_URI, null, null);
assertEquals(0, count);
// Confirm there are no bodies
count = EmailContent.count(mMockContext, Body.CONTENT_URI, null, null);
assertEquals(0, count);
}
}