Reenable delete-message UI and write delete-from-provider.

* Longpress delete in MessageList
* Delete or Menu-Delete in MessageView
* Add deleteMessage() to Controller
* Unit tests for the new code in Controller
This commit is contained in:
Andrew Stadler 2009-07-06 13:58:30 -07:00
parent 28448e782b
commit 7c3cca80a0
4 changed files with 279 additions and 10 deletions

View File

@ -19,8 +19,14 @@ package com.android.email;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Store;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Mailbox;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import java.util.HashSet;
@ -33,18 +39,32 @@ public class Controller {
static Controller sInstance;
private Context mContext;
private Context mProviderContext;
private MessagingController mLegacyController;
private HashSet<Result> mListeners = new HashSet<Result>();
private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
EmailContent.RECORD_ID,
EmailContent.MessageColumns.ACCOUNT_KEY
};
private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
private static String[] ACCOUNTID_TO_MAILBOXTYPE_PROJECTION = new String[] {
EmailContent.RECORD_ID,
EmailContent.MailboxColumns.ACCOUNT_KEY,
EmailContent.MailboxColumns.TYPE
};
private static int ACCOUNTID_TO_MAILBOXTYPE_COLUMN_ID = 0;
protected Controller(Context _context) {
mContext = _context;
mProviderContext = _context;
mLegacyController = MessagingController.getInstance(mContext);
}
/**
* Gets or creates the singleton instance of Controller. Application is used to
* provide a Context to classes that need it.
* @param _context
* Gets or creates the singleton instance of Controller.
* @param _context The context that will be used for all underlying system access
*/
public synchronized static Controller getInstance(Context _context) {
if (sInstance == null) {
@ -53,6 +73,15 @@ public class Controller {
return sInstance;
}
/**
* For testing only: Inject a different context for provider access. This will be
* used internally for access the underlying provider (e.g. getContentResolver().query()).
* @param providerContext the provider context to be used by this instance
*/
public void setProviderContext(Context providerContext) {
mProviderContext = providerContext;
}
/**
* Any UI code that wishes for callback results (on async ops) should register their callback
* here (typically from onResume()). Unregistered callbacks will never be called, to prevent
@ -148,6 +177,95 @@ public class Controller {
}.start();
}
/**
* Delete a single message by moving it to the trash.
*
* This function has no callback, no result reporting, because the desired outcome
* is reflected entirely by changes to one or more cursors.
*
* @param messageId The id of the message to "delete".
* @param accountId The id of the message's account, or -1 if not known by caller
*
* TODO: Move out of UI thread
* TODO: "get account a for message m" should be a utility
* TODO: "get mailbox of type n for account a" should be a utility
*/
public void deleteMessage(long messageId, long accountId) {
ContentResolver resolver = mProviderContext.getContentResolver();
// 1. Look up acct# for message we're deleting
Cursor c = null;
if (accountId == -1) {
try {
c = resolver.query(EmailContent.Message.CONTENT_URI,
MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
new String[] { Long.toString(messageId) }, null);
if (c.moveToFirst()) {
accountId = c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID);
} else {
return;
}
} finally {
if (c != null) c.close();
}
}
// 2. Confirm that there is a trash mailbox available
long trashMailboxId = -1;
c = null;
try {
c = resolver.query(EmailContent.Mailbox.CONTENT_URI,
ACCOUNTID_TO_MAILBOXTYPE_PROJECTION,
EmailContent.MailboxColumns.ACCOUNT_KEY + "=? AND " +
EmailContent.MailboxColumns.TYPE + "=" + EmailContent.Mailbox.TYPE_TRASH,
new String[] { Long.toString(accountId) }, null);
if (c.moveToFirst()) {
trashMailboxId = c.getLong(ACCOUNTID_TO_MAILBOXTYPE_COLUMN_ID);
}
} finally {
if (c != null) c.close();
}
// 3. If there's no trash mailbox, create one
// TODO: Does this need to be signaled explicitly to the sync engines?
if (trashMailboxId == -1) {
Mailbox box = new Mailbox();
box.mDisplayName = mContext.getString(R.string.special_mailbox_name_trash);
box.mAccountKey = accountId;
box.mType = Mailbox.TYPE_TRASH;
box.mSyncFrequency = EmailContent.Account.CHECK_INTERVAL_NEVER;
box.mFlagVisible = true;
box.saveOrUpdate(mProviderContext);
trashMailboxId = box.mId;
}
// 4. Change the mailbox key for the message we're "deleting"
ContentValues cv = new ContentValues();
cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
resolver.update(uri, cv, null, null);
// 5. Drop non-essential data for the message (e.g. attachments)
// TODO: find the actual files (if any, if loaded) & delete them
c = null;
try {
c = resolver.query(EmailContent.Attachment.CONTENT_URI,
EmailContent.Attachment.CONTENT_PROJECTION,
EmailContent.AttachmentColumns.MESSAGE_KEY + "=?",
new String[] { Long.toString(messageId) }, null);
while (c.moveToNext()) {
// delete any associated storage
// delete row?
}
} finally {
if (c != null) c.close();
}
// 6. For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings)
// TODO write this
}
/**
* Simple helper to determine if legacy MessagingController should be used
*/

View File

@ -50,6 +50,7 @@ import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;
import java.util.Date;
@ -201,7 +202,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
onOpenMessage(info.id);
break;
case R.id.delete:
//onDelete(holder);
onDelete(info.id);
break;
case R.id.reply:
//onReply(holder);
@ -265,6 +266,11 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
}
}
private void onDelete(long messageId) {
Controller.getInstance(getApplication()).deleteMessage(messageId, -1);
Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
}
private void onToggleRead(long messageId, View itemView) {
// TODO the read-unread state of the given message should be cached in the listview item.
// Instead, we're going to pull it from the DB here (expensively and in the wrong thread)

View File

@ -16,6 +16,7 @@
package com.android.email.activity;
import com.android.email.Controller;
import com.android.email.Email;
import com.android.email.MessagingController;
import com.android.email.MessagingListener;
@ -513,12 +514,9 @@ public class MessageView extends Activity
}
private void onDelete() {
if (mOldMessage != null) {
MessagingController.getInstance(getApplication()).deleteMessage(
mAccount,
mFolder,
mOldMessage,
null);
if (mMessage != null) {
Controller.getInstance(getApplication()).deleteMessage(
mMessageId, mMessage.mAccountKey);
Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
if (onPrevious()) {

View File

@ -0,0 +1,147 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
import android.content.Context;
import android.test.ProviderTestCase2;
/**
* Tests of the Controller class that depend on the underlying provider.
*
* NOTE: It would probably make sense to rewrite this using a MockProvider, instead of the
* ProviderTestCase (which is a real provider running on a temp database). This would be more of
* a true "unit test".
*
* You can run this entire test case with:
* runtest -c com.android.email.ControllerProviderOpsTests email
*/
public class ControllerProviderOpsTests extends ProviderTestCase2<EmailProvider> {
EmailProvider mProvider;
Context mProviderContext;
Context mContext;
public ControllerProviderOpsTests() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
}
@Override
public void setUp() throws Exception {
super.setUp();
mProviderContext = getMockContext();
mContext = getContext();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
/**
* Lightweight subclass of the Controller class allows injection of mock context
*/
public static class TestController extends Controller {
protected TestController(Context providerContext, Context systemContext) {
super(systemContext);
setProviderContext(providerContext);
}
}
/**
* Test the "delete message" function. Sunny day:
* - message/mailbox/account all exist
* - trash mailbox exists
*/
public void testDeleteMessage() {
Account account1 = ProviderTestUtils.setupAccount("message-delete", true, mProviderContext);
long account1Id = account1.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mProviderContext);
long box1Id = box1.mId;
Mailbox box2 = ProviderTestUtils.setupMailbox("box2", account1Id, false, mProviderContext);
box2.mType = EmailContent.Mailbox.TYPE_TRASH;
box2.save(mProviderContext);
long box2Id = box2.mId;
Message message1 = ProviderTestUtils.setupMessage("message1", account1Id, box1Id, false,
true, mProviderContext);
long message1Id = message1.mId;
Controller ct = new TestController(mProviderContext, mContext);
ct.deleteMessage(message1Id, account1Id);
// now read back a fresh copy and confirm it's in the trash
Message message1get = EmailContent.Message.restoreMessageWithId(mProviderContext,
message1Id);
assertEquals(box2Id, message1get.mMailboxKey);
// Now repeat test with accountId "unknown"
Message message2 = ProviderTestUtils.setupMessage("message2", account1Id, box1Id, false,
true, mProviderContext);
long message2Id = message2.mId;
ct.deleteMessage(message2Id, -1);
// now read back a fresh copy and confirm it's in the trash
Message message2get = EmailContent.Message.restoreMessageWithId(mProviderContext,
message2Id);
assertEquals(box2Id, message2get.mMailboxKey);
}
/**
* Test deleting message when there is no trash mailbox
*/
public void testDeleteMessageNoTrash() {
Account account1 =
ProviderTestUtils.setupAccount("message-delete-notrash", true, mProviderContext);
long account1Id = account1.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mProviderContext);
long box1Id = box1.mId;
Message message1 =
ProviderTestUtils.setupMessage("message1", account1Id, box1Id, false, true,
mProviderContext);
long message1Id = message1.mId;
Controller ct = new TestController(mProviderContext, mContext);
ct.deleteMessage(message1Id, account1Id);
// now read back a fresh copy and confirm it's in the trash
Message message1get =
EmailContent.Message.restoreMessageWithId(mProviderContext, message1Id);
// check the new mailbox and see if it looks right
assertFalse(-1 == message1get.mMailboxKey);
assertFalse(box1Id == message1get.mMailboxKey);
Mailbox mailbox2get = EmailContent.Mailbox.restoreMailboxWithId(mProviderContext,
message1get.mMailboxKey);
assertEquals(EmailContent.Mailbox.TYPE_TRASH, mailbox2get.mType);
}
/**
* TODO: releasing associated data (e.g. attachments, embedded images)
*/
}