diff --git a/src/com/android/email/Clock.java b/src/com/android/email/Clock.java new file mode 100644 index 000000000..a2b44bb9b --- /dev/null +++ b/src/com/android/email/Clock.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * A class provide the current time (like {@link System#currentTimeMillis()}). + * It's intended to be mocked out for unit tests. + */ +public class Clock { + public static final Clock INSTANCE = new Clock(); + + protected Clock() { + } + + public long getTime() { + return System.currentTimeMillis(); + } +} diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 9bdb333fc..c3192a458 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -571,26 +571,6 @@ public class Controller { } } - /** - * Call {@link #sendPendingMessages} for all accounts. - */ - public void sendPendingMessagesForAllAccounts(final Context context) { - Utility.runAsync(new Runnable() { - public void run() { - Cursor c = context.getContentResolver().query(Account.CONTENT_URI, - Account.ID_PROJECTION, null, null, null); - try { - while (c.moveToNext()) { - long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); - sendPendingMessages(accountId); - } - } finally { - c.close(); - } - } - }); - } - /** * Reset visible limits for all accounts. * For each account: diff --git a/src/com/android/email/ControllerResultUiThreadWrapper.java b/src/com/android/email/ControllerResultUiThreadWrapper.java index a2c9cce9d..d4e5fd9cc 100644 --- a/src/com/android/email/ControllerResultUiThreadWrapper.java +++ b/src/com/android/email/ControllerResultUiThreadWrapper.java @@ -24,6 +24,9 @@ import android.os.Handler; /** * A {@link Result} that wraps another {@link Result} and makes sure methods gets called back * on the UI thread. + * + *

Optionally it supports the "synchronous" mode, if you pass null for the {@code handler} + * parameter, which allows unit tests to run synchronously. */ public class ControllerResultUiThreadWrapper extends Result { private final Handler mHandler; @@ -38,10 +41,18 @@ public class ControllerResultUiThreadWrapper extends Result { return mWrappee; } + private void run(Runnable runnable) { + if (mHandler == null) { + runnable.run(); + } else { + mHandler.post(runnable); + } + } + @Override public void loadAttachmentCallback(final MessagingException result, final long messageId, final long attachmentId, final int progress) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.loadAttachmentCallback(result, messageId, attachmentId, progress); } @@ -51,7 +62,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void loadMessageForViewCallback(final MessagingException result, final long messageId, final int progress) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.loadMessageForViewCallback(result, messageId, progress); } @@ -61,7 +72,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void sendMailCallback(final MessagingException result, final long accountId, final long messageId, final int progress) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.sendMailCallback(result, accountId, messageId, progress); } @@ -71,7 +82,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void serviceCheckMailCallback(final MessagingException result, final long accountId, final long mailboxId, final int progress, final long tag) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.serviceCheckMailCallback(result, accountId, mailboxId, progress, tag); } @@ -81,7 +92,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void updateMailboxCallback(final MessagingException result, final long accountId, final long mailboxId, final int progress, final int numNewMessages) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.updateMailboxCallback(result, accountId, mailboxId, progress, numNewMessages); @@ -92,7 +103,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void updateMailboxListCallback(final MessagingException result, final long accountId, final int progress) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.updateMailboxListCallback(result, accountId, progress); } @@ -101,7 +112,7 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void deleteAccountCallback(final long accountId) { - mHandler.post(new Runnable() { + run(new Runnable() { public void run() { mWrappee.deleteAccountCallback(accountId); } diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index dae5f2123..0beb01c80 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -141,9 +141,6 @@ public class Email extends Application { */ public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024); - private static HashMap sMailboxSyncTimes = new HashMap(); - private static final long UPDATE_INTERVAL = 5 * DateUtils.MINUTE_IN_MILLIS; - /** * This is used to force stacked UI to return to the "welcome" screen any time we change * the accounts list (e.g. deleting accounts in the Account Manager preferences.) @@ -271,6 +268,8 @@ public class Email extends Application { DEBUG = prefs.getEnableDebugLogging(); setTempDirectory(this); + // Tie MailRefreshManager to the Controller. + RefreshManager.getInstance(this); // Reset all accounts to default visible window Controller.getInstance(this).resetVisibleLimits(); @@ -286,30 +285,6 @@ public class Email extends Application { Log.d(LOG_TAG, message); } - /** - * Update the time when the mailbox is refreshed - * @param mailboxId mailbox which need to be updated - */ - public static void updateMailboxRefreshTime(long mailboxId) { - synchronized (sMailboxSyncTimes) { - sMailboxSyncTimes.put(mailboxId, System.currentTimeMillis()); - } - } - - /** - * Check if the mailbox is need to be refreshed - * @param mailboxId mailbox checked the need of refreshing - * @return the need of refreshing - */ - public static boolean mailboxRequiresRefresh(long mailboxId) { - synchronized (sMailboxSyncTimes) { - return - !sMailboxSyncTimes.containsKey(mailboxId) - || (System.currentTimeMillis() - sMailboxSyncTimes.get(mailboxId) - > UPDATE_INTERVAL); - } - } - /** * Called by the accounts reconciler to notify that accounts have changed, or by "Welcome" * to clear the flag. diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 769bc18ea..906cce811 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -2026,14 +2026,13 @@ public class MessagingController implements Runnable { } /** - * Checks mail for one or multiple accounts. If account is null all accounts - * are checked. This entry point is for use by the mail checking service only, because it + * Checks mail for an account. + * This entry point is for use by the mail checking service only, because it * gives slightly different callbacks (so the service doesn't get confused by callbacks * triggered by/for the foreground UI. * * TODO clean up the execution model which is unnecessarily threaded due to legacy code * - * @param context * @param accountId the account to check * @param listener */ diff --git a/src/com/android/email/RefreshManager.java b/src/com/android/email/RefreshManager.java new file mode 100644 index 000000000..19f98f53f --- /dev/null +++ b/src/com/android/email/RefreshManager.java @@ -0,0 +1,443 @@ +/* + * 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; + +import com.android.email.mail.MessagingException; +import com.android.email.provider.EmailContent; + +import android.content.Context; +import android.database.Cursor; +import android.os.Handler; +import android.util.Log; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +/** + * Class that handles "refresh" (and "send pending messages" for outboxes) related functionalities. + * + *

This class is responsible for two things: + *

+ * Refresh requests will be ignored if a request to the same target is already requested, or is + * already being refreshed. + * + *

Conceptually it can be a part of {@link Controller}, but extracted for easy testing. + */ +public class RefreshManager { + private static final boolean DEBUG_CALLBACK_LOG = true; + private static final long MAILBOX_AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // in milliseconds + + private static RefreshManager sInstance; + + private final Clock mClock; + private final Context mContext; + private final Controller mController; + private final Controller.Result mControllerResult; + + /** Last error message */ + private String mErrorMessage; + + public interface Listener { + public void onRefreshStatusChanged(long accountId, long mailboxId); + public void onMessagingError(long accountId, long mailboxId, String message); + } + + private final ArrayList mListeners = new ArrayList(); + + /** + * Status of a mailbox list/message list. + */ + /* package */ static class Status { + /** + * True if a refresh of the mailbox is requested, and not finished yet. + */ + private boolean mIsRefreshRequested; + + /** + * True if the mailbox is being refreshed. + * + * Set true when {@link #onRefreshRequested} is called, i.e. refresh is requested by UI. + * Note refresh can occur without a request from UI as well (e.g. timer based refresh). + * In which case, {@link #mIsRefreshing} will be true with {@link #mIsRefreshRequested} + * being false. + */ + private boolean mIsRefreshing; + + private long mLastRefreshTime; + + public boolean isRefreshing() { + return mIsRefreshRequested || mIsRefreshing; + } + + public boolean canRefresh() { + return !isRefreshing(); + } + + public void onRefreshRequested() { + mIsRefreshRequested = true; + } + + public long getLastRefreshTime() { + return mLastRefreshTime; + } + + public void onCallback(MessagingException exception, int progress, Clock clock) { + if (exception == null && progress == 0) { + // Refresh started + mIsRefreshing = true; + } else if (exception != null || progress == 100) { + // Refresh finished + mIsRefreshing = false; + mIsRefreshRequested = false; + mLastRefreshTime = clock.getTime(); + } + } + } + + /** + * Map of accounts/mailboxes to {@link Status}. + */ + private static class RefreshStatusMap { + private final HashMap mMap = new HashMap(); + + public Status get(long id) { + Status s = mMap.get(id); + if (s == null) { + s = new Status(); + mMap.put(id, s); + } + return s; + } + + public boolean isRefreshingAny() { + for (Status s : mMap.values()) { + if (s.isRefreshing()) { + return true; + } + } + return false; + } + } + + private final RefreshStatusMap mMailboxListStatus = new RefreshStatusMap(); + private final RefreshStatusMap mMessageListStatus = new RefreshStatusMap(); + private final RefreshStatusMap mOutboxStatus = new RefreshStatusMap(); + + /** + * @return the singleton instance. + */ + public static synchronized RefreshManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new RefreshManager(context, Controller.getInstance(context), + Clock.INSTANCE, new Handler()); + } + return sInstance; + } + + /* package */ RefreshManager(Context context, Controller controller, Clock clock, + Handler handler) { + mClock = clock; + mContext = context.getApplicationContext(); + mController = controller; + mControllerResult = new ControllerResultUiThreadWrapper( + handler, new ControllerResult()); + mController.addResultCallback(mControllerResult); + } + + public void registerListener(Listener listener) { + if (listener == null) { + throw new InvalidParameterException(); + } + mListeners.add(listener); + } + + public void unregisterListener(Listener listener) { + if (listener == null) { + throw new InvalidParameterException(); + } + mListeners.remove(listener); + } + + /** + * Refresh the mailbox list of an account. + */ + public boolean refreshMailboxList(long accountId) { + final Status status = mMailboxListStatus.get(accountId); + if (!status.canRefresh()) return false; + + Log.i(Email.LOG_TAG, "refreshMailboxList " + accountId); + status.onRefreshRequested(); + notifyRefreshStatusChanged(accountId, -1); + mController.updateMailboxList(accountId); + return true; + } + + public boolean isMailboxStale(long mailboxId) { + return mClock.getTime() >= (mMessageListStatus.get(mailboxId).getLastRefreshTime() + + MAILBOX_AUTO_REFRESH_INTERVAL); + } + + /** + * Refresh messages in a mailbox. + */ + public boolean refreshMessageList(long accountId, long mailboxId) { + return refreshMessageList(accountId, mailboxId, false); + } + + /** + * "load more messages" in a mailbox. + */ + public boolean loadMoreMessages(long accountId, long mailboxId) { + return refreshMessageList(accountId, mailboxId, true); + } + + private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages) { + final Status status = mMessageListStatus.get(mailboxId); + if (!status.canRefresh()) return false; + + Log.i(Email.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", " + + loadMoreMessages); + status.onRefreshRequested(); + notifyRefreshStatusChanged(accountId, mailboxId); + mController.updateMailbox(accountId, mailboxId); + return true; + } + + /** + * Send pending messages. + */ + public boolean sendPendingMessages(long accountId) { + final Status status = mOutboxStatus.get(accountId); + if (!status.canRefresh()) return false; + + Log.i(Email.LOG_TAG, "sendPendingMessages " + accountId); + status.onRefreshRequested(); + notifyRefreshStatusChanged(accountId, -1); + mController.sendPendingMessages(accountId); + return true; + } + + /** + * Call {@link #sendPendingMessages} for all accounts. + */ + public void sendPendingMessagesForAllAccounts() { + Log.i(Email.LOG_TAG, "sendPendingMessagesForAllAccounts"); + Utility.runAsync(new Runnable() { + public void run() { + sendPendingMessagesForAllAccountsSync(); + } + }); + } + + /** + * Synced internal method for {@link #sendPendingMessagesForAllAccounts} for testing. + */ + /* package */ void sendPendingMessagesForAllAccountsSync() { + Cursor c = mContext.getContentResolver().query(EmailContent.Account.CONTENT_URI, + EmailContent.Account.ID_PROJECTION, null, null, null); + try { + while (c.moveToNext()) { + sendPendingMessages(c.getLong(EmailContent.Account.ID_PROJECTION_COLUMN)); + } + } finally { + c.close(); + } + } + + public boolean isMailboxListRefreshing(long accountId) { + return mMailboxListStatus.get(accountId).isRefreshing(); + } + + public boolean isMessageListRefreshing(long mailboxId) { + return mMessageListStatus.get(mailboxId).isRefreshing(); + } + + public boolean isSendingMessage(long accountId) { + return mOutboxStatus.get(accountId).isRefreshing(); + } + + public boolean isRefreshingAnyMailboxList() { + return mMailboxListStatus.isRefreshingAny(); + } + + public boolean isRefreshingAnyMessageList() { + return mMessageListStatus.isRefreshingAny(); + } + + public boolean isSendingAnyMessage() { + return mOutboxStatus.isRefreshingAny(); + } + + public boolean isRefreshingOrSendingAny() { + return isRefreshingAnyMailboxList() || isRefreshingAnyMessageList() + || isSendingAnyMessage(); + } + + public String getErrorMessage() { + return mErrorMessage; + } + + private void notifyRefreshStatusChanged(long accountId, long mailboxId) { + for (Listener l : mListeners) { + l.onRefreshStatusChanged(accountId, mailboxId); + } + } + + private void reportError(long accountId, long mailboxId, String errorMessage) { + mErrorMessage = errorMessage; + for (Listener l : mListeners) { + l.onMessagingError(accountId, mailboxId, mErrorMessage); + } + } + + /* package */ Collection getListenersForTest() { + return mListeners; + } + + /* package */ Status getMailboxListStatusForTest(long accountId) { + return mMailboxListStatus.get(accountId); + } + + /* package */ Status getMessageListStatusForTest(long mailboxId) { + return mMessageListStatus.get(mailboxId); + } + + /* package */ Status getOutboxStatusForTest(long acountId) { + return mOutboxStatus.get(acountId); + } + + private class ControllerResult extends Controller.Result { + private boolean mSendMailExceptionReported = false; + + private String exceptionToString(MessagingException exception) { + if (exception == null) { + return "(no exception)"; + } else { + return exception.getUiErrorMessage(mContext); + } + } + + /** + * Callback for mailbox list refresh. + */ + @Override + public void updateMailboxListCallback(MessagingException exception, long accountId, + int progress) { + if (Email.DEBUG && DEBUG_CALLBACK_LOG) { + Log.d(Email.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress + + ", " + exceptionToString(exception)); + } + mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock); + if (exception != null) { + reportError(accountId, -1, exception.getUiErrorMessage(mContext)); + } + notifyRefreshStatusChanged(accountId, -1); + } + + /** + * Callback for explicit (user-driven) mailbox refresh. + */ + @Override + public void updateMailboxCallback(MessagingException exception, long accountId, + long mailboxId, int progress, int dontUseNumNewMessages) { + if (Email.DEBUG && DEBUG_CALLBACK_LOG) { + Log.d(Email.LOG_TAG, "updateMailboxCallback " + accountId + ", " + + mailboxId + ", " + progress + ", " + exceptionToString(exception)); + } + updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); + } + + /** + * Callback for implicit (timer-based) mailbox refresh. + * + * Do the same as {@link #updateMailboxCallback}. + * TODO: Figure out if it's really okay to do the same as updateMailboxCallback. + * If both the explicit refresh and the implicit refresh can run at the same time, + * we need to keep track of their status separately. + */ + @Override + public void serviceCheckMailCallback( + MessagingException exception, long accountId, long mailboxId, int progress, + long tag) { + if (Email.DEBUG && DEBUG_CALLBACK_LOG) { + Log.d(Email.LOG_TAG, "serviceCheckMailCallback " + accountId + ", " + + mailboxId + ", " + progress + ", " + exceptionToString(exception)); + } + updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); + } + + private void updateMailboxCallbackInternal(MessagingException exception, long accountId, + long mailboxId, int progress, int dontUseNumNewMessages) { + // Don't use dontUseNumNewMessages. serviceCheckMailCallback() don't set it. + mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock); + if (exception != null) { + reportError(accountId, mailboxId, exception.getUiErrorMessage(mContext)); + } + notifyRefreshStatusChanged(accountId, mailboxId); + } + + + /** + * Send message progress callback. + * + * This callback is overly overloaded: + * + * First, we get this. + * result == null, messageId == -1, progress == 0: start batch send + * + * Then we get these callbacks per message. + * (Exchange backend may skip "start sending one message".) + * result == null, messageId == xx, progress == 0: start sending one message + * result == xxxx, messageId == xx, progress == 0; failed sending one message + * + * Finally we get this. + * result == null, messageId == -1, progres == 100; finish sending batch + * + * So, let's just report the first exception we get, and ignore the rest. + */ + @Override + public void sendMailCallback(MessagingException exception, long accountId, long messageId, + int progress) { + if (Email.DEBUG && DEBUG_CALLBACK_LOG) { + Log.d(Email.LOG_TAG, "sendMailCallback " + accountId + ", " + + messageId + ", " + progress + ", " + exceptionToString(exception)); + } + if (progress == 0 && messageId == -1) { + mSendMailExceptionReported = false; + } + if (messageId == -1) { + // Update the status only for the batch start/end. + // (i.e. don't report for each message.) + mOutboxStatus.get(accountId).onCallback(exception, progress, mClock); + notifyRefreshStatusChanged(accountId, -1); + } + if (exception != null && !mSendMailExceptionReported) { + // Only the first error in a batch will be reported. + mSendMailExceptionReported = true; + reportError(accountId, messageId, exception.getUiErrorMessage(mContext)); + } + } + } +} diff --git a/src/com/android/email/activity/AccountFolderList.java b/src/com/android/email/activity/AccountFolderList.java index 972bf1cc9..fcecbaedb 100644 --- a/src/com/android/email/activity/AccountFolderList.java +++ b/src/com/android/email/activity/AccountFolderList.java @@ -322,9 +322,6 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag @Override public void updateMailboxCallback(MessagingException result, long accountKey, long mailboxKey, int progress, int numNewMessages) { - if (result != null || progress == 100) { - Email.updateMailboxRefreshTime(mailboxKey); - } updateProgress(result, progress); } diff --git a/src/com/android/email/activity/MailboxList.java b/src/com/android/email/activity/MailboxList.java index 37cad67e4..6592da771 100644 --- a/src/com/android/email/activity/MailboxList.java +++ b/src/com/android/email/activity/MailboxList.java @@ -22,8 +22,6 @@ import com.android.email.Email; import com.android.email.R; import com.android.email.Utility; import com.android.email.activity.setup.AccountSettings; -import com.android.email.mail.AuthenticationFailedException; -import com.android.email.mail.CertificateValidationException; import com.android.email.mail.MessagingException; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; @@ -189,7 +187,7 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac onAccounts(); return true; case R.id.refresh: - onRefresh(-1); + onRefresh(); return true; case R.id.compose: onCompose(); @@ -210,17 +208,12 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac } /** - * Refresh the mailbox list, or a single mailbox - * @param mailboxId -1 for all + * Refresh the mailbox list */ - private void onRefresh(long mailboxId) { + private void onRefresh() { Controller controller = Controller.getInstance(getApplication()); showProgressIcon(true); - if (mailboxId >= 0) { - controller.updateMailbox(mAccountId, mailboxId); - } else { - controller.updateMailboxList(mAccountId); - } + mListFragment.onRefresh(); } private void onAccounts() { @@ -274,7 +267,6 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac */ private class ControllerResults extends Controller.Result { - // TODO report errors into UI @Override public void updateMailboxListCallback(MessagingException result, long accountKey, int progress) { @@ -284,13 +276,9 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac } } - // TODO report errors into UI @Override public void updateMailboxCallback(MessagingException result, long accountKey, long mailboxKey, int progress, int numNewMessages) { - if (result != null || progress == 100) { - Email.updateMailboxRefreshTime(mailboxKey); - } if (accountKey == mAccountId) { updateBanner(result, progress); updateProgress(result, progress); @@ -321,28 +309,7 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac */ private void updateBanner(MessagingException result, int progress) { if (result != null) { - int id = R.string.status_network_error; - if (result instanceof AuthenticationFailedException) { - id = R.string.account_setup_failed_dlg_auth_message; - } else if (result instanceof CertificateValidationException) { - id = R.string.account_setup_failed_dlg_certificate_message; - } else { - switch (result.getExceptionType()) { - case MessagingException.IOERROR: - id = R.string.account_setup_failed_ioerror; - break; - case MessagingException.TLS_REQUIRED: - id = R.string.account_setup_failed_tls_required; - break; - case MessagingException.AUTH_REQUIRED: - id = R.string.account_setup_failed_auth_required; - break; - case MessagingException.GENERAL_SECURITY: - id = R.string.account_setup_failed_security; - break; - } - } - showErrorBanner(getString(id)); + showErrorBanner(result.getUiErrorMessage(MailboxList.this)); } else if (progress > 0) { showErrorBanner(null); } diff --git a/src/com/android/email/activity/MailboxListFragment.java b/src/com/android/email/activity/MailboxListFragment.java index bbbf89778..4b2c91270 100644 --- a/src/com/android/email/activity/MailboxListFragment.java +++ b/src/com/android/email/activity/MailboxListFragment.java @@ -16,7 +16,9 @@ package com.android.email.activity; +import com.android.email.Controller; import com.android.email.Email; +import com.android.email.RefreshManager; import com.android.email.Utility; import android.app.Activity; @@ -238,4 +240,10 @@ public class MailboxListFragment extends ListFragment implements OnItemClickList public void onItemClick(AdapterView parent, View view, int position, long id) { mCallback.onMailboxSelected(mAccountId, id); } + + public void onRefresh() { + if (mAccountId != -1) { + RefreshManager.getInstance(getActivity()).refreshMailboxList(mAccountId); + } + } } diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 0b954b80b..b23cb9e80 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -145,7 +145,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus private TextView mRightTitle; private Controller mController; - private Listener mListener; private boolean mDraftNeedsSaving; private boolean mMessageLoaded; private AsyncTask mLoadAttachmentsTask; @@ -270,7 +269,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.list_title); mController = Controller.getInstance(getApplication()); - mListener = new Listener(); initViews(); setDraftNeedsSaving(false); @@ -342,7 +340,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus @Override public void onResume() { super.onResume(); - mController.addResultCallback(mListener); // Exit immediately if the accounts list has changed (e.g. externally deleted) if (Email.getNotifyUiAccountsChanged()) { @@ -356,7 +353,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus public void onPause() { super.onPause(); saveIfNeeded(); - mController.removeResultCallback(mListener); } /** @@ -1523,14 +1519,4 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus setMessageContentSelection((mAccount != null) ? mAccount.mSignature : null); } } - - private class Listener extends Controller.Result { - @Override - public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int numNewMessages) { - if (result != null || progress == 100) { - Email.updateMailboxRefreshTime(mailboxId); - } - } - } } diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index cfbd0da20..5b7a0d3c9 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -23,8 +23,6 @@ import com.android.email.R; import com.android.email.Utility; import com.android.email.activity.setup.AccountSecurity; import com.android.email.activity.setup.AccountSettings; -import com.android.email.mail.AuthenticationFailedException; -import com.android.email.mail.CertificateValidationException; import com.android.email.mail.MessagingException; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; @@ -542,7 +540,6 @@ public class MessageList extends Activity implements OnClickListener, private void showProgressIcon(boolean show) { int visibility = show ? View.VISIBLE : View.GONE; mProgressIcon.setVisibility(visibility); - mListFragment.showProgressIcon(show); } private void showErrorBanner(String message) { @@ -579,9 +576,6 @@ public class MessageList extends Activity implements OnClickListener, public void updateMailboxCallback(MessagingException result, long accountKey, long mailboxKey, int progress, int numNewMessages) { updateBanner(result, progress, mailboxKey); - if (result != null || progress == 100) { - Email.updateMailboxRefreshTime(mailboxKey); - } updateProgress(result, progress); } @@ -632,33 +626,7 @@ public class MessageList extends Activity implements OnClickListener, return; } if (result != null) { - int id = R.string.status_network_error; - if (result instanceof AuthenticationFailedException) { - id = R.string.account_setup_failed_dlg_auth_message; - } else if (result instanceof CertificateValidationException) { - id = R.string.account_setup_failed_dlg_certificate_message; - } else { - switch (result.getExceptionType()) { - case MessagingException.IOERROR: - id = R.string.account_setup_failed_ioerror; - break; - case MessagingException.TLS_REQUIRED: - id = R.string.account_setup_failed_tls_required; - break; - case MessagingException.AUTH_REQUIRED: - id = R.string.account_setup_failed_auth_required; - break; - case MessagingException.GENERAL_SECURITY: - id = R.string.account_setup_failed_security; - break; - // TODO Generate a unique string for this case, which is the case - // where the security policy needs to be updated. - case MessagingException.SECURITY_POLICIES_REQUIRED: - id = R.string.account_setup_failed_security; - break; - } - } - showErrorBanner(getString(id)); + showErrorBanner(result.getUiErrorMessage(MessageList.this)); } else if (progress > 0) { showErrorBanner(null); } diff --git a/src/com/android/email/activity/MessageListFragment.java b/src/com/android/email/activity/MessageListFragment.java index 3eea957ae..28a64b417 100644 --- a/src/com/android/email/activity/MessageListFragment.java +++ b/src/com/android/email/activity/MessageListFragment.java @@ -18,6 +18,7 @@ package com.android.email.activity; import com.android.email.Controller; import com.android.email.Email; +import com.android.email.RefreshManager; import com.android.email.R; import com.android.email.Utility; import com.android.email.data.MailboxAccountLoader; @@ -62,6 +63,8 @@ import java.util.Set; * * We run them sequentially. i.e. First starts {@link MailboxAccountLoader}, and when it finishes * starts the other. + * + * TODO Add "send all messages" button to outboxes */ public class MessageListFragment extends ListFragment implements OnItemClickListener, OnItemLongClickListener, MessagesAdapter.Callback { @@ -90,6 +93,8 @@ public class MessageListFragment extends ListFragment // Controller access private Controller mController; + private RefreshManager mRefreshManager; + private RefreshListener mRefreshListener = new RefreshListener(); // Misc members private boolean mDoAutoRefresh; @@ -151,6 +156,8 @@ public class MessageListFragment extends ListFragment super.onCreate(savedInstanceState); mActivity = getActivity(); mController = Controller.getInstance(mActivity); + mRefreshManager = RefreshManager.getInstance(mActivity); + mRefreshManager.registerListener(mRefreshListener); } @Override @@ -218,7 +225,7 @@ public class MessageListFragment extends ListFragment if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Email.LOG_TAG, "MessageListFragment onDestroy"); } - + mRefreshManager.unregisterListener(mRefreshListener); super.onDestroy(); } @@ -391,9 +398,9 @@ public class MessageListFragment extends ListFragment * Refresh the list. NOOP for special mailboxes (e.g. combined inbox). */ public void onRefresh() { - final long accountId = getAccountId(); + long accountId = getAccountId(); if (accountId != -1) { - mController.updateMailbox(accountId, mMailboxId); + mRefreshManager.refreshMessageList(accountId, mMailboxId); } } @@ -410,16 +417,18 @@ public class MessageListFragment extends ListFragment * Load more messages. NOOP for special mailboxes (e.g. combined inbox). */ private void onLoadMoreMessages() { - if (!isMagicMailbox()) { - mController.loadMoreMessages(mMailboxId); + long accountId = getAccountId(); + if (accountId != -1) { + mRefreshManager.loadMoreMessages(accountId, mMailboxId); } } public void onSendPendingMessages() { + RefreshManager rm = RefreshManager.getInstance(mActivity); if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) { - mController.sendPendingMessagesForAllAccounts(mActivity); + rm.sendPendingMessagesForAllAccounts(); } else if (!isMagicMailbox()) { // Magic boxes don't have a specific account id. - mController.sendPendingMessages(getAccountId()); + rm.sendPendingMessages(getAccountId()); } } @@ -618,24 +627,12 @@ public class MessageListFragment extends ListFragment return; } mDoAutoRefresh = false; - if (!Email.mailboxRequiresRefresh(mMailboxId)) { + if (!mRefreshManager.isMailboxStale(mMailboxId)) { return; } onRefresh(); } - /** - * 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 - * itself, rather than letting the activity controll this. - */ - public void showProgressIcon(boolean show) { - if (mListFooterProgress != null) { - mListFooterProgress.setVisibility(show ? View.VISIBLE : View.GONE); - } - updateListFooterText(show); - } - /** Implements {@link MessagesAdapter.Callback} */ @Override public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { @@ -651,6 +648,10 @@ public class MessageListFragment extends ListFragment private void determineFooterMode() { mListFooterMode = LIST_FOOTER_MODE_NONE; + if ((mMailbox == null) || (mMailbox.mType == Mailbox.TYPE_OUTBOX) + || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) { + return; // No footer + } if (mAccount != null && !mAccount.isEasAccount()) { // IMAP, POP has "load more" mListFooterMode = LIST_FOOTER_MODE_MORE; @@ -658,12 +659,12 @@ public class MessageListFragment extends ListFragment } private void addFooterView() { + ListView lv = getListView(); + if (mListFooterView != null) { + lv.removeFooterView(mListFooterView); + } determineFooterMode(); if (mListFooterMode != LIST_FOOTER_MODE_NONE) { - ListView lv = getListView(); - if (mListFooterView != null) { - lv.removeFooterView(mListFooterView); - } lv.addFooterView(mListFooterView); lv.setAdapter(mListAdapter); @@ -671,22 +672,22 @@ public class MessageListFragment extends ListFragment mListFooterProgress = mListFooterView.findViewById(R.id.progress); mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); - // TODO We don't know if it's really "inactive". Someone has to - // remember all sync status. - updateListFooterText(false); + updateListFooter(); } } /** - * Set the list footer text based on mode and "network active" status + * Set the list footer text based on mode and the current "network active" status */ - private void updateListFooterText(boolean networkActive) { + private void updateListFooter() { if (mListFooterMode != LIST_FOOTER_MODE_NONE) { int footerTextId = 0; switch (mListFooterMode) { case LIST_FOOTER_MODE_MORE: - footerTextId = networkActive ? R.string.status_loading_messages + boolean active = mRefreshManager.isMessageListRefreshing(mMailboxId); + footerTextId = active ? R.string.status_loading_messages : R.string.message_list_load_more_messages_action; + mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE); break; } mListFooterText.setText(footerTextId); @@ -914,4 +915,15 @@ public class MessageListFragment extends ListFragment mSelectionMode = null; } } -} + + private class RefreshListener implements RefreshManager.Listener { + @Override + public void onMessagingError(long accountId, long mailboxId, String message) { + } + + @Override + public void onRefreshStatusChanged(long accountId, long mailboxId) { + updateListFooter(); + } + } +} \ No newline at end of file diff --git a/src/com/android/email/activity/MessageListXL.java b/src/com/android/email/activity/MessageListXL.java index d6cebcd27..1350f4d2f 100644 --- a/src/com/android/email/activity/MessageListXL.java +++ b/src/com/android/email/activity/MessageListXL.java @@ -17,6 +17,7 @@ package com.android.email.activity; import com.android.email.Email; +import com.android.email.RefreshManager; import com.android.email.R; import com.android.email.Utility; import com.android.email.activity.setup.AccountSettings; @@ -54,6 +55,9 @@ public class MessageListXL extends Activity implements View.OnClickListener, private static final int LOADER_ID_ACCOUNT_LIST = 0; private Context mContext; + private RefreshManager mRefreshManager; + private final RefreshListener mMailRefreshManagerListener + = new RefreshListener(); private View mMessageViewButtonPanel; private View mMoveToNewerButton; @@ -91,6 +95,8 @@ public class MessageListXL extends Activity implements View.OnClickListener, final boolean isRestoring = (savedInstanceState != null); mContext = getApplicationContext(); + mRefreshManager = RefreshManager.getInstance(this); + mRefreshManager.registerListener(mMailRefreshManagerListener); mFragmentManager.setMailboxListFragmentCallback(new MailboxListFragmentCallback()); mFragmentManager.setMessageListFragmentCallback(new MessageListFragmentCallback()); @@ -171,6 +177,7 @@ public class MessageListXL extends Activity implements View.OnClickListener, @Override protected void onDestroy() { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Email.LOG_TAG, "MessageListXL onDestroy"); + mRefreshManager.unregisterListener(mMailRefreshManagerListener); super.onDestroy(); } @@ -492,6 +499,26 @@ public class MessageListXL extends Activity implements View.OnClickListener, } } + private class RefreshListener + implements RefreshManager.Listener { + @Override + public void onMessagingError(long accountId, long mailboxId, String message) { + Utility.showToast(MessageListXL.this, message); // STOPSHIP temporary UI + invalidateOptionsMenu(); + } + + @Override + public void onRefreshStatusChanged(long accountId, long mailboxId) { + invalidateOptionsMenu(); + } + } + + private boolean isProgressActive() { + final long mailboxId = mFragmentManager.getMailboxId(); + return (mailboxId >= 0) && mRefreshManager.isMessageListRefreshing(mailboxId); + + } + @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); @@ -499,13 +526,26 @@ public class MessageListXL extends Activity implements View.OnClickListener, return true; } + // STOPSHIP - this is a placeholder if/until there's support for progress in actionbar + // Remove it, or replace with a better icon + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem item = menu.findItem(R.id.refresh); + if (isProgressActive()) { + item.setIcon(android.R.drawable.progress_indeterminate_horizontal); + } else { + item.setIcon(R.drawable.ic_menu_refresh); + } + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.compose: return onCompose(); case R.id.refresh: - // TODO Implement this + onRefresh(); return true; case R.id.account_settings: return onAccountSettings(); @@ -539,6 +579,27 @@ public class MessageListXL extends Activity implements View.OnClickListener, return true; } + private void onRefresh() { + // Temporary implementation + if (mFragmentManager.isMailboxSelected()) { + mRefreshManager.refreshMessageList(mFragmentManager.getAccountId(), + mFragmentManager.getMailboxId()); + } + + // TODO implement this + // - Refresh mailbox list. But don't do that always; implement a min interval. + // + // - Refresh the selected mailbox, if it's supported. + // (regardless if the right-pane is MessageList or MessageView) + // - If not suppoted (e.g. outbox, draft, or push mailboxes), refresh the inbox of the + // current account. + + // To do that, we need a way to tell the type of the currently selected mailbox. + // We can do this with MessageListFragment, but it's gone it if a message is being viewed. + // Maybe we should always have a MessageListFragment instance? + // That way it'll be easier to restore the scroll position. + } + /** * STOPSHIP: Remove this. * Rotate screen when the R key is pressed. Workaround for auto-orientation not working. diff --git a/src/com/android/email/activity/MessageViewFragmentBase.java b/src/com/android/email/activity/MessageViewFragmentBase.java index 50e98e208..25d70b18d 100644 --- a/src/com/android/email/activity/MessageViewFragmentBase.java +++ b/src/com/android/email/activity/MessageViewFragmentBase.java @@ -1024,14 +1024,6 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O attachment.downloadButton.setEnabled(enable); } } - - @Override - public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int numNewMessages) { - if (result != null || progress == 100) { - Email.updateMailboxRefreshTime(mailboxId); - } - } } public boolean isMessageLoadedForTest() { diff --git a/src/com/android/email/mail/AuthenticationFailedException.java b/src/com/android/email/mail/AuthenticationFailedException.java index 2bcd606d5..7a767657f 100644 --- a/src/com/android/email/mail/AuthenticationFailedException.java +++ b/src/com/android/email/mail/AuthenticationFailedException.java @@ -16,6 +16,8 @@ package com.android.email.mail; +import com.android.email.R; + public class AuthenticationFailedException extends MessagingException { public static final long serialVersionUID = -1; @@ -29,5 +31,10 @@ public class AuthenticationFailedException extends MessagingException { public AuthenticationFailedException(String message, Throwable throwable) { super(MessagingException.AUTHENTICATION_FAILED, message, throwable); - } + } + + @Override + public int getUiErrorMessageResourceId() { + return R.string.account_setup_failed_dlg_auth_message; + } } diff --git a/src/com/android/email/mail/CertificateValidationException.java b/src/com/android/email/mail/CertificateValidationException.java index 6f8f43fc5..6f8fa85fe 100644 --- a/src/com/android/email/mail/CertificateValidationException.java +++ b/src/com/android/email/mail/CertificateValidationException.java @@ -16,6 +16,8 @@ package com.android.email.mail; +import com.android.email.R; + public class CertificateValidationException extends MessagingException { public static final long serialVersionUID = -1; @@ -26,4 +28,9 @@ public class CertificateValidationException extends MessagingException { public CertificateValidationException(String message, Throwable throwable) { super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable); } + + @Override + public int getUiErrorMessageResourceId() { + return R.string.account_setup_failed_dlg_certificate_message; + } } \ No newline at end of file diff --git a/src/com/android/email/mail/MessagingException.java b/src/com/android/email/mail/MessagingException.java index e8fa83704..981d01148 100644 --- a/src/com/android/email/mail/MessagingException.java +++ b/src/com/android/email/mail/MessagingException.java @@ -16,18 +16,22 @@ package com.android.email.mail; +import com.android.email.R; + +import android.content.Context; + /** * This exception is used for most types of failures that occur during server interactions. - * + * * Data passed through this exception should be considered non-localized. Any strings should * either be internal-only (for debugging) or server-generated. - * + * * TO DO: Does it make sense to further collapse AuthenticationFailedException and * CertificateValidationException and any others into this? */ public class MessagingException extends Exception { public static final long serialVersionUID = -1; - + public static final int NO_ERROR = -1; /** Any exception that does not specify a specific issue */ public static final int UNSPECIFIED_EXCEPTION = 0; @@ -53,9 +57,9 @@ public class MessagingException extends Exception { public static final int CERTIFICATE_VALIDATION_ERROR = 10; /** Authentication failed during autodiscover */ public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11; - + protected int mExceptionType; - + public MessagingException(String message) { super(message); mExceptionType = UNSPECIFIED_EXCEPTION; @@ -70,7 +74,7 @@ public class MessagingException extends Exception { super(message, throwable); mExceptionType = exceptionType; } - + /** * Constructs a MessagingException with an exceptionType and a null message. * @param exceptionType The exception type to set for this exception. @@ -79,7 +83,7 @@ public class MessagingException extends Exception { super(); mExceptionType = exceptionType; } - + /** * Constructs a MessagingException with an exceptionType and a message. * @param exceptionType The exception type to set for this exception. @@ -88,13 +92,41 @@ public class MessagingException extends Exception { super(message); mExceptionType = exceptionType; } - + /** * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set. - * + * * @return Returns the exception type. */ public int getExceptionType() { return mExceptionType; } -} + + /** + * @return the error message associated with this exception. + */ + public final String getUiErrorMessage(Context context) { + return context.getResources().getString(getUiErrorMessageResourceId()); + } + + /** + * @return the resource ID of the error message associated with this exception. + */ + public int getUiErrorMessageResourceId() { + switch (getExceptionType()) { + case MessagingException.IOERROR: + return R.string.account_setup_failed_ioerror; + case MessagingException.TLS_REQUIRED: + return R.string.account_setup_failed_tls_required; + case MessagingException.AUTH_REQUIRED: + return R.string.account_setup_failed_auth_required; + case MessagingException.GENERAL_SECURITY: + return R.string.account_setup_failed_security; + // TODO Generate a unique string for this case, which is the case + // where the security policy needs to be updated. + case MessagingException.SECURITY_POLICIES_REQUIRED: + return R.string.account_setup_failed_security; + } + return R.string.status_network_error; // default + } +} \ No newline at end of file diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index e4066dacd..52d5a4521 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -655,8 +655,6 @@ public class MailService extends Service { updateAccountReport(accountId, -1); } } - // Call the global refresh tracker for all mailboxes - Email.updateMailboxRefreshTime(mailboxId); } } diff --git a/tests/src/com/android/email/MockClock.java b/tests/src/com/android/email/MockClock.java new file mode 100644 index 000000000..2d80dc56e --- /dev/null +++ b/tests/src/com/android/email/MockClock.java @@ -0,0 +1,32 @@ +/* + * 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; + +public class MockClock extends Clock { + public static final long DEFAULT_TIME = 10000; // Arbitrary value + + public long mTime = DEFAULT_TIME; + + @Override + public long getTime() { + return mTime; + } + + public void advance() { + mTime++; + } +} diff --git a/tests/src/com/android/email/RefreshManagerTest.java b/tests/src/com/android/email/RefreshManagerTest.java new file mode 100644 index 000000000..42230e5ed --- /dev/null +++ b/tests/src/com/android/email/RefreshManagerTest.java @@ -0,0 +1,532 @@ +/* + * 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; + +import com.android.email.mail.MessagingException; +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import android.util.Log; + +import java.util.ArrayList; + +import junit.framework.Assert; + +public class RefreshManagerTest extends AndroidTestCase { + private MockClock mClock; + private MockController mController; + private RefreshManager mTarget; + private RefreshListener mListener; + + // Isolated Context for providers. + private Context mProviderContext; + + private static final MessagingException EXCEPTION = new MessagingException("test"); + + // Looks silly, but it'll make it more readable. + private static final long ACCOUNT_1 = 1; + private static final long ACCOUNT_2 = 2; + private static final long MAILBOX_1 = 3; + private static final long MAILBOX_2 = 4; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mClock = new MockClock(); + mController = new MockController(getContext()); + mListener = new RefreshListener(); + mProviderContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext( + mContext, EmailProvider.class); + mTarget = new RefreshManager(mProviderContext, mController, mClock, null); + mTarget.registerListener(mListener); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mController.cleanupForTest(); + } + + public void testRegisterUnregisterListener() { + // mListener is already registered + assertEquals(1, mTarget.getListenersForTest().size()); + + mTarget.unregisterListener(mListener); + assertEquals(0, mTarget.getListenersForTest().size()); + } + + public void testRefreshStatus() { + RefreshManager.Status s = new RefreshManager.Status(); + assertFalse(s.isRefreshing()); + assertTrue(s.canRefresh()); + assertEquals(0, s.getLastRefreshTime()); + + // Request refresh + s.onRefreshRequested(); + assertTrue(s.isRefreshing()); + assertFalse(s.canRefresh()); + assertEquals(0, s.getLastRefreshTime()); + + // Refresh start + s.onCallback(null, 0, mClock); + assertTrue(s.isRefreshing()); + assertFalse(s.canRefresh()); + assertEquals(0, s.getLastRefreshTime()); + + // Refresh 50% done -- nothing changes + s.onCallback(null, 50, mClock); + assertTrue(s.isRefreshing()); + assertFalse(s.canRefresh()); + assertEquals(0, s.getLastRefreshTime()); + + // Refresh finish + s.onCallback(null, 100, mClock); + assertFalse(s.isRefreshing()); + assertTrue(s.canRefresh()); + assertEquals(mClock.mTime, s.getLastRefreshTime()); + + // Refresh start without request + s.onCallback(null, 0, mClock); + assertTrue(s.isRefreshing()); + assertFalse(s.canRefresh()); + assertEquals(mClock.mTime, s.getLastRefreshTime()); + + mClock.advance(); + + // Refresh finish with error. + s.onCallback(EXCEPTION, 0, mClock); + assertFalse(s.isRefreshing()); + assertTrue(s.canRefresh()); + assertEquals(mClock.mTime, s.getLastRefreshTime()); + } + + public void testRefreshMailboxList() { + // request refresh for account 1 + assertTrue(mTarget.refreshMailboxList(ACCOUNT_1)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledUpdateMailboxList); + assertEquals(ACCOUNT_1, mController.mAccountId); + assertEquals(-1, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isMailboxListRefreshing(ACCOUNT_1)); + assertTrue(mTarget.isRefreshingAnyMailboxList()); + + // Request again -- shouldn't be accepted. + assertFalse(mTarget.refreshMailboxList(ACCOUNT_1)); + + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + mListener.reset(); + assertFalse(mController.mCalledUpdateMailboxList); + mController.reset(); + + // request refresh for account 2 + assertTrue(mTarget.refreshMailboxList(ACCOUNT_2)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledUpdateMailboxList); + assertEquals(ACCOUNT_2, mController.mAccountId); + assertEquals(-1, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isMailboxListRefreshing(ACCOUNT_2)); + assertTrue(mTarget.isRefreshingAnyMailboxList()); + + // Refreshing for account 1... + mController.mListener.updateMailboxListCallback(null, ACCOUNT_1, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isMailboxListRefreshing(ACCOUNT_1)); + assertEquals(0, mTarget.getMailboxListStatusForTest(ACCOUNT_1).getLastRefreshTime()); + + // Done. + Log.w(Email.LOG_TAG, "" + mController.mListener.getClass()); + mController.mListener.updateMailboxListCallback(null, ACCOUNT_1, 100); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertFalse(mTarget.isMailboxListRefreshing(ACCOUNT_1)); + assertEquals(mClock.mTime, mTarget.getMailboxListStatusForTest(ACCOUNT_1) + .getLastRefreshTime()); + + // Check "any" method. + assertTrue(mTarget.isRefreshingAnyMailboxList()); // still refreshing account 2 + + // Refreshing for account 2... + mClock.advance(); + + mController.mListener.updateMailboxListCallback(null, ACCOUNT_2, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isMailboxListRefreshing(ACCOUNT_2)); + assertEquals(0, mTarget.getMailboxListStatusForTest(ACCOUNT_2).getLastRefreshTime()); + + // Done with exception. + mController.mListener.updateMailboxListCallback(EXCEPTION, ACCOUNT_2, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertTrue(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + assertEquals(EXCEPTION.getUiErrorMessage(mContext), mListener.mMessage); + mListener.reset(); + assertFalse(mTarget.isMailboxListRefreshing(ACCOUNT_2)); + assertEquals(mClock.mTime, mTarget.getMailboxListStatusForTest(ACCOUNT_2) + .getLastRefreshTime()); + + // Check "any" method. + assertFalse(mTarget.isRefreshingAnyMailboxList()); + } + + public void testRefreshMessageList() { + // request refresh mailbox 1 + assertTrue(mTarget.refreshMessageList(ACCOUNT_1, MAILBOX_1)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(MAILBOX_1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledUpdateMailbox); + assertEquals(ACCOUNT_1, mController.mAccountId); + assertEquals(MAILBOX_1, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isMessageListRefreshing(MAILBOX_1)); + assertTrue(mTarget.isRefreshingAnyMessageList()); + + // Request again -- shouldn't be accepted. + assertFalse(mTarget.refreshMessageList(ACCOUNT_1, MAILBOX_1)); + + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + mListener.reset(); + assertFalse(mController.mCalledUpdateMailbox); + mController.reset(); + + // request refresh mailbox 2 + assertTrue(mTarget.refreshMessageList(ACCOUNT_2, MAILBOX_2)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(MAILBOX_2, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledUpdateMailbox); + assertEquals(ACCOUNT_2, mController.mAccountId); + assertEquals(MAILBOX_2, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isMessageListRefreshing(MAILBOX_2)); + assertTrue(mTarget.isRefreshingAnyMessageList()); + + // Refreshing mailbox 1... + mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 0, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(MAILBOX_1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isMessageListRefreshing(MAILBOX_1)); + assertEquals(0, mTarget.getMessageListStatusForTest(MAILBOX_1).getLastRefreshTime()); + + // Done. + Log.w(Email.LOG_TAG, "" + mController.mListener.getClass()); + mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 100, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(MAILBOX_1, mListener.mMailboxId); + mListener.reset(); + assertFalse(mTarget.isMessageListRefreshing(MAILBOX_1)); + assertEquals(mClock.mTime, mTarget.getMessageListStatusForTest(MAILBOX_1) + .getLastRefreshTime()); + + // Check "any" method. + assertTrue(mTarget.isRefreshingAnyMessageList()); // still refreshing mailbox 2 + + // Refreshing mailbox 2... + mClock.advance(); + + mController.mListener.updateMailboxCallback(null, ACCOUNT_2, MAILBOX_2, 0, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(MAILBOX_2, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isMessageListRefreshing(MAILBOX_2)); + assertEquals(0, mTarget.getMessageListStatusForTest(MAILBOX_2).getLastRefreshTime()); + + // Done with exception. + mController.mListener.updateMailboxCallback(EXCEPTION, ACCOUNT_2, MAILBOX_2, 0, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertTrue(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(MAILBOX_2, mListener.mMailboxId); + assertEquals(EXCEPTION.getUiErrorMessage(mContext), mListener.mMessage); + mListener.reset(); + assertFalse(mTarget.isMessageListRefreshing(MAILBOX_2)); + assertEquals(mClock.mTime, mTarget.getMessageListStatusForTest(MAILBOX_2) + .getLastRefreshTime()); + + // Check "any" method. + assertFalse(mTarget.isRefreshingAnyMessageList()); + } + + public void testSendPendingMessages() { + // request sending for account 1 + assertTrue(mTarget.sendPendingMessages(ACCOUNT_1)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledSendPendingMessages); + assertEquals(ACCOUNT_1, mController.mAccountId); + assertEquals(-1, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isSendingMessage(ACCOUNT_1)); + assertTrue(mTarget.isSendingAnyMessage()); + + // Request again -- shouldn't be accepted. + assertFalse(mTarget.sendPendingMessages(ACCOUNT_1)); + + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + mListener.reset(); + assertFalse(mController.mCalledSendPendingMessages); + mController.reset(); + + // request sending for account 2 + assertTrue(mTarget.sendPendingMessages(ACCOUNT_2)); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mController.mCalledSendPendingMessages); + assertEquals(ACCOUNT_2, mController.mAccountId); + assertEquals(-1, mController.mMailboxId); + mController.reset(); + assertTrue(mTarget.isSendingMessage(ACCOUNT_2)); + assertTrue(mTarget.isSendingAnyMessage()); + + // sending for account 1... + mController.mListener.sendMailCallback(null, ACCOUNT_1, -1, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isSendingMessage(ACCOUNT_1)); + assertEquals(0, mTarget.getOutboxStatusForTest(ACCOUNT_1).getLastRefreshTime()); + + // Per message callback (1) + mController.mListener.sendMailCallback(null, ACCOUNT_1, 100, 0); + mController.mListener.sendMailCallback(null, ACCOUNT_1, 101, 0); + + // No callback per message + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + mListener.reset(); + + // Exception -- first error will be reported. + mController.mListener.sendMailCallback(EXCEPTION, ACCOUNT_1, 102, 0); + + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertTrue(mListener.mCalledOnConnectionError); + assertEquals(EXCEPTION.getUiErrorMessage(mContext), mListener.mMessage); + mListener.reset(); + + // Exception again -- no more error callbacks + mController.mListener.sendMailCallback(null, ACCOUNT_1, 103, 0); + mController.mListener.sendMailCallback(EXCEPTION, ACCOUNT_1, 104, 0); + + assertFalse(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + mListener.reset(); + + // Done. + Log.w(Email.LOG_TAG, "" + mController.mListener.getClass()); + mController.mListener.sendMailCallback(null, ACCOUNT_1, -1, 100); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_1, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertFalse(mTarget.isSendingMessage(ACCOUNT_1)); + assertEquals(mClock.mTime, mTarget.getOutboxStatusForTest(ACCOUNT_1) + .getLastRefreshTime()); + + // Check "any" method. + assertTrue(mTarget.isSendingAnyMessage()); // still sending for account 2 + + // sending for account 2... + mClock.advance(); + + mController.mListener.sendMailCallback(null, ACCOUNT_2, -1, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertFalse(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + mListener.reset(); + assertTrue(mTarget.isSendingMessage(ACCOUNT_2)); + assertEquals(0, mTarget.getOutboxStatusForTest(ACCOUNT_2).getLastRefreshTime()); + + // Done with exception. + mController.mListener.sendMailCallback(EXCEPTION, ACCOUNT_2, -1, 0); + + assertTrue(mListener.mCalledOnRefreshStatusChanged); + assertTrue(mListener.mCalledOnConnectionError); + assertEquals(ACCOUNT_2, mListener.mAccountId); + assertEquals(-1, mListener.mMailboxId); + assertEquals(EXCEPTION.getUiErrorMessage(mContext), mListener.mMessage); + mListener.reset(); + assertFalse(mTarget.isSendingMessage(ACCOUNT_2)); + assertEquals(mClock.mTime, mTarget.getOutboxStatusForTest(ACCOUNT_2) + .getLastRefreshTime()); + + // Check "any" method. + assertFalse(mTarget.isSendingAnyMessage()); + } + + public void testSendPendingMessagesForAllAccounts() { + Account acct1 = ProviderTestUtils.setupAccount("acct1", true, mProviderContext); + Account acct2 = ProviderTestUtils.setupAccount("acct2", true, mProviderContext); + + mTarget.sendPendingMessagesForAllAccountsSync(); + assertTrue(mController.mCalledSendPendingMessages); + + MoreAsserts.assertEquals(new Long[] {acct1.mId, acct2.mId}, mListener.getAccountIds()); + } + + private static class MockController extends Controller { + public long mAccountId = -1; + public long mMailboxId = -1; + public boolean mCalledSendPendingMessages; + public boolean mCalledUpdateMailbox; + public boolean mCalledUpdateMailboxList; + public Result mListener; + + protected MockController(Context context) { + super(context); + } + + public void reset() { + mAccountId = -1; + mMailboxId = -1; + mCalledSendPendingMessages = false; + mCalledUpdateMailbox = false; + mCalledUpdateMailboxList = false; + } + + @Override + public void sendPendingMessages(long accountId) { + mCalledSendPendingMessages = true; + mAccountId = accountId; + } + + @Override + public void updateMailbox(long accountId, long mailboxId) { + mCalledUpdateMailbox = true; + mAccountId = accountId; + mMailboxId = mailboxId; + } + + @Override + public void updateMailboxList(long accountId) { + mCalledUpdateMailboxList = true; + mAccountId = accountId; + } + + @Override + public void addResultCallback(Result listener) { + Assert.assertTrue(mListener == null); + mListener = listener; + } + } + + private static class RefreshListener implements RefreshManager.Listener { + public long mAccountId = -1; + public long mMailboxId = -1; + public String mMessage; + public boolean mCalledOnConnectionError; + public boolean mCalledOnRefreshStatusChanged; + private final ArrayList mAccountIds = new ArrayList(); + + public void reset() { + mAccountId = -1; + mMailboxId = -1; + mMessage = null; + mAccountIds.clear(); + mCalledOnConnectionError = false; + mCalledOnRefreshStatusChanged = false; + } + + @Override + public void onRefreshStatusChanged(long accountId, long mailboxId) { + mAccountId = accountId; + mMailboxId = mailboxId; + mAccountIds.add(mAccountId); + mCalledOnRefreshStatusChanged = true; + } + + @Override + public void onMessagingError(long accountId, long mailboxId, String message) { + mAccountId = accountId; + mMailboxId = mailboxId; + mMessage = message; + mAccountIds.add(mAccountId); + mCalledOnConnectionError = true; + } + + public Long[] getAccountIds() { + return mAccountIds.toArray(new Long[0]); + } + } +}