diff --git a/res/values/strings.xml b/res/values/strings.xml index cf225e8dd..50baae45b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -21,6 +21,14 @@ + + + + Sending messages\u2026 + + Send outgoing messages + Read Email attachments @@ -109,9 +117,7 @@ Choose attachment - Loading messages\u2026 - - Sending messages\u2026 + Loading messages\u2026 Connection error @@ -200,9 +206,6 @@ Load more messages - - Send outgoing messages To @@ -721,5 +724,4 @@ Email 1 Pane Change orientation - diff --git a/src/com/android/email/activity/AccountSelectorAdapter.java b/src/com/android/email/activity/AccountSelectorAdapter.java index 3d1ebe0e6..ccfccbadd 100644 --- a/src/com/android/email/activity/AccountSelectorAdapter.java +++ b/src/com/android/email/activity/AccountSelectorAdapter.java @@ -16,6 +16,7 @@ package com.android.email.activity; +import com.android.email.data.NoAutoRequeryCursorLoader; import com.android.email.provider.EmailContent; import android.content.Context; @@ -77,20 +78,4 @@ public class AccountSelectorAdapter extends CursorAdapter { public static long getAccountId(Cursor c) { return c.getLong(ID_COLUMN); } - - /** - * Same as {@link CursorLoader} but it doesn't auto-requery when it gets content-changed - * notifications. - */ - private static class NoAutoRequeryCursorLoader extends CursorLoader { - public NoAutoRequeryCursorLoader(Context context, Uri uri, String[] projection, - String selection, String[] selectionArgs, String sortOrder) { - super(context, uri, projection, selection, selectionArgs, sortOrder); - } - - @Override - public void onContentChanged() { - // Don't reload. - } - } } diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index 61b7291aa..6e9eb6d1d 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -206,7 +206,7 @@ public class MessageList extends Activity implements OnClickListener, // Specific mailbox ID was provided - go directly to it mSetTitleTask = new SetTitleTask(mailboxId); mSetTitleTask.execute(); - mListFragment.openMailbox(-1, mailboxId); + mListFragment.openMailbox(mailboxId); } else { int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX); Uri uri = intent.getData(); @@ -308,7 +308,8 @@ public class MessageList extends Activity implements OnClickListener, } public void onAnimationEnd(Animation animation) { - mListFragment.updateListPosition(); + // TODO: If the button panel hides the only selected item, scroll the list to make it + // visible again. } public void onAnimationRepeat(Animation animation) { @@ -661,7 +662,7 @@ public class MessageList extends Activity implements OnClickListener, public void onMailboxFound(long accountId, long mailboxId) { mSetTitleTask = new SetTitleTask(mailboxId); mSetTitleTask.execute(); - mListFragment.openMailbox(accountId, mailboxId); + mListFragment.openMailbox(mailboxId); } @Override diff --git a/src/com/android/email/activity/MessageListFragment.java b/src/com/android/email/activity/MessageListFragment.java index 03d548acb..8bc2f7f8c 100644 --- a/src/com/android/email/activity/MessageListFragment.java +++ b/src/com/android/email/activity/MessageListFragment.java @@ -20,21 +20,18 @@ import com.android.email.Controller; import com.android.email.Email; import com.android.email.R; import com.android.email.Utility; +import com.android.email.data.MailboxAccountLoader; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Mailbox; -import com.android.email.provider.EmailContent.MailboxColumns; -import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.service.MailService; import android.app.Activity; import android.app.ListFragment; -import android.content.ContentResolver; -import android.content.ContentUris; +import android.app.LoaderManager; import android.content.Context; +import android.content.Loader; import android.database.Cursor; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -50,57 +47,51 @@ import java.security.InvalidParameterException; import java.util.HashSet; import java.util.Set; -public class MessageListFragment extends ListFragment implements OnItemClickListener, - OnItemLongClickListener, MessagesAdapter.Callback { - private static final String STATE_SELECTED_ITEM_TOP = - "com.android.email.activity.MessageList.selectedItemTop"; - private static final String STATE_SELECTED_POSITION = - "com.android.email.activity.MessageList.selectedPosition"; - private static final String STATE_CHECKED_ITEMS = - "com.android.email.activity.MessageList.checkedItems"; +// TODO Better handling of restoring list position/adapter check status +/** + * Message list. + * + *

This fragment uses two different loaders to load data. + *

+ * We run them sequentially. i.e. First starts {@link MailboxAccountLoader}, and when it finishes + * starts the other. + */ +public class MessageListFragment extends ListFragment + implements OnItemClickListener, OnItemLongClickListener, MessagesAdapter.Callback { + + private static final int LOADER_ID_MAILBOX_LOADER = 1; + private static final int LOADER_ID_MESSAGES_LOADER = 2; // UI Support private Activity mActivity; private Callback mCallback = EmptyCallback.INSTANCE; + private View mListFooterView; private TextView mListFooterText; private View mListFooterProgress; private static final int LIST_FOOTER_MODE_NONE = 0; - private static final int LIST_FOOTER_MODE_REFRESH = 1; private static final int LIST_FOOTER_MODE_MORE = 2; - private static final int LIST_FOOTER_MODE_SEND = 3; private int mListFooterMode; private MessagesAdapter mListAdapter; - // DB access - private ContentResolver mResolver; - private long mAccountId = -1; private long mMailboxId = -1; - private LoadMessagesTask mLoadMessagesTask; - private SetFooterTask mSetFooterTask; - - /* package */ static final String[] MESSAGE_PROJECTION = new String[] { - EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, - MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, - MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, - MessageColumns.FLAGS, - }; + private long mLastLoadedMailboxId = -1; + private Account mAccount; + private Mailbox mMailbox; // Controller access private Controller mController; // Misc members - private Boolean mPushModeMailbox = null; - private int mSavedItemTop = 0; - private int mSavedItemPosition = -1; - private int mFirstSelectedItemTop = 0; - private int mFirstSelectedItemPosition = -1; - private int mFirstSelectedItemHeight = -1; - private boolean mCanAutoRefresh; + private boolean mDoAutoRefresh; - private boolean mStarted; + /** true between {@link #onResume} and {@link #onPause}. */ + private boolean mResumed; /** * Callback interface that owning activities must implement @@ -158,9 +149,7 @@ public class MessageListFragment extends ListFragment implements OnItemClickList } super.onCreate(savedInstanceState); mActivity = getActivity(); - mResolver = mActivity.getContentResolver(); mController = Controller.getInstance(mActivity); - mCanAutoRefresh = true; } @Override @@ -180,8 +169,6 @@ public class MessageListFragment extends ListFragment implements OnItemClickList mListFooterView = getActivity().getLayoutInflater().inflate( R.layout.message_list_item_footer, listView, false); - // TODO extend this to properly deal with multiple mailboxes, cursor, etc. - if (savedInstanceState != null) { // Fragment doesn't have this method. Call it manually. loadState(savedInstanceState); @@ -194,19 +181,6 @@ public class MessageListFragment extends ListFragment implements OnItemClickList Log.d(Email.LOG_TAG, "MessageListFragment onStart"); } super.onStart(); - mStarted = true; - if (mMailboxId != -1) { - startLoading(); - } - } - - @Override - public void onStop() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { - Log.d(Email.LOG_TAG, "MessageListFragment onStop"); - } - mStarted = false; - super.onStop(); } @Override @@ -215,8 +189,27 @@ public class MessageListFragment extends ListFragment implements OnItemClickList Log.d(Email.LOG_TAG, "MessageListFragment onResume"); } super.onResume(); - restoreListPosition(); - autoRefreshStaleMailbox(); + mResumed = true; + if (mMailboxId != -1) { + startLoading(); + } + } + + @Override + public void onPause() { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, "MessageListFragment onPause"); + } + mResumed = false; + super.onStop(); + } + + @Override + public void onStop() { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, "MessageListFragment onStop"); + } + super.onStop(); } @Override @@ -224,14 +217,7 @@ public class MessageListFragment extends ListFragment implements OnItemClickList if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Email.LOG_TAG, "MessageListFragment onDestroy"); } - Utility.cancelTaskInterrupt(mLoadMessagesTask); - mLoadMessagesTask = null; - Utility.cancelTaskInterrupt(mSetFooterTask); - mSetFooterTask = null; - if (mListAdapter != null) { - mListAdapter.changeCursor(null); - } super.onDestroy(); } @@ -241,27 +227,12 @@ public class MessageListFragment extends ListFragment implements OnItemClickList Log.d(Email.LOG_TAG, "MessageListFragment onSaveInstanceState"); } super.onSaveInstanceState(outState); - saveListPosition(); - outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition); - outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop); - Set checkedset = mListAdapter.getSelectedSet(); - long[] checkedarray = new long[checkedset.size()]; - int i = 0; - for (Long l : checkedset) { - checkedarray[i] = l; - i++; - } - outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray); + mListAdapter.onSaveInstanceState(outState); } // Unit tests use it - /* package */ void loadState(Bundle savedInstanceState) { - mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0); - mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1); - Set checkedset = mListAdapter.getSelectedSet(); - for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { - checkedset.add(l); - } + /* package */void loadState(Bundle savedInstanceState) { + mListAdapter.loadState(savedInstanceState); } public void setCallback(Callback callback) { @@ -271,46 +242,28 @@ public class MessageListFragment extends ListFragment implements OnItemClickList /** * Called by an Activity to open an mailbox. * - * @param accountId account id of the mailbox, if already known. Pass -1 if unknown or - * {@code mailboxId} is of a special mailbox. If -1 is passed, this fragment will find it - * using {@code mailboxId}, which the activity can get later with {@link #getAccountId()}. - * Passing -1 is always safe, but we can skip a database lookup if specified. - * * @param mailboxId the ID of a mailbox, or one of "special" mailbox IDs like * {@link Mailbox#QUERY_ALL_INBOXES}. -1 is not allowed. */ - public void openMailbox(long accountId, long mailboxId) { + public void openMailbox(long mailboxId) { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Email.LOG_TAG, "MessageListFragment openMailbox"); } if (mailboxId == -1) { throw new InvalidParameterException(); } - if ((mAccountId == accountId) && (mMailboxId == mailboxId)) { + if (mMailboxId == mailboxId) { return; } - mAccountId = accountId; mMailboxId = mailboxId; - if (mStarted) { + if (mResumed) { startLoading(); } } - private void startLoading() { - // Clear the list. (ListFragment will show the "Loading" animation) - getListView().removeFooterView(mListFooterView); - setListAdapter(null); - setListShown(false); - - // Start loading... - Utility.cancelTaskInterrupt(mLoadMessagesTask); - mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); - mLoadMessagesTask.execute(); - } - - /* package */ MessagesAdapter getAdapterForTest() { + /* package */MessagesAdapter getAdapterForTest() { return mListAdapter; } @@ -318,7 +271,7 @@ public class MessageListFragment extends ListFragment implements OnItemClickList * @return the account id or -1 if it's unknown yet. It's also -1 if it's a magic mailbox. */ public long getAccountId() { - return mAccountId; + return (mMailbox == null) ? -1 : mMailbox.mAccountKey; } /** @@ -338,10 +291,16 @@ public class MessageListFragment extends ListFragment implements OnItemClickList } /** - * @return if it's an outbox. + * @return true if it's an outbox. false otherwise, or the mailbox type is + * unknown yet. + * @deprecated It's used by MessageList to see if we should show a progress + * for sending messages. The logic here means we can't catch + * callbacks while the mailbox type isn't figured out yet. That + * show/hide progress logic isn't working in the way it should + * in the first place, so fix it and remove this method. */ public boolean isOutbox() { - return mListFooterMode == LIST_FOOTER_MODE_SEND; + return mMailbox == null ? false : (mMailbox.mType == Mailbox.TYPE_OUTBOX); } /** @@ -358,40 +317,6 @@ public class MessageListFragment extends ListFragment implements OnItemClickList return getSelectedCount() > 0; } - /** - * Save the focused list item. - * - * TODO It's not really working. Fix it. - */ - private void saveListPosition() { - mSavedItemPosition = getListView().getSelectedItemPosition(); - if (mSavedItemPosition >= 0 && getListView().isSelected()) { - mSavedItemTop = getListView().getSelectedView().getTop(); - } else { - mSavedItemPosition = getListView().getFirstVisiblePosition(); - if (mSavedItemPosition >= 0) { - mSavedItemTop = 0; - View topChild = getListView().getChildAt(0); - if (topChild != null) { - mSavedItemTop = topChild.getTop(); - } - } - } - } - - /** - * Restore the focused list item. - * - * TODO It's not really working. Fix it. - */ - private void restoreListPosition() { - if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) { - getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop); - mSavedItemPosition = -1; - mSavedItemTop = 0; - } - } - /** * Called when a message is clicked. */ @@ -425,33 +350,27 @@ public class MessageListFragment extends ListFragment implements OnItemClickList } private void onMessageOpen(final long mailboxId, final long messageId) { - // Use asynctask to determine the type. - new AsyncTask() { - @Override - protected Integer doInBackground(Void... params) { - EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId( - getActivity(), mailboxId); - if (mailbox == null) { - return null; - } - switch (mailbox.mType) { - case EmailContent.Mailbox.TYPE_DRAFTS: - return Callback.TYPE_DRAFT; - case EmailContent.Mailbox.TYPE_TRASH: - return Callback.TYPE_TRASH; - default: - return Callback.TYPE_REGULAR; - } + final int type; + if (mMailbox == null) { // Magic mailbox + if (mMailboxId == Mailbox.QUERY_ALL_DRAFTS) { + type = Callback.TYPE_DRAFT; + } else { + type = Callback.TYPE_REGULAR; } - - @Override - protected void onPostExecute(Integer type) { - if (type == null) { - return; - } - mCallback.onMessageOpen(messageId, mailboxId, getMailboxId(), type); + } else { + switch (mMailbox.mType) { + case EmailContent.Mailbox.TYPE_DRAFTS: + type = Callback.TYPE_DRAFT; + break; + case EmailContent.Mailbox.TYPE_TRASH: + type = Callback.TYPE_TRASH; + break; + default: + type = Callback.TYPE_REGULAR; + break; } - }.execute(); + } + mCallback.onMessageOpen(messageId, mailboxId, getMailboxId(), type); } public void onMultiToggleRead() { @@ -470,10 +389,9 @@ public class MessageListFragment extends ListFragment implements OnItemClickList * Refresh the list. NOOP for special mailboxes (e.g. combined inbox). */ public void onRefresh() { - if (!isMagicMailbox()) { - // Note we can use mAccountId here because it's not a magic mailbox, which doesn't have - // a specific account id. - mController.updateMailbox(mAccountId, mMailboxId); + final long accountId = getAccountId(); + if (accountId != -1) { + mController.updateMailbox(accountId, mMailboxId); } } @@ -646,7 +564,7 @@ public class MessageListFragment extends ListFragment implements OnItemClickList while (c.moveToNext()) { long id = c.getInt(MessagesAdapter.COLUMN_ID); if (selectedSet.contains(Long.valueOf(id))) { - if (c.getInt(column_id) == (defaultflag? 1 : 0)) { + if (c.getInt(column_id) == (defaultflag ? 1 : 0)) { return true; } } @@ -669,6 +587,18 @@ public class MessageListFragment extends ListFragment implements OnItemClickList return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true); } + /** + * Called by activity to indicate that the user explicitly opened the + * mailbox and it needs auto-refresh when it's first shown. TODO: + * {@link MessageList} needs to call this as well. + * + * TODO It's a bit ugly. We can remove this if this fragment "remembers" the current mailbox ID + * through configuration changes. + */ + public void doAutoRefresh() { + mDoAutoRefresh = true; + } + /** * Implements a timed refresh of "stale" mailboxes. This should only happen when * multiple conditions are true, including: @@ -677,29 +607,19 @@ public class MessageListFragment extends ListFragment implements OnItemClickList * Only when the mailbox is "stale" (currently set to 5 minutes since last refresh) */ private void autoRefreshStaleMailbox() { - if (!mCanAutoRefresh - || (mListAdapter.getCursor() == null) // Check if messages info is loaded - || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode - || isMagicMailbox()) { // Check if this mailbox is synthetic/combined + if (!mDoAutoRefresh // Not explicitly open + || (mMailbox == null) // Magic inbox + || (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) // Not push + ) { return; } - mCanAutoRefresh = false; + mDoAutoRefresh = false; if (!Email.mailboxRequiresRefresh(mMailboxId)) { return; } onRefresh(); } - public void updateListPosition() { // TODO give it a better name - int listViewHeight = getListView().getHeight(); - if (mListAdapter.getSelectedSet().size() == 1 && mFirstSelectedItemPosition >= 0 - && mFirstSelectedItemPosition < getListView().getCount() - && listViewHeight < mFirstSelectedItemTop) { - getListView().setSelectionFromTop(mFirstSelectedItemPosition, - listViewHeight - mFirstSelectedItemHeight); - } - } - /** * Show/hide the progress icon on the list footer. It's called by the host activity. * TODO: It might be cleaner if the fragment listen to the controller events and show it by @@ -709,169 +629,60 @@ public class MessageListFragment extends ListFragment implements OnItemClickList if (mListFooterProgress != null) { mListFooterProgress.setVisibility(show ? View.VISIBLE : View.GONE); } - setListFooterText(show); + updateListFooterText(show); } - // Adapter callbacks + /** Implements {@link MessagesAdapter.Callback} */ + @Override public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { onSetMessageFavorite(itemView.mMessageId, newFavorite); } - public void onAdapterRequery() { - // This updates the "multi-selection" button labels. + /** Implements {@link MessagesAdapter.Callback} */ + @Override + public void onAdapterSelectedChanged( + MessageListItem itemView, boolean newSelected, int mSelectedCount) { mCallback.onSelectionChanged(); } - public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, - int mSelectedCount) { - if (mSelectedCount == 1 && newSelected) { - mFirstSelectedItemPosition = getListView().getPositionForView(itemView); - mFirstSelectedItemTop = itemView.getBottom(); - mFirstSelectedItemHeight = itemView.getHeight(); - } else { - mFirstSelectedItemPosition = -1; - } - mCallback.onSelectionChanged(); - } - - /** - * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes). - * - * Here are some rules (finish this list): - * - * Any merged, synced box (except send): refresh - * Any push-mode account: refresh - * Any non-push-mode account: load more - * Any outbox (send again): - * - * @param mailboxId the ID of the mailbox - * @param accountId the ID of the account - */ - private void addFooterView(long mailboxId, long accountId) { - // first, look for shortcuts that don't need us to spin up a DB access task - if (mailboxId == Mailbox.QUERY_ALL_INBOXES - || mailboxId == Mailbox.QUERY_ALL_UNREAD - || mailboxId == Mailbox.QUERY_ALL_FAVORITES) { - finishFooterView(LIST_FOOTER_MODE_REFRESH); - return; - } - if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { - finishFooterView(LIST_FOOTER_MODE_NONE); - return; - } - if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { - finishFooterView(LIST_FOOTER_MODE_SEND); - return; - } - - // We don't know enough to select the footer command type (yet), so we'll - // launch an async task to do the remaining lookups and decide what to do - mSetFooterTask = new SetFooterTask(); - mSetFooterTask.execute(mailboxId, accountId); - } - - private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION = - new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE }; - - private class SetFooterTask extends AsyncTask { - /** - * There are two operational modes here, requiring different lookup. - * mailboxIs != -1: A specific mailbox - check its type, then look up its account - * accountId != -1: A specific account - look up the account - */ - @Override - protected Integer doInBackground(Long... params) { - long mailboxId = params[0]; - long accountId = params[1]; - int mailboxType = -1; - if (mailboxId != -1) { - try { - Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); - Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION, - null, null, null); - if (c.moveToFirst()) { - try { - accountId = c.getLong(0); - mailboxType = c.getInt(1); - } finally { - c.close(); - } - } - } catch (IllegalArgumentException iae) { - // can't do any more here - return LIST_FOOTER_MODE_NONE; - } - } - switch (mailboxType) { - case Mailbox.TYPE_OUTBOX: - return LIST_FOOTER_MODE_SEND; - case Mailbox.TYPE_DRAFTS: - return LIST_FOOTER_MODE_NONE; - } - if (accountId != -1) { - // This is inefficient but the best fix is not here but in isMessagingController - Account account = Account.restoreAccountWithId(mActivity, accountId); - if (account != null) { - // TODO move this to more appropriate place - // (don't change member fields on a worker thread.) - mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH; - if (mController.isMessagingController(account)) { - return LIST_FOOTER_MODE_MORE; // IMAP or POP - } else { - return LIST_FOOTER_MODE_NONE; // EAS - } - } - } - return LIST_FOOTER_MODE_NONE; - } - - @Override - protected void onPostExecute(Integer listFooterMode) { - if (isCancelled()) { - return; - } - if (listFooterMode == null) { - return; - } - finishFooterView(listFooterMode); + private void determineFooterMode() { + mListFooterMode = LIST_FOOTER_MODE_NONE; + if (mAccount != null && !mAccount.isEasAccount()) { + // IMAP, POP has "load more" + mListFooterMode = LIST_FOOTER_MODE_MORE; } } - /** - * Add the fixed footer view as specified, and set up the test as well. - * - * @param listFooterMode the footer mode we've determined should be used for this list - */ - private void finishFooterView(int listFooterMode) { - mListFooterMode = listFooterMode; + private void addFooterView() { + determineFooterMode(); if (mListFooterMode != LIST_FOOTER_MODE_NONE) { - getListView().addFooterView(mListFooterView); - getListView().setAdapter(mListAdapter); + ListView lv = getListView(); + if (mListFooterView != null) { + lv.removeFooterView(mListFooterView); + } + + lv.addFooterView(mListFooterView); + lv.setAdapter(mListAdapter); mListFooterProgress = mListFooterView.findViewById(R.id.progress); mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); - setListFooterText(false); + + // TODO We don't know if it's really "inactive". Someone has to + // remember all sync status. + updateListFooterText(false); } } /** - * Set the list footer text based on mode and "active" status + * Set the list footer text based on mode and "network active" status */ - private void setListFooterText(boolean active) { + private void updateListFooterText(boolean networkActive) { if (mListFooterMode != LIST_FOOTER_MODE_NONE) { int footerTextId = 0; switch (mListFooterMode) { - case LIST_FOOTER_MODE_REFRESH: - footerTextId = active ? R.string.status_loading_more - : R.string.refresh_action; - break; case LIST_FOOTER_MODE_MORE: - footerTextId = active ? R.string.status_loading_more - : R.string.message_list_load_more_messages_action; - break; - case LIST_FOOTER_MODE_SEND: - footerTextId = active ? R.string.status_sending_messages - : R.string.message_list_send_pending_messages_action; + footerTextId = networkActive ? R.string.status_loading_messages + : R.string.message_list_load_more_messages_action; break; } mListFooterText.setText(footerTextId); @@ -883,96 +694,128 @@ public class MessageListFragment extends ListFragment implements OnItemClickList */ private void doFooterClick() { switch (mListFooterMode) { - case LIST_FOOTER_MODE_NONE: // should never happen - break; - case LIST_FOOTER_MODE_REFRESH: - onRefresh(); + case LIST_FOOTER_MODE_NONE: // should never happen break; case LIST_FOOTER_MODE_MORE: onLoadMoreMessages(); break; - case LIST_FOOTER_MODE_SEND: - onSendPendingMessages(); - break; + } + } + + private void startLoading() { + // Clear the list. (ListFragment will show the "Loading" animation) + setListAdapter(null); + setListShown(false); + + // Start loading... + final LoaderManager lm = getLoaderManager(); + + // If we're loading a different mailbox, discard the previous result. + if ((mLastLoadedMailboxId != -1) && (mLastLoadedMailboxId != mMailboxId)) { + lm.stopLoader(LOADER_ID_MAILBOX_LOADER); + lm.stopLoader(LOADER_ID_MESSAGES_LOADER); + } + lm.initLoader(LOADER_ID_MAILBOX_LOADER, null, new MailboxAccountLoaderCallback()); + } + + /** + * Loader callbacks for {@link MailboxAccountLoader}. + */ + private class MailboxAccountLoaderCallback implements LoaderManager.LoaderCallbacks< + MailboxAccountLoader.Result> { + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, + "MessageListFragment onCreateLoader(mailbox) mailboxId=" + mMailboxId); + } + return new MailboxAccountLoader(getActivity().getApplicationContext(), mMailboxId); + } + + @Override + public void onLoadFinished(Loader loader, + MailboxAccountLoader.Result result) { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, "MessageListFragment onLoadFinished(mailbox) mailboxId=" + + mMailboxId); + } + if (!isMagicMailbox() && !result.isFound()) { + mCallback.onMailboxNotFound(); + return; + } + + mLastLoadedMailboxId = mMailboxId; + mAccount = result.mAccount; + mMailbox = result.mMailbox; + getLoaderManager().initLoader(LOADER_ID_MESSAGES_LOADER, null, + new MessagesLoaderCallback()); } } /** - * Async task for loading a single folder out of the UI thread - * - * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some - * specific notes: - * TODO: Move the double query into a specialized URI that returns all inbox messages - * and do the dirty work in raw SQL in the provider. - * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) + * Loader callbacks for message list. */ - private class LoadMessagesTask extends AsyncTask { + private class MessagesLoaderCallback implements LoaderManager.LoaderCallbacks { + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, + "MessageListFragment onCreateLoader(messages) mailboxId=" + mMailboxId); + } - private final long mMailboxKey; - private long mAccountKey; - - /** - * Special constructor to cache some local info - */ - public LoadMessagesTask(long mailboxKey, long accountKey) { - mMailboxKey = mailboxKey; - mAccountKey = accountKey; + // Reset new message count. + // TODO Do it in onLoadFinished(). Unfortunately + // resetNewMessageCount() ends up a + // db operation, which causes a onContentChanged notification, which + // makes cursor + // loaders to requery. Until we fix ContentProvider (don't notify + // unrelated cursors) + // we need to do it here. + resetNewMessageCount(mActivity, mMailboxId, getAccountId()); + return MessagesAdapter.createLoader(getActivity(), mMailboxId); } @Override - protected Cursor doInBackground(Void... params) { - // First, determine account id, if unknown - if (mAccountKey == -1) { // TODO Use constant instead of -1 - if (isMagicMailbox()) { - // Magic mailbox. No accountid. - } else { - EmailContent.Mailbox mailbox = - EmailContent.Mailbox.restoreMailboxWithId(mActivity, mMailboxKey); - if (mailbox != null) { - mAccountKey = mailbox.mAccountKey; - } else { - // Mailbox not found. - // TODO We used to close the activity in this case, but what to do now?? - return null; - } - } + public void onLoadFinished(Loader loader, Cursor cursor) { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, + "MessageListFragment onLoadFinished(messages) mailboxId=" + mMailboxId); } - // Load messages - String selection = - Utility.buildMailboxIdSelection(mResolver, mMailboxKey); - Cursor c = mActivity.getContentResolver().query( - EmailContent.Message.CONTENT_URI, MESSAGE_PROJECTION, - selection, null, EmailContent.MessageColumns.TIMESTAMP + " DESC"); - return c; - } - - @Override - protected void onPostExecute(Cursor cursor) { - if (isCancelled()) { - return; - } - if (cursor == null || cursor.isClosed()) { - mCallback.onMailboxNotFound(); - return; - } - MessageListFragment.this.mAccountId = mAccountKey; + // Save list view state (primarily scroll position) + final ListView lv = getListView(); + final Utility.ListStateSaver lss = new Utility.ListStateSaver(lv); + // Update the list mListAdapter.changeCursor(cursor); setListAdapter(mListAdapter); + setListShown(true); - addFooterView(mMailboxKey, mAccountKey); + // Restore the state + lss.restore(lv); - // changeCursor occurs the jumping of position in ListView, so it's need to restore - // the position; - restoreListPosition(); + // Various post processing... + // (resetNewMessageCount should be here. See above.) autoRefreshStaleMailbox(); - // Reset the "new messages" count in the service, since we're seeing them now - if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { - MailService.resetNewMessageCount(mActivity, -1); - } else if (mMailboxKey >= 0 && mAccountKey != -1) { - MailService.resetNewMessageCount(mActivity, mAccountKey); - } + addFooterView(); + } + } + + /** + * Reset the "new message" count. + *
    + *
  • If {@code mailboxId} is {@link Mailbox#QUERY_ALL_INBOXES}, reset the + * counts of all accounts. + *
  • If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code + * accountId} is valid, reset the count of the specified account. + *
+ */ + /* protected */static void resetNewMessageCount( + Context context, long mailboxId, long accountId) { + if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { + MailService.resetNewMessageCount(context, -1); + } else if (mailboxId >= 0 && accountId != -1) { + MailService.resetNewMessageCount(context, accountId); } } } diff --git a/src/com/android/email/activity/MessageListXL.java b/src/com/android/email/activity/MessageListXL.java index 479343645..721f50bf1 100644 --- a/src/com/android/email/activity/MessageListXL.java +++ b/src/com/android/email/activity/MessageListXL.java @@ -189,7 +189,7 @@ public class MessageListXL extends Activity implements View.OnClickListener, // position. // TODO: FragmentTransaction *does* support backstack, but the behavior isn't too clear // at this point. - mFragmentManager.selectMailbox(mFragmentManager.getMailboxId()); + mFragmentManager.selectMailbox(mFragmentManager.getMailboxId(), false); } else { // Perform the default behavior == close the activity. super.onBackPressed(); @@ -292,7 +292,7 @@ public class MessageListXL extends Activity implements View.OnClickListener, // TODO Rename to onSelectMailbox @Override public void onMailboxSelected(long accountId, long mailboxId) { - mFragmentManager.selectMailbox(mailboxId); + mFragmentManager.selectMailbox(mailboxId, true); } } diff --git a/src/com/android/email/activity/MessageListXLFragmentManager.java b/src/com/android/email/activity/MessageListXLFragmentManager.java index 7e348f290..3d019ab08 100644 --- a/src/com/android/email/activity/MessageListXLFragmentManager.java +++ b/src/com/android/email/activity/MessageListXLFragmentManager.java @@ -303,8 +303,12 @@ class MessageListXLFragmentManager { * * We assume the mailbox selected here belongs to the account selected with * {@link #selectAccount}. + * + * @param mailboxId ID of mailbox + * @param byUserAction set true if the user is explicitly opening the mailbox, in which case + * we perform "auto-refresh". */ - public void selectMailbox(long mailboxId) { + public void selectMailbox(long mailboxId, boolean byUserAction) { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Email.LOG_TAG, "selectMailbox mMailboxId=" + mailboxId); } @@ -322,6 +326,9 @@ class MessageListXLFragmentManager { // Update fragments. if (mMessageListFragment == null) { MessageListFragment f = new MessageListFragment(); + if (byUserAction) { + f.doAutoRefresh(); + } mTargetActivity.openFragmentTransaction().replace(R.id.right_pane, f).commit(); if (mMessageViewFragment != null) { @@ -330,6 +337,9 @@ class MessageListXLFragmentManager { mTargetActivity.onMessageViewFragmentHidden(); // Don't forget to tell the activity. } } else { + if (byUserAction) { + mMessageListFragment.doAutoRefresh(); + } updateMessageListFragment(mMessageListFragment); } } @@ -344,7 +354,7 @@ class MessageListXLFragmentManager { mMessageListFragment = fragment; fragment.setCallback(mMessageListFragmentCallback); - fragment.openMailbox(mAccountId, mMailboxId); + fragment.openMailbox(mMailboxId); } /** @@ -435,7 +445,7 @@ class MessageListXLFragmentManager { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Email.LOG_TAG, " Found inbox"); } - selectMailbox(mailboxId); + selectMailbox(mailboxId, true); } @Override diff --git a/src/com/android/email/activity/MessagesAdapter.java b/src/com/android/email/activity/MessagesAdapter.java index a7330ed41..b8088a1e7 100644 --- a/src/com/android/email/activity/MessagesAdapter.java +++ b/src/com/android/email/activity/MessagesAdapter.java @@ -19,18 +19,22 @@ package com.android.email.activity; import com.android.email.Email; import com.android.email.R; import com.android.email.Utility; +import com.android.email.data.ThrottlingCursorLoader; +import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Message; +import com.android.email.provider.EmailContent.MessageColumns; import android.content.Context; +import android.content.Loader; import android.content.res.ColorStateList; import android.content.res.Resources; -import android.content.res.TypedArray; import android.content.res.Resources.Theme; +import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Handler; -import android.os.SystemClock; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -42,14 +46,21 @@ import android.widget.TextView; import java.util.Date; import java.util.HashSet; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; /** * This class implements the adapter for displaying messages based on cursors. */ /* package */ class MessagesAdapter extends CursorAdapter { + private static final String STATE_CHECKED_ITEMS = + "com.android.email.activity.MessagesAdapter.checkedItems"; + + /* package */ static final String[] MESSAGE_PROJECTION = new String[] { + EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, + MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, + MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, + MessageColumns.FLAGS, + }; public static final int COLUMN_ID = 0; public static final int COLUMN_MAILBOX_KEY = 1; @@ -73,13 +84,9 @@ import java.util.TimerTask; private final ColorStateList mTextColorPrimary; private final ColorStateList mTextColorSecondary; - // Timer to control the refresh rate of the list - private final RefreshTimer mRefreshTimer = new RefreshTimer(); - // Last time we allowed a refresh of the list - private long mLastRefreshTime = 0; // How long we want to wait for refreshes (a good starting guess) // I suspect this could be lowered down to even 1000 or so, but this seems ok for now - private static final long REFRESH_INTERVAL_MS = 2500; + private static final int REFRESH_INTERVAL_MS = 2500; private final java.text.DateFormat mDateFormat; private final java.text.DateFormat mTimeFormat; @@ -90,8 +97,6 @@ import java.util.TimerTask; * Callback from MessageListAdapter. All methods are called on the UI thread. */ public interface Callback { - /** Called when the adapter refreshes */ - void onAdapterRequery(); /** Called when the use starts/unstars a message */ void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite); /** Called when the user selects/unselects a message */ @@ -107,7 +112,7 @@ import java.util.TimerTask; private final Handler mHandler; public MessagesAdapter(Context context, Handler handler, Callback callback) { - super(context.getApplicationContext(), null, true); + super(context.getApplicationContext(), null, 0 /* no auto requery */); mHandler = handler; mCallback = callback; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -129,72 +134,22 @@ import java.util.TimerTask; mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time } - /** - * We override onContentChange to throttle the refresh, which can happen way too often - * on syncing a large list (up to many times per second). This will prevent ANR's during - * initial sync and potentially at other times as well. - */ - @Override - protected synchronized void onContentChanged() { - final Cursor cursor = getCursor(); - if (cursor != null && !cursor.isClosed()) { - long sinceRefresh = SystemClock.elapsedRealtime() - mLastRefreshTime; - mRefreshTimer.schedule(REFRESH_INTERVAL_MS - sinceRefresh); + public void onSaveInstanceState(Bundle outState) { + Set checkedset = getSelectedSet(); + long[] checkedarray = new long[checkedset.size()]; + int i = 0; + for (Long l : checkedset) { + checkedarray[i] = l; + i++; } + outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray); } - /** - * Called in UI thread only to complete the requery that we - * intercepted in onContentChanged(). - */ - private void doRequery() { - super.onContentChanged(); - } - - private class RefreshTimer extends Timer { - private TimerTask timerTask = null; - - protected void clear() { - timerTask = null; + public void loadState(Bundle savedInstanceState) { + Set checkedset = getSelectedSet(); + for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { + checkedset.add(l); } - - protected synchronized void schedule(long delay) { - if (timerTask != null) return; - if (delay < 0) { - refreshList(); - } else { - timerTask = new RefreshTimerTask(); - schedule(timerTask, delay); - } - } - } - - private class RefreshTimerTask extends TimerTask { - @Override - public void run() { - refreshList(); - } - } - - /** - * Do the work of requerying the list and notifying the UI of changed data - * Make sure we call notifyDataSetChanged on the UI thread. - */ - private synchronized void refreshList() { - if (Email.LOGD) { - Log.d("messageList", "refresh: " - + (SystemClock.elapsedRealtime() - mLastRefreshTime) + "ms"); - } - mHandler.post(new Runnable() { - public void run() { - doRequery(); - if (mCallback != null) { - mCallback.onAdapterRequery(); - } - } - }); - mLastRefreshTime = SystemClock.elapsedRealtime(); - mRefreshTimer.clear(); } public Set getSelectedSet() { @@ -321,4 +276,16 @@ import java.util.TimerTask; itemView.setBackgroundDrawable(null); // Change back to default. } } + + public static Loader createLoader(Context context, long mailboxId) { + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { + Log.d(Email.LOG_TAG, "MessagesAdapter createLoader mailboxId=" + mailboxId); + } + String selection = + Utility.buildMailboxIdSelection(context.getContentResolver(), mailboxId); + return new ThrottlingCursorLoader(context, EmailContent.Message.CONTENT_URI, + MESSAGE_PROJECTION, selection, null, + EmailContent.MessageColumns.TIMESTAMP + " DESC", REFRESH_INTERVAL_MS); + + } } diff --git a/src/com/android/email/data/MailboxAccountLoader.java b/src/com/android/email/data/MailboxAccountLoader.java new file mode 100644 index 000000000..c00092998 --- /dev/null +++ b/src/com/android/email/data/MailboxAccountLoader.java @@ -0,0 +1,81 @@ +/* + * 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.data; + +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Mailbox; + +import android.content.AsyncTaskLoader; +import android.content.Context; + + +/** + * Loader to load {@link Mailbox} and {@link Account}. + */ +public class MailboxAccountLoader extends AsyncTaskLoader { + public static class Result { + public Account mAccount; + public Mailbox mMailbox; + + public boolean isFound() { + return (mAccount != null) && (mMailbox != null); + } + } + + private final Context mContext; + private final long mMailboxId; + + public MailboxAccountLoader(Context context, long mailboxId) { + super(context); + mContext = context; + mMailboxId = mailboxId; + } + + @Override + public Result loadInBackground() { + Result result = new Result(); + if (mMailboxId < 0) { + // Magic mailbox. + } else { + result.mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); + if (result.mMailbox != null) { + result.mAccount = Account.restoreAccountWithId(mContext, + result.mMailbox.mAccountKey); + } + if (result.mAccount == null) { // account removed?? + result.mMailbox = null; + } + } + return result; + } + + @Override + public void startLoading() { + cancelLoad(); + forceLoad(); + } + + @Override + public void stopLoading() { + cancelLoad(); + } + + @Override + public void destroy() { + stopLoading(); + } +} diff --git a/src/com/android/email/data/NoAutoRequeryCursorLoader.java b/src/com/android/email/data/NoAutoRequeryCursorLoader.java new file mode 100644 index 000000000..e1e024246 --- /dev/null +++ b/src/com/android/email/data/NoAutoRequeryCursorLoader.java @@ -0,0 +1,37 @@ +/* + * 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.data; + +import android.content.Context; +import android.content.CursorLoader; +import android.net.Uri; + +/** + * Same as {@link CursorLoader} but it doesn't do auto-requery when it gets content-changed + * notifications. + */ +public class NoAutoRequeryCursorLoader extends CursorLoader { + public NoAutoRequeryCursorLoader(Context context, Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + super(context, uri, projection, selection, selectionArgs, sortOrder); + } + + @Override + public void onContentChanged() { + // Don't reload. + } +} diff --git a/tests/src/com/android/email/activity/MessageListUnitTests.java b/tests/src/com/android/email/activity/MessageListUnitTests.java deleted file mode 100644 index 98300bba2..000000000 --- a/tests/src/com/android/email/activity/MessageListUnitTests.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.activity; - -import com.android.email.Email; -import com.android.email.provider.EmailContent; -import com.android.email.provider.EmailContent.Message; -import com.android.email.provider.EmailContent.MessageColumns; - -import android.content.Context; -import android.content.Intent; -import android.database.CursorIndexOutOfBoundsException; -import android.database.AbstractCursor; -import android.database.SQLException; -import android.os.Bundle; -import android.test.ActivityInstrumentationTestCase2; -import android.test.suitebuilder.annotation.LargeTest; -import android.widget.CursorAdapter; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Various instrumentation tests for MessageList. - * - * It might be possible to convert these to ActivityUnitTest, which would be faster. - */ -@LargeTest -public class MessageListUnitTests - extends ActivityInstrumentationTestCase2 { - - private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; - private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; - private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; - private static final String STATE_CHECKED_ITEMS = - "com.android.email.activity.MessageList.checkedItems"; - private Context mContext; - private MessageList mMessageList; - private CursorAdapter mListAdapter; - private HashMap> mRowsMap; - private ArrayList mIDarray; - - public MessageListUnitTests() { - super(MessageList.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - mContext = getInstrumentation().getTargetContext(); - Email.setServicesEnabled(mContext); - - Intent i = new Intent() - .putExtra(EXTRA_ACCOUNT_ID, Long.MIN_VALUE) - .putExtra(EXTRA_MAILBOX_TYPE, Long.MIN_VALUE) - .putExtra(EXTRA_MAILBOX_ID, Long.MIN_VALUE); - this.setActivityIntent(i); - mMessageList = getActivity(); - } - - /** - * Add a dummy message to the data map - */ - private void addElement(long id, long mailboxKey, long accountKey, String displayName, - String subject, long timestamp, int flagRead, int flagFavorite, int flagAttachment, - int flags) { - HashMap emap = new HashMap(); - emap.put(EmailContent.RECORD_ID, id); - emap.put(MessageColumns.MAILBOX_KEY, mailboxKey); - emap.put(MessageColumns.ACCOUNT_KEY, accountKey); - emap.put(MessageColumns.DISPLAY_NAME, displayName); - emap.put(MessageColumns.SUBJECT, subject); - emap.put(MessageColumns.TIMESTAMP, timestamp); - emap.put(MessageColumns.FLAG_READ, flagRead); - emap.put(MessageColumns.FLAG_FAVORITE, flagFavorite); - emap.put(MessageColumns.FLAG_ATTACHMENT, flagAttachment); - emap.put(MessageColumns.FLAGS, flags); - mRowsMap.put(id, emap); - mIDarray.add(id); - } - - /** - * Create dummy messages - */ - private void setUpCustomCursor() throws Throwable { - runTestOnUiThread(new Runnable() { - public void run() { - mListAdapter = mMessageList.getListFragmentForTest().getAdapterForTest(); - mRowsMap = new HashMap>(0); - mIDarray = new ArrayList(0); - final int FIMI = Message.FLAG_INCOMING_MEETING_INVITE; - addElement(0, Long.MIN_VALUE, Long.MIN_VALUE, "a", "A", 0, 0, 0, 0, 0); - addElement(1, Long.MIN_VALUE, Long.MIN_VALUE, "b", "B", 0, 0, 0, 0, 0); - addElement(2, Long.MIN_VALUE, Long.MIN_VALUE, "c", "C", 0, 0, 0, 0, 0); - addElement(3, Long.MIN_VALUE, Long.MIN_VALUE, "d", "D", 0, 0, 0, 0, FIMI); - addElement(4, Long.MIN_VALUE, Long.MIN_VALUE, "e", "E", 0, 0, 0, 0, 0); - addElement(5, Long.MIN_VALUE, Long.MIN_VALUE, "f", "F", 0, 0, 0, 0, 0); - addElement(6, Long.MIN_VALUE, Long.MIN_VALUE, "g", "G", 0, 0, 0, 0, 0); - addElement(7, Long.MIN_VALUE, Long.MIN_VALUE, "h", "H", 0, 0, 0, 0, 0); - addElement(8, Long.MIN_VALUE, Long.MIN_VALUE, "i", "I", 0, 0, 0, 0, 0); - addElement(9, Long.MIN_VALUE, Long.MIN_VALUE, "j", "J", 0, 0, 0, 0, 0); - CustomCursor cc = new CustomCursor(mIDarray, MessageListFragment.MESSAGE_PROJECTION, - mRowsMap); - mListAdapter.changeCursor(cc); - } - }); - } - - public void testRestoreAndSaveInstanceState() throws Throwable { - setUpCustomCursor(); - Bundle bundle = new Bundle(); - mMessageList.getListFragmentForTest().onSaveInstanceState(bundle); - long[] checkedarray = bundle.getLongArray(STATE_CHECKED_ITEMS); - assertEquals(0, checkedarray.length); - Set checkedset = ((MessagesAdapter)mListAdapter).getSelectedSet(); - checkedset.add(1L); - checkedset.add(3L); - checkedset.add(5L); - mMessageList.getListFragmentForTest().onSaveInstanceState(bundle); - checkedarray = bundle.getLongArray(STATE_CHECKED_ITEMS); - java.util.Arrays.sort(checkedarray); - assertEquals(3, checkedarray.length); - assertEquals(1, checkedarray[0]); - assertEquals(3, checkedarray[1]); - assertEquals(5, checkedarray[2]); - } - - public void testRestoreInstanceState() throws Throwable { - setUpCustomCursor(); - Bundle bundle = new Bundle(); - long[] checkedarray = new long[3]; - checkedarray[0] = 1; - checkedarray[1] = 3; - checkedarray[2] = 5; - Set checkedset = ((MessagesAdapter)mListAdapter).getSelectedSet(); - assertEquals(0, checkedset.size()); - bundle.putLongArray(STATE_CHECKED_ITEMS, checkedarray); - mMessageList.getListFragmentForTest().loadState(bundle); - checkedset = ((MessagesAdapter)mListAdapter).getSelectedSet(); - assertEquals(3, checkedset.size()); - assertTrue(checkedset.contains(1L)); - assertTrue(checkedset.contains(3L)); - assertTrue(checkedset.contains(5L)); - } - - /** - * Mock Cursor for MessageList - */ - static class CustomCursor extends AbstractCursor { - private final ArrayList mSortedIdList; - private final String[] mColumnNames; - - public CustomCursor(ArrayList sortedIdList, - String[] columnNames, - HashMap> rows) { - mSortedIdList = sortedIdList; - mColumnNames = columnNames; - mUpdatedRows = rows; - } - - @Override - public void close() { - super.close(); - } - - @Override - public String[] getColumnNames() { - return mColumnNames; - } - - private Object getObject(int columnIndex) { - if (isClosed()) { - throw new SQLException("Already closed."); - } - int size = mSortedIdList.size(); - if (mPos < 0 || mPos >= size) { - throw new CursorIndexOutOfBoundsException(mPos, size); - } - if (columnIndex < 0 || columnIndex >= getColumnCount()) { - return null; - } - return mUpdatedRows.get(mSortedIdList.get(mPos)).get(mColumnNames[columnIndex]); - } - - @Override - public float getFloat(int columnIndex) { - return Float.valueOf(getObject(columnIndex).toString()); - } - - @Override - public double getDouble(int columnIndex) { - return Double.valueOf(getObject(columnIndex).toString()); - } - - @Override - public int getInt(int columnIndex) { - return Integer.valueOf(getObject(columnIndex).toString()); - } - - @Override - public long getLong(int columnIndex) { - return Long.valueOf(getObject(columnIndex).toString()); - } - - @Override - public short getShort(int columnIndex) { - return Short.valueOf(getObject(columnIndex).toString()); - } - - @Override - public String getString(int columnIndex) { - return String.valueOf(getObject(columnIndex)); - } - - @Override - public boolean isNull(int columnIndex) { - return getObject(columnIndex) == null; - } - - @Override - public int getCount() { - return mSortedIdList.size(); - } - } -} - diff --git a/tests/src/com/android/email/data/MailboxAccountLoaderTestCase.java b/tests/src/com/android/email/data/MailboxAccountLoaderTestCase.java new file mode 100644 index 000000000..254a5370b --- /dev/null +++ b/tests/src/com/android/email/data/MailboxAccountLoaderTestCase.java @@ -0,0 +1,102 @@ +/* + * 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.data; + +import com.android.email.DBTestHelper; +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Mailbox; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; + +import android.content.Context; +import android.test.LoaderTestCase; + +public class MailboxAccountLoaderTestCase extends LoaderTestCase { + // Isolted Context for providers. + private Context mProviderContext; + + @Override + protected void setUp() throws Exception { + mProviderContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext( + getContext(), EmailProvider.class); + } + + private long createAccount() { + Account acct = ProviderTestUtils.setupAccount("acct1", true, mProviderContext); + return acct.mId; + } + + private long createMailbox(long accountId) { + Mailbox box = ProviderTestUtils.setupMailbox("name", accountId, true, mProviderContext); + return box.mId; + } + + /** + * Test for {@link MailboxAccountLoader.Result#isFound()} + */ + public void testIsFound() { + MailboxAccountLoader.Result result = new MailboxAccountLoader.Result(); + assertFalse(result.isFound()); + + result.mAccount = new Account(); + assertFalse(result.isFound()); + + result.mMailbox = new Mailbox(); + assertTrue(result.isFound()); + + result.mAccount = null; + assertFalse(result.isFound()); + } + + /** + * Test for normal case. (account, mailbox found) + */ + public void testLoad() { + final long accountId = createAccount(); + final long mailboxId = createMailbox(accountId); + + MailboxAccountLoader.Result result = getLoaderResultSynchronously( + new MailboxAccountLoader(mProviderContext, mailboxId)); + assertTrue(result.isFound()); + assertEquals(accountId, result.mAccount.mId); + assertEquals(mailboxId, result.mMailbox.mId); + } + + /** + * Mailbox not found. + */ + public void testMailboxNotFound() { + MailboxAccountLoader.Result result = getLoaderResultSynchronously( + new MailboxAccountLoader(mProviderContext, 123)); + assertFalse(result.isFound()); + assertNull(result.mAccount); + assertNull(result.mMailbox); + } + + /** + * Account not found. + */ + public void testAccountNotFound() { + final long mailboxId = createMailbox(1); + + MailboxAccountLoader.Result result = getLoaderResultSynchronously( + new MailboxAccountLoader(mProviderContext, mailboxId)); + assertFalse(result.isFound()); + assertNull(result.mAccount); + assertNull(result.mMailbox); + } +}