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:
commit
9dc9f44ba3
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user