/*
* 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.emailcommon.Logging;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.utility.Utility;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
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:
*
* - Taking refresh requests of mailbox-lists and message-lists and the "send outgoing
* messages" requests from UI, and calls appropriate methods of {@link Controller}.
* Note at this point the timer-based refresh
* (by {@link com.android.email.service.MailService}) uses {@link Controller} directly.
*
- Keeping track of which mailbox list/message list is actually being refreshed.
*
* 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.
*
* (All public methods must be called on the UI thread. All callbacks will be called on the UI
* thread.)
*/
public class RefreshManager {
private static final boolean LOG_ENABLED = false; // DONT SUBMIT WITH TRUE
private static final long MAILBOX_AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // in milliseconds
private static final long MAILBOX_LIST_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 {
/**
* Refresh status of a mailbox list or a message list has changed.
*
* @param accountId ID of the account.
* @param mailboxId -1 if it's about the mailbox list, or the ID of the mailbox list in
* question.
*/
public void onRefreshStatusChanged(long accountId, long mailboxId);
/**
* Error callback.
*
* @param accountId ID of the account, or -1 if unknown.
* @param mailboxId ID of the mailbox, or -1 if unknown.
* @param message error message which can be shown to the user.
*/
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();
/**
* @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;
}
protected 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);
}
/**
* MUST be called for mock instances. (The actual instance is a singleton, so no cleanup
* is necessary.)
*/
public void cleanUpForTest() {
mController.removeResultCallback(mControllerResult);
}
public void registerListener(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException();
}
mListeners.add(listener);
}
public void unregisterListener(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException();
}
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;
if (LOG_ENABLED) {
Log.d(Logging.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);
}
public boolean isMailboxListStale(long accountId) {
return mClock.getTime() >= (mMailboxListStatus.get(accountId).getLastRefreshTime()
+ MAILBOX_LIST_AUTO_REFRESH_INTERVAL);
}
/**
* Refresh messages in a mailbox.
*/
public boolean refreshMessageList(long accountId, long mailboxId, boolean userRequest) {
return refreshMessageList(accountId, mailboxId, false, userRequest);
}
/**
* "load more messages" in a mailbox.
*/
public boolean loadMoreMessages(long accountId, long mailboxId) {
return refreshMessageList(accountId, mailboxId, true, true);
}
private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages,
boolean userRequest) {
final Status status = mMessageListStatus.get(mailboxId);
if (!status.canRefresh()) return false;
if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", "
+ loadMoreMessages);
}
status.onRefreshRequested();
notifyRefreshStatusChanged(accountId, mailboxId);
if (loadMoreMessages) {
mController.loadMoreMessages(mailboxId);
} else {
mController.updateMailbox(accountId, mailboxId, userRequest);
}
return true;
}
/**
* Send pending messages.
*/
public boolean sendPendingMessages(long accountId) {
if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "sendPendingMessages " + accountId);
}
notifyRefreshStatusChanged(accountId, -1);
mController.sendPendingMessages(accountId);
return true;
}
/**
* Call {@link #sendPendingMessages} for all accounts.
*/
public void sendPendingMessagesForAllAccounts() {
if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "sendPendingMessagesForAllAccounts");
}
new SendPendingMessagesForAllAccountsImpl()
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount {
public SendPendingMessagesForAllAccountsImpl() {
super(mContext);
}
@Override
protected void performAction(long accountId) {
sendPendingMessages(accountId);
}
}
public long getLastMailboxListRefreshTime(long accountId) {
return mMailboxListStatus.get(accountId).getLastRefreshTime();
}
public long getLastMessageListRefreshTime(long mailboxId) {
return mMessageListStatus.get(mailboxId).getLastRefreshTime();
}
public boolean isMailboxListRefreshing(long accountId) {
return mMailboxListStatus.get(accountId).isRefreshing();
}
public boolean isMessageListRefreshing(long mailboxId) {
return mMessageListStatus.get(mailboxId).isRefreshing();
}
public boolean isRefreshingAnyMailboxListForTest() {
return mMailboxListStatus.isRefreshingAny();
}
public boolean isRefreshingAnyMessageListForTest() {
return mMessageListStatus.isRefreshingAny();
}
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);
}
private class ControllerResult extends Controller.Result {
private boolean mSendMailExceptionReported = false;
private String exceptionToString(MessagingException exception) {
if (exception == null) {
return "(no exception)";
} else {
return MessagingExceptionStrings.getErrorString(mContext, exception);
}
}
/**
* Callback for mailbox list refresh.
*/
@Override
public void updateMailboxListCallback(MessagingException exception, long accountId,
int progress) {
if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress
+ ", " + exceptionToString(exception));
}
mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock);
if (exception != null) {
reportError(accountId, -1,
MessagingExceptionStrings.getErrorString(mContext, exception));
}
notifyRefreshStatusChanged(accountId, -1);
}
/**
* Callback for explicit (user-driven) mailbox refresh.
*/
@Override
public void updateMailboxCallback(MessagingException exception, long accountId,
long mailboxId, int progress, int dontUseNumNewMessages,
ArrayList addedMessages) {
if (LOG_ENABLED) {
Log.d(Logging.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 (LOG_ENABLED) {
Log.d(Logging.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,
MessagingExceptionStrings.getErrorString(mContext, exception));
}
notifyRefreshStatusChanged(accountId, mailboxId);
}
/**
* Send message progress callback.
*
* We don't keep track of the status of outboxes, but we monitor this to catch
* errors.
*/
@Override
public void sendMailCallback(MessagingException exception, long accountId, long messageId,
int progress) {
if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "sendMailCallback " + accountId + ", "
+ messageId + ", " + progress + ", " + exceptionToString(exception));
}
if (progress == 0 && messageId == -1) {
mSendMailExceptionReported = false;
}
if (exception != null && !mSendMailExceptionReported) {
// Only the first error in a batch will be reported.
mSendMailExceptionReported = true;
reportError(accountId, messageId,
MessagingExceptionStrings.getErrorString(mContext, exception));
}
if (progress == 100) {
mSendMailExceptionReported = false;
}
}
}
}