Extract MessageListTask and make it self-contained.

Introducing MessageOrderManager which maintains a message list for
MessageView.  It's used to tell if there is newer/older messages
in a mailbox, and the id of them.

Also, slightly related to this, moved mWaitForLoadMessageId to
ControllerResults where it should belong.

Change-Id: I84e32180c7e84a317f2204bb10ad7245ec022dca
This commit is contained in:
Makoto Onuki 2010-06-30 18:22:41 -07:00
parent e60d3648fd
commit de0a1c33c9
3 changed files with 701 additions and 146 deletions

View File

@ -0,0 +1,297 @@
/*
* Copyright (C) 2010 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.activity;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Handler;
/**
* Used by {@link MessageView} to determine the message-id of the previous/next messages.
*
* All public methods must be called on the main thread.
*
* Call {@link #moveTo} to set the current message id. As a result,
* either {@link Callback#onMessagesChanged} or {@link Callback#onMessageNotFound} is called.
*
* Use {@link #canMoveToNewer()} and {@link #canMoveToOlder()} to see if there is a newer/older
* message, and {@link #moveToNewer()} and {@link #moveToOlder()} to update the current position.
*
* If the message list changes (e.g. message removed, new message arrived, etc), {@link Callback}
* gets called again.
*
* When an instance is no longer needed, call {@link #close()}, which closes an underlying cursor
* and shuts down an async task.
*
* TODO: Is there better words than "newer"/"older" that works even if we support other sort orders
* than timestamp?
*/
public class MessageOrderManager {
private final Context mContext;
private final ContentResolver mContentResolver;
private final long mMailboxId;
private final ContentObserver mObserver;
private final Callback mCallback;
private LoadMessageListTask mLoadMessageListTask;
private Cursor mCursor;
private long mCurrentMessageId = -1;
private boolean mClosed = false;
public interface Callback {
/**
* Called when the message set by {@link MessageOrderManager#moveTo(long)} is found in the
* mailbox. {@link #canMoveToOlder}, {@link #canMoveToNewer}, {@link #moveToOlder} and
* {@link #moveToNewer} are ready to be called.
*/
public void onMessagesChanged();
/**
* Called when the message set by {@link MessageOrderManager#moveTo(long)} is not found.
*/
public void onMessageNotFound();
}
public MessageOrderManager(Context context, long mailboxId, Callback callback) {
mContext = context.getApplicationContext();
mContentResolver = mContext.getContentResolver();
mMailboxId = mailboxId;
mCallback = callback;
mObserver = new ContentObserver(getHandlerForContentObserver()) {
@Override public void onChange(boolean selfChange) {
if (mClosed) {
return;
}
onContentChanged();
}
};
startTask();
}
/**
* @return a {@link Handler} for {@link ContentObserver}.
*
* Unit tests override this and return null, so that {@link ContentObserver#onChange} is
* called synchronously.
*/
/* package */ Handler getHandlerForContentObserver() {
return new Handler();
}
private boolean isTaskRunning() {
return mLoadMessageListTask != null;
}
private void startTask() {
cancelTask();
startQuery();
}
/**
* Start {@link LoadMessageListTask} to query DB.
* Unit tests override this to make tests synchronous and to inject a mock query.
*/
/* package */ void startQuery() {
mLoadMessageListTask = new LoadMessageListTask();
mLoadMessageListTask.execute();
}
private void cancelTask() {
Utility.cancelTaskInterrupt(mLoadMessageListTask);
mLoadMessageListTask = null;
}
private void closeCursor() {
if (mCursor != null) {
mCursor.close();
mCursor = null;
}
}
private void setCurrentMessageIdFromCursor() {
if (mCursor != null) {
mCurrentMessageId = mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
}
}
private void onContentChanged() {
if (!isTaskRunning()) { // Start only if not running already.
startTask();
}
}
/**
* Shutdown itself and release resources.
*/
public void close() {
mClosed = true;
cancelTask();
closeCursor();
}
public long getCurrentMessageId() {
return mCurrentMessageId;
}
/**
* Set the current message id. As a result, either {@link Callback#onMessagesChanged} or
* {@link Callback#onMessageNotFound} is called.
*/
public void moveTo(long messageId) {
if (mCurrentMessageId != messageId) {
mCurrentMessageId = messageId;
adjustCursorPosition();
}
}
private void adjustCursorPosition() {
if (mCurrentMessageId == -1) {
return; // Current ID not specified yet.
}
if (mCursor == null) {
// Task not finished yet.
// We call adjustCursorPosition() again when we've opened a cursor.
return;
}
mCursor.moveToPosition(-1);
while (mCursor.moveToNext()
&& mCursor.getLong(EmailContent.ID_PROJECTION_COLUMN) != mCurrentMessageId) {
}
if (mCursor.isAfterLast()) {
mCallback.onMessageNotFound(); // Message not found... Already deleted?
} else {
mCallback.onMessagesChanged();
}
}
/**
* @return true if the message set to {@link #moveTo} has an older message in the mailbox.
* false otherwise, or unknown yet.
*/
public boolean canMoveToOlder() {
return (mCursor != null) && !mCursor.isLast();
}
/**
* @return true if the message set to {@link #moveTo} has an newer message in the mailbox.
* false otherwise, or unknown yet.
*/
public boolean canMoveToNewer() {
return (mCursor != null) && !mCursor.isFirst();
}
/**
* Move to the older message.
*
* @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
*/
public boolean moveToOlder() {
if (canMoveToOlder() && mCursor.moveToNext()) {
setCurrentMessageIdFromCursor();
mCallback.onMessagesChanged();
return true;
} else {
return false;
}
}
/**
* Move to the newer message.
*
* @return true iif succeed, and {@link Callback#onMessagesChanged} is called.
*/
public boolean moveToNewer() {
if (canMoveToNewer() && mCursor.moveToPrevious()) {
setCurrentMessageIdFromCursor();
mCallback.onMessagesChanged();
return true;
} else {
return false;
}
}
/**
* Task to open a Cursor on a worker thread.
*/
private class LoadMessageListTask extends AsyncTask<Void, Void, Cursor> {
@Override
protected Cursor doInBackground(Void... params) {
return openNewCursor();
}
@Override
protected void onCancelled() {
onCursorOpenDone(null);
}
@Override
protected void onPostExecute(Cursor cursor) {
if (mClosed || isCancelled()) { // Is this really necessary??
if (cursor != null) {
cursor.close();
}
onCancelled();
} else {
onCursorOpenDone(cursor);
}
}
}
/* package */ String getQuerySelection() { // Extracted for testing
return Utility.buildMailboxIdSelection(mContentResolver, mMailboxId);
}
/**
* Open a new cursor for a message list.
*
* This method is called on a worker thread by LoadMessageListTask.
*/
private Cursor openNewCursor() {
final Cursor cursor = mContentResolver.query(EmailContent.Message.CONTENT_URI,
EmailContent.ID_PROJECTION, getQuerySelection(), null,
EmailContent.MessageColumns.TIMESTAMP + " DESC");
return cursor;
}
/**
* Called when {@link #openNewCursor()} is finished.
*
* Unit tests call this directly to inject a mock cursor.
*/
/* package */ void onCursorOpenDone(Cursor cursor) {
try {
closeCursor();
if (cursor == null || cursor.isClosed()) {
return; // Task canceled
}
mCursor = cursor;
mCursor.registerContentObserver(mObserver);
adjustCursorPosition();
} finally {
mLoadMessageListTask = null; // isTaskRunning() becomes false.
}
}
}

View File

@ -28,7 +28,6 @@ import com.android.email.mail.PackedString;
import com.android.email.mail.internet.EmailHtmlUtil;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.Message;
@ -42,7 +41,6 @@ import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -83,7 +81,7 @@ import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MessageView extends Activity implements OnClickListener {
public class MessageView extends Activity implements OnClickListener, MessageOrderManager.Callback {
private static final String EXTRA_MESSAGE_ID = "com.android.email.MessageView_message_id";
private static final String EXTRA_MAILBOX_ID = "com.android.email.MessageView_mailbox_id";
/* package */ static final String EXTRA_DISABLE_REPLY =
@ -139,13 +137,14 @@ public class MessageView extends Activity implements OnClickListener {
private long mMessageId;
private long mMailboxId;
private Message mMessage;
private long mWaitForLoadMessageId;
private LoadMessageTask mLoadMessageTask;
private LoadBodyTask mLoadBodyTask;
private LoadAttachmentsTask mLoadAttachmentsTask;
private PresenceUpdater mPresenceUpdater;
private MessageOrderManager mOrderManager;
private long mLoadAttachmentId; // the attachment being saved/viewed
private boolean mLoadAttachmentSave; // if true, saving - if false, viewing
private String mLoadAttachmentName; // the display name
@ -157,13 +156,10 @@ public class MessageView extends Activity implements OnClickListener {
private Drawable mFavoriteIconOff;
private Controller mController;
private Controller.Result mControllerCallback;
private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
private View mMoveToNewer;
private View mMoveToOlder;
private LoadMessageListTask mLoadMessageListTask;
private Cursor mMessageListCursor;
private ContentObserver mCursorObserver;
// contains the HTML body. Is used by LoadAttachmentTask to display inline images.
// is null most of the time, is used transiently to pass info to LoadAttachementTask
@ -278,26 +274,6 @@ public class MessageView extends Activity implements OnClickListener {
}
mController = Controller.getInstance(getApplication());
// Set up ContentObserver.
// This observer is used to watch for external changes to the message list.
// Pass a Handler so that onChange() gets called back in the UI thread.
// (All we want to do here is to run an AsyncTask, so it could run on a bg thread, but doing
// so would require synchronization to protect mLoadMessageListTask. Let's just do it on
// the UI thread to keep it simple.)
mCursorObserver = new ContentObserver(new Handler()){
@Override
public void onChange(boolean selfChange) {
// get a new message list cursor, but only if we already had one
// (otherwise it's "too soon" and other pathways will cause it to be loaded)
if (mLoadMessageListTask == null && mMessageListCursor != null) {
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
mLoadMessageListTask.execute();
}
}
};
messageChanged();
}
@ -335,7 +311,6 @@ public class MessageView extends Activity implements OnClickListener {
@Override
public void onResume() {
super.onResume();
mWaitForLoadMessageId = -1;
mController.addResultCallback(mControllerCallback);
// Exit immediately if the accounts list has changed (e.g. externally deleted)
@ -347,29 +322,20 @@ public class MessageView extends Activity implements OnClickListener {
if (mMessage != null) {
startPresenceCheck();
// get a new message list cursor, but only if mailbox is set
// (otherwise it's "too soon" and other pathways will cause it to be loaded)
if (mLoadMessageListTask == null && mMailboxId != -1) {
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
mLoadMessageListTask.execute();
}
}
if (!isViewingEmailFile()) {
mOrderManager = new MessageOrderManager(this, mMailboxId, this);
}
}
@Override
public void onPause() {
mController.removeResultCallback(mControllerCallback);
closeMessageListCursor();
super.onPause();
}
private void closeMessageListCursor() {
if (mMessageListCursor != null) {
mMessageListCursor.unregisterContentObserver(mCursorObserver);
mMessageListCursor.close();
mMessageListCursor = null;
if (mOrderManager != null) {
mOrderManager.close();
mOrderManager = null;
}
super.onPause();
}
private void cancelAllTasks() {
@ -379,8 +345,6 @@ public class MessageView extends Activity implements OnClickListener {
mLoadBodyTask = null;
Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
mLoadAttachmentsTask = null;
Utility.cancelTaskInterrupt(mLoadMessageListTask);
mLoadMessageListTask = null;
mPresenceUpdater.cancelAll();
}
@ -390,6 +354,9 @@ public class MessageView extends Activity implements OnClickListener {
*/
@Override
public void onDestroy() {
if (mOrderManager != null) {
mOrderManager.close();
}
cancelAllTasks();
mMessageContentView.destroy();
mMessageContentView = null;
@ -561,10 +528,8 @@ public class MessageView extends Activity implements OnClickListener {
}
// Guard with !isLast() because Cursor.moveToNext() returns false even as it moves
// from last to after-last.
if (mMessageListCursor != null
&& !mMessageListCursor.isLast()
&& mMessageListCursor.moveToNext()) {
mMessageId = mMessageListCursor.getLong(0);
if (mOrderManager != null && mOrderManager.moveToOlder()) {
mMessageId = mOrderManager.getCurrentMessageId();
messageChanged();
return true;
}
@ -577,10 +542,8 @@ public class MessageView extends Activity implements OnClickListener {
}
// Guard with !isFirst() because Cursor.moveToPrev() returns false even as it moves
// from first to before-first.
if (mMessageListCursor != null
&& !mMessageListCursor.isFirst()
&& mMessageListCursor.moveToPrevious()) {
mMessageId = mMessageListCursor.getLong(0);
if (mOrderManager != null && mOrderManager.moveToNewer()) {
mMessageId = mOrderManager.getCurrentMessageId();
messageChanged();
return true;
}
@ -802,50 +765,17 @@ public class MessageView extends Activity implements OnClickListener {
// Start an AsyncTask to make a new cursor and load the message
mLoadMessageTask = new LoadMessageTask(mFileEmailUri, mMessageId, true);
mLoadMessageTask.execute();
updateNavigationArrows(mMessageListCursor);
}
/**
* Reposition the older/newer cursor. Finish() the activity if we are no longer
* in the list. Update the UI arrows as appropriate.
*/
private void repositionMessageListCursor() {
if (Email.DEBUG) {
Email.log("MessageView: reposition to id=" + mMessageId);
}
// position the cursor on the current message
mMessageListCursor.moveToPosition(-1);
while (mMessageListCursor.moveToNext() && mMessageListCursor.getLong(0) != mMessageId) {
}
if (mMessageListCursor.isAfterLast()) {
// overshoot - get out now, the list is no longer valid
finish();
}
updateNavigationArrows(mMessageListCursor);
updateNavigationArrows();
}
/**
* Update the arrows based on the current position of the older/newer cursor.
*/
private void updateNavigationArrows(Cursor cursor) {
if (isViewingEmailFile()) {
mMoveToNewer.setVisibility(View.INVISIBLE);
mMoveToOlder.setVisibility(View.INVISIBLE);
return;
}
if (cursor != null) {
boolean hasNewer, hasOlder;
if (cursor.isAfterLast() || cursor.isBeforeFirst()) {
// The cursor not being on a message means that the current message was not found.
// While this should not happen, simply disable prev/next arrows in that case.
hasNewer = hasOlder = false;
} else {
hasNewer = !cursor.isFirst();
hasOlder = !cursor.isLast();
}
mMoveToNewer.setVisibility(hasNewer ? View.VISIBLE : View.INVISIBLE);
mMoveToOlder.setVisibility(hasOlder ? View.VISIBLE : View.INVISIBLE);
}
private void updateNavigationArrows() {
mMoveToNewer.setVisibility((mOrderManager != null) && mOrderManager.canMoveToNewer()
? View.VISIBLE : View.INVISIBLE);
mMoveToOlder.setVisibility((mOrderManager != null) && mOrderManager.canMoveToOlder()
? View.VISIBLE : View.INVISIBLE);
}
private Bitmap getPreviewIcon(AttachmentInfo attachment) {
@ -971,50 +901,6 @@ public class MessageView extends Activity implements OnClickListener {
}
}
/**
* This task finds out the messageId for the previous and next message
* in the order given by mailboxId as used in MessageList.
*
* It generates the same cursor as the one used in MessageList (but with an id-only projection),
* scans through it until finds the current messageId, and takes the previous and next ids.
*/
private class LoadMessageListTask extends AsyncTask<Void, Void, Cursor> {
private long mLocalMailboxId;
public LoadMessageListTask(long mailboxId) {
mLocalMailboxId = mailboxId;
}
@Override
protected Cursor doInBackground(Void... params) {
String selection =
Utility.buildMailboxIdSelection(getContentResolver(), mLocalMailboxId);
Cursor c = getContentResolver().query(EmailContent.Message.CONTENT_URI,
EmailContent.ID_PROJECTION,
selection, null,
EmailContent.MessageColumns.TIMESTAMP + " DESC");
return c;
}
@Override
protected void onPostExecute(Cursor cursor) {
if (cursor == null) {
return;
}
// remove the reference to ourselves so another one can be launched
MessageView.this.mLoadMessageListTask = null;
if (cursor.isClosed()) {
return;
}
// replace the older cursor if there is one
closeMessageListCursor();
mMessageListCursor = cursor;
mMessageListCursor.registerContentObserver(MessageView.this.mCursorObserver);
repositionMessageListCursor();
}
}
/**
* Async task for loading a single message outside of the UI thread
*/
@ -1189,10 +1075,9 @@ public class MessageView extends Activity implements OnClickListener {
if (mMailboxId == -1) {
mMailboxId = message.mMailboxKey;
}
// only start LoadMessageListTask here if it's the first time
if (mMessageListCursor == null) {
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
mLoadMessageListTask.execute();
if (mOrderManager != null) {
mOrderManager.moveTo(message.mId);
}
mSubjectView.setText(message.mSubject);
@ -1216,10 +1101,10 @@ public class MessageView extends Activity implements OnClickListener {
// 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
// 4. Else start the loader tasks right away (message already loaded)
if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
mWaitForLoadMessageId = message.mId;
mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId);
mController.loadMessageForView(message.mId);
} else {
mWaitForLoadMessageId = -1;
mControllerCallback.getWrappee().setWaitForLoadMessageId(-1);
// Ask for body
mLoadBodyTask = new LoadBodyTask(message.mId);
mLoadBodyTask.execute();
@ -1302,11 +1187,16 @@ public class MessageView extends Activity implements OnClickListener {
* so all methods are called on the UI thread.
*/
private class ControllerResults extends Controller.Result {
private long mWaitForLoadMessageId;
public void setWaitForLoadMessageId(long messageId) {
mWaitForLoadMessageId = messageId;
}
@Override
public void loadMessageForViewCallback(MessagingException result, long messageId,
int progress) {
if (messageId != MessageView.this.mMessageId
|| messageId != MessageView.this.mWaitForLoadMessageId) {
if (messageId != mWaitForLoadMessageId) {
// We are not waiting for this message to load, so exit quickly
return;
}
@ -1490,4 +1380,14 @@ public class MessageView extends Activity implements OnClickListener {
}
}
}
@Override
public void onMessageNotFound() {
finish();
}
@Override
public void onMessagesChanged() {
updateNavigationArrows();
}
}

View File

@ -0,0 +1,358 @@
/*
* Copyright (C) 2010 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.activity;
import com.android.email.Email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import android.content.Context;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.os.Handler;
import android.test.ProviderTestCase2;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.Assert;
@SmallTest
public class MessageOrderManagerTest extends ProviderTestCase2<EmailProvider> {
private MyCallback mCallback;
@Override protected void setUp() throws Exception {
super.setUp();
mCallback = new MyCallback();
}
public MessageOrderManagerTest() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
}
private static void assertCanMove(MessageOrderManager mom, boolean newer, boolean older) {
Assert.assertEquals(older, mom.canMoveToOlder());
Assert.assertEquals(newer, mom.canMoveToNewer());
}
public void testBasic() {
MessageOrderManagerForTest mom = new MessageOrderManagerForTest(getContext(), 1, mCallback);
mom.assertStartQueryCalledAndReset();
// moveTo not called, so it returns -1
assertEquals(-1, mom.getCurrentMessageId());
// Task not finished, so all returns false.
assertCanMove(mom, false, false);
assertFalse(mom.moveToNewer());
assertFalse(mom.moveToOlder());
// Set current message
mom.moveTo(54);
assertEquals(54, mom.getCurrentMessageId());
// Task still not finished, so all returns false.
assertCanMove(mom, false, false);
assertFalse(mom.moveToNewer());
assertFalse(mom.moveToOlder());
// Both callbacks shouldn't have called.
mCallback.assertCallbacksCalled(false, false);
}
public void testSelection() {
MessageOrderManagerForTest mom = new MessageOrderManagerForTest(getContext(), 5, mCallback);
assertEquals("flagLoaded IN (2,1) AND mailboxKey=5", mom.getQuerySelection());
}
/**
* Test with actual message list.
*
* In this test, {@link MessageOrderManager#moveTo} is called AFTER the cursor opens.
*/
public void testWithList() {
MessageOrderManagerForTest mom = new MessageOrderManagerForTest(getContext(), 1, mCallback);
mom.assertStartQueryCalledAndReset();
// Callback not called yet.
mCallback.assertCallbacksCalled(false, false);
// Inject mock cursor. (Imitate async query done.)
MyCursor cursor = new MyCursor(11, 22, 33, 44); // Newer to older
mom.onCursorOpenDone(cursor);
// Current message id not set yet, so callback should have called yet.
mCallback.assertCallbacksCalled(false, false);
// Set current message id -- now onMessagesChanged() should get called.
mom.moveTo(22);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
// Move to row 1
assertTrue(mom.moveToNewer());
assertEquals(11, mom.getCurrentMessageId());
assertCanMove(mom, false, true);
mCallback.assertCallbacksCalled(true, false);
// Try to move to newer, but no newer messages
assertFalse(mom.moveToNewer());
assertEquals(11, mom.getCurrentMessageId()); // Still row 1
mCallback.assertCallbacksCalled(false, false);
// Move to row 2
assertTrue(mom.moveToOlder());
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
mCallback.assertCallbacksCalled(true, false);
// Move to row 3
assertTrue(mom.moveToOlder());
assertEquals(33, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
mCallback.assertCallbacksCalled(true, false);
// Move to row 4
assertTrue(mom.moveToOlder());
assertEquals(44, mom.getCurrentMessageId());
assertCanMove(mom, true, false);
mCallback.assertCallbacksCalled(true, false);
// Try to move older, but no Older messages
assertFalse(mom.moveToOlder());
mCallback.assertCallbacksCalled(false, false);
// Move to row 3
assertTrue(mom.moveToNewer());
assertEquals(33, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
mCallback.assertCallbacksCalled(true, false);
}
/**
* Test with actual message list.
*
* In this test, {@link MessageOrderManager#moveTo} is called BEFORE the cursor opens.
*/
public void testWithList2() {
MessageOrderManagerForTest mom = new MessageOrderManagerForTest(getContext(), 1, mCallback);
mom.assertStartQueryCalledAndReset();
// Callback not called yet.
mCallback.assertCallbacksCalled(false, false);
mom.moveTo(22);
mCallback.assertCallbacksCalled(false, false); // Cursor not open, callback not called yet.
assertEquals(22, mom.getCurrentMessageId());
// Inject mock cursor. (Imitate async query done.)
MyCursor cursor = new MyCursor(11, 22, 33, 44); // Newer to older
mom.onCursorOpenDone(cursor);
// As soon as the cursor opens, callback gets called.
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
}
public void testContentChanged() {
MessageOrderManagerForTest mom = new MessageOrderManagerForTest(getContext(), 1, mCallback);
// Inject mock cursor. (Imitate async query done.)
MyCursor cursor = new MyCursor(11, 22, 33, 44); // Newer to older
mom.onCursorOpenDone(cursor);
// Move to 22
mom.moveTo(22);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
// Delete 33
mom.updateMessageList(11, 22, 44);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
// Delete 44
mom.updateMessageList(11, 22);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, false); // Can't move to older
// Append 55
mom.updateMessageList(11, 22, 55);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, true, true);
// Delete 11
mom.updateMessageList(22, 55);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, false, true);
// Delete 55
mom.updateMessageList(22);
mCallback.assertCallbacksCalled(true, false);
assertEquals(22, mom.getCurrentMessageId());
assertCanMove(mom, false, false); // Can't move either way
// Now the current message is gone... messageNotFound gets called.
mom.updateMessageList(11, 33, 44);
mCallback.assertCallbacksCalled(false, true);
}
/**
* Test using the actual {@link MessageOrderManager} rather than
* {@link MessageOrderManagerForTest}.
*/
public void testWithActualClass() {
// There are not many things we can test synchronously.
// Just open & close just to make sure it won't crash.
MessageOrderManager mom = new MessageOrderManager(getContext(), 1, new MyCallback());
mom.moveTo(123);
mom.close();
}
private static class MyCallback implements MessageOrderManager.Callback {
public boolean mCalledOnMessageNotFound;
public boolean mCalledOnMessagesChanged;
@Override public void onMessagesChanged() {
mCalledOnMessagesChanged = true;
}
@Override public void onMessageNotFound() {
mCalledOnMessageNotFound = true;
}
/**
* Asserts that the callbacks have/have not been called, and reset the flags.
*/
public void assertCallbacksCalled(boolean messagesChanged, boolean messageNotFound) {
assertEquals(messagesChanged, mCalledOnMessagesChanged);
assertEquals(messageNotFound, mCalledOnMessageNotFound);
mCalledOnMessagesChanged = false;
mCalledOnMessageNotFound = false;
}
}
/**
* MessageOrderManager for test. Overrides {@link #startQuery}
*/
private static class MessageOrderManagerForTest extends MessageOrderManager {
private Cursor mLastCursor;
public boolean mStartQueryCalled;
public MessageOrderManagerForTest(Context context, long mailboxId, Callback callback) {
super(context, mailboxId, callback);
}
@Override void startQuery() {
// To make tests synchronous, we replace this method.
mStartQueryCalled = true;
}
@Override /* package */ Handler getHandlerForContentObserver() {
return null;
}
@Override void onCursorOpenDone(Cursor cursor) {
super.onCursorOpenDone(cursor);
mLastCursor = cursor;
}
/**
* Utility method to emulate data set changed.
*/
public void updateMessageList(long... idList) {
assertNotNull(mLastCursor); // Make sure a cursor is set.
// Notify dataset change -- it should end up startQuery() gets called.
((MyCursor) mLastCursor).notifyChanged();
assertStartQueryCalledAndReset(); // Start
// Set a new cursor with a new list.
onCursorOpenDone(new MyCursor(idList));
}
public void assertStartQueryCalledAndReset() {
assertTrue(mStartQueryCalled);
mStartQueryCalled = false;
}
}
private static class MyCursor extends AbstractCursor {
private long[] mList;
public MyCursor(long... idList) {
mList = (idList == null) ? new long[0] : idList;
}
public void notifyChanged() {
onChange(false);
}
@Override public int getColumnCount() {
return 1;
}
@Override public int getCount() {
return mList.length;
}
@Override public String[] getColumnNames() {
return new String[] {EmailContent.RECORD_ID};
}
@Override public long getLong(int columnIndex) {
Assert.assertEquals(EmailContent.ID_PROJECTION_COLUMN, columnIndex);
return mList[mPos];
}
@Override public double getDouble(int column) {
throw new junit.framework.AssertionFailedError();
}
@Override public float getFloat(int column) {
throw new junit.framework.AssertionFailedError();
}
@Override public int getInt(int column) {
throw new junit.framework.AssertionFailedError();
}
@Override public short getShort(int column) {
throw new junit.framework.AssertionFailedError();
}
@Override public String getString(int column) {
throw new junit.framework.AssertionFailedError();
}
@Override public boolean isNull(int column) {
throw new junit.framework.AssertionFailedError();
}
}
}