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) {
// 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,6 +877,7 @@ public class EmailProvider extends ContentProvider {
Uri resultUri = null;
try {
switch (match) {
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
@ -881,7 +899,7 @@ public class EmailProvider extends ContentProvider {
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
// 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);
@ -906,6 +924,10 @@ public class EmailProvider extends ContentProvider {
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)
getContext().getContentResolver().notifyChange(uri, null);
@ -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,6 +984,7 @@ public class EmailProvider extends ContentProvider {
Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match);
}
try {
switch (match) {
case BODY:
case MESSAGE:
@ -968,6 +1019,10 @@ public class EmailProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
}
if ((c != null) && !isTemporary()) {
c.setNotificationUri(getContext().getContentResolver(), notificationUri);
@ -1028,7 +1083,14 @@ 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;
try {
switch (match) {
case MAILBOX_ID_ADD_TO_FIELD:
case ACCOUNT_ID_ADD_TO_FIELD:
@ -1040,7 +1102,8 @@ public class EmailProvider extends ContentProvider {
throw new IllegalArgumentException("No field/add specified " + uri);
}
Cursor c = db.query(TABLE_NAMES[table],
new String[] {EmailContent.RECORD_ID, field}, whereWithId(id, selection),
new String[] {EmailContent.RECORD_ID, field},
whereWithId(id, selection),
selectionArgs, null, null, null);
try {
result = 0;
@ -1091,6 +1154,10 @@ public class EmailProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
} catch (SQLiteException e) {
checkDatabases();
throw e;
}
getContext().getContentResolver().notifyChange(uri, null);
return result;

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