1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.email; 18 19 import com.android.emailcommon.Logging; 20 import com.android.emailcommon.mail.MessagingException; 21 import com.android.emailcommon.utility.Utility; 22 23 import android.content.Context; 24 import android.os.AsyncTask; 25 import android.os.Handler; 26 import android.util.Log; 27 28 import java.util.ArrayList; 29 import java.util.Collection; 30 import java.util.HashMap; 31 32 /** 33 * Class that handles "refresh" (and "send pending messages" for outboxes) related functionalities. 34 * 35 * <p>This class is responsible for two things: 36 * <ul> 37 * <li>Taking refresh requests of mailbox-lists and message-lists and the "send outgoing 38 * messages" requests from UI, and calls appropriate methods of {@link Controller}. 39 * Note at this point the timer-based refresh 40 * (by {@link com.android.email.service.MailService}) uses {@link Controller} directly. 41 * <li>Keeping track of which mailbox list/message list is actually being refreshed. 42 * </ul> 43 * Refresh requests will be ignored if a request to the same target is already requested, or is 44 * already being refreshed. 45 * 46 * <p>Conceptually it can be a part of {@link Controller}, but extracted for easy testing. 47 * 48 * (All public methods must be called on the UI thread. All callbacks will be called on the UI 49 * thread.) 50 */ 51 public class RefreshManager { 52 private static final boolean LOG_ENABLED = false; // DONT SUBMIT WITH TRUE 53 private static final long MAILBOX_AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // in milliseconds 54 private static final long MAILBOX_LIST_AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // in milliseconds 55 56 private static RefreshManager sInstance; 57 58 private final Clock mClock; 59 private final Context mContext; 60 private final Controller mController; 61 private final Controller.Result mControllerResult; 62 63 /** Last error message */ 64 private String mErrorMessage; 65 66 public interface Listener { 67 /** 68 * Refresh status of a mailbox list or a message list has changed. 69 * 70 * @param accountId ID of the account. 71 * @param mailboxId -1 if it's about the mailbox list, or the ID of the mailbox list in 72 * question. 73 */ onRefreshStatusChanged(long accountId, long mailboxId)74 public void onRefreshStatusChanged(long accountId, long mailboxId); 75 76 /** 77 * Error callback. 78 * 79 * @param accountId ID of the account, or -1 if unknown. 80 * @param mailboxId ID of the mailbox, or -1 if unknown. 81 * @param message error message which can be shown to the user. 82 */ onMessagingError(long accountId, long mailboxId, String message)83 public void onMessagingError(long accountId, long mailboxId, String message); 84 } 85 86 private final ArrayList<Listener> mListeners = new ArrayList<Listener>(); 87 88 /** 89 * Status of a mailbox list/message list. 90 */ 91 /* package */ static class Status { 92 /** 93 * True if a refresh of the mailbox is requested, and not finished yet. 94 */ 95 private boolean mIsRefreshRequested; 96 97 /** 98 * True if the mailbox is being refreshed. 99 * 100 * Set true when {@link #onRefreshRequested} is called, i.e. refresh is requested by UI. 101 * Note refresh can occur without a request from UI as well (e.g. timer based refresh). 102 * In which case, {@link #mIsRefreshing} will be true with {@link #mIsRefreshRequested} 103 * being false. 104 */ 105 private boolean mIsRefreshing; 106 107 private long mLastRefreshTime; 108 isRefreshing()109 public boolean isRefreshing() { 110 return mIsRefreshRequested || mIsRefreshing; 111 } 112 canRefresh()113 public boolean canRefresh() { 114 return !isRefreshing(); 115 } 116 onRefreshRequested()117 public void onRefreshRequested() { 118 mIsRefreshRequested = true; 119 } 120 getLastRefreshTime()121 public long getLastRefreshTime() { 122 return mLastRefreshTime; 123 } 124 onCallback(MessagingException exception, int progress, Clock clock)125 public void onCallback(MessagingException exception, int progress, Clock clock) { 126 if (exception == null && progress == 0) { 127 // Refresh started 128 mIsRefreshing = true; 129 } else if (exception != null || progress == 100) { 130 // Refresh finished 131 mIsRefreshing = false; 132 mIsRefreshRequested = false; 133 mLastRefreshTime = clock.getTime(); 134 } 135 } 136 } 137 138 /** 139 * Map of accounts/mailboxes to {@link Status}. 140 */ 141 private static class RefreshStatusMap { 142 private final HashMap<Long, Status> mMap = new HashMap<Long, Status>(); 143 get(long id)144 public Status get(long id) { 145 Status s = mMap.get(id); 146 if (s == null) { 147 s = new Status(); 148 mMap.put(id, s); 149 } 150 return s; 151 } 152 isRefreshingAny()153 public boolean isRefreshingAny() { 154 for (Status s : mMap.values()) { 155 if (s.isRefreshing()) { 156 return true; 157 } 158 } 159 return false; 160 } 161 } 162 163 private final RefreshStatusMap mMailboxListStatus = new RefreshStatusMap(); 164 private final RefreshStatusMap mMessageListStatus = new RefreshStatusMap(); 165 166 /** 167 * @return the singleton instance. 168 */ getInstance(Context context)169 public static synchronized RefreshManager getInstance(Context context) { 170 if (sInstance == null) { 171 sInstance = new RefreshManager(context, Controller.getInstance(context), 172 Clock.INSTANCE, new Handler()); 173 } 174 return sInstance; 175 } 176 RefreshManager(Context context, Controller controller, Clock clock, Handler handler)177 protected RefreshManager(Context context, Controller controller, Clock clock, 178 Handler handler) { 179 mClock = clock; 180 mContext = context.getApplicationContext(); 181 mController = controller; 182 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>( 183 handler, new ControllerResult()); 184 mController.addResultCallback(mControllerResult); 185 } 186 187 /** 188 * MUST be called for mock instances. (The actual instance is a singleton, so no cleanup 189 * is necessary.) 190 */ cleanUpForTest()191 public void cleanUpForTest() { 192 mController.removeResultCallback(mControllerResult); 193 } 194 registerListener(Listener listener)195 public void registerListener(Listener listener) { 196 if (listener == null) { 197 throw new IllegalArgumentException(); 198 } 199 mListeners.add(listener); 200 } 201 unregisterListener(Listener listener)202 public void unregisterListener(Listener listener) { 203 if (listener == null) { 204 throw new IllegalArgumentException(); 205 } 206 mListeners.remove(listener); 207 } 208 209 /** 210 * Refresh the mailbox list of an account. 211 */ refreshMailboxList(long accountId)212 public boolean refreshMailboxList(long accountId) { 213 final Status status = mMailboxListStatus.get(accountId); 214 if (!status.canRefresh()) return false; 215 216 if (LOG_ENABLED) { 217 Log.d(Logging.LOG_TAG, "refreshMailboxList " + accountId); 218 } 219 status.onRefreshRequested(); 220 notifyRefreshStatusChanged(accountId, -1); 221 mController.updateMailboxList(accountId); 222 return true; 223 } 224 isMailboxStale(long mailboxId)225 public boolean isMailboxStale(long mailboxId) { 226 return mClock.getTime() >= (mMessageListStatus.get(mailboxId).getLastRefreshTime() 227 + MAILBOX_AUTO_REFRESH_INTERVAL); 228 } 229 isMailboxListStale(long accountId)230 public boolean isMailboxListStale(long accountId) { 231 return mClock.getTime() >= (mMailboxListStatus.get(accountId).getLastRefreshTime() 232 + MAILBOX_LIST_AUTO_REFRESH_INTERVAL); 233 } 234 235 /** 236 * Refresh messages in a mailbox. 237 */ refreshMessageList(long accountId, long mailboxId, boolean userRequest)238 public boolean refreshMessageList(long accountId, long mailboxId, boolean userRequest) { 239 return refreshMessageList(accountId, mailboxId, false, userRequest); 240 } 241 242 /** 243 * "load more messages" in a mailbox. 244 */ loadMoreMessages(long accountId, long mailboxId)245 public boolean loadMoreMessages(long accountId, long mailboxId) { 246 return refreshMessageList(accountId, mailboxId, true, true); 247 } 248 refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages, boolean userRequest)249 private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages, 250 boolean userRequest) { 251 final Status status = mMessageListStatus.get(mailboxId); 252 if (!status.canRefresh()) return false; 253 254 if (LOG_ENABLED) { 255 Log.d(Logging.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", " 256 + loadMoreMessages); 257 } 258 status.onRefreshRequested(); 259 notifyRefreshStatusChanged(accountId, mailboxId); 260 if (loadMoreMessages) { 261 mController.loadMoreMessages(mailboxId); 262 } else { 263 mController.updateMailbox(accountId, mailboxId, userRequest); 264 } 265 return true; 266 } 267 268 /** 269 * Send pending messages. 270 */ sendPendingMessages(long accountId)271 public boolean sendPendingMessages(long accountId) { 272 if (LOG_ENABLED) { 273 Log.d(Logging.LOG_TAG, "sendPendingMessages " + accountId); 274 } 275 notifyRefreshStatusChanged(accountId, -1); 276 mController.sendPendingMessages(accountId); 277 return true; 278 } 279 280 /** 281 * Call {@link #sendPendingMessages} for all accounts. 282 */ sendPendingMessagesForAllAccounts()283 public void sendPendingMessagesForAllAccounts() { 284 if (LOG_ENABLED) { 285 Log.d(Logging.LOG_TAG, "sendPendingMessagesForAllAccounts"); 286 } 287 new SendPendingMessagesForAllAccountsImpl() 288 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 289 } 290 291 private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount { SendPendingMessagesForAllAccountsImpl()292 public SendPendingMessagesForAllAccountsImpl() { 293 super(mContext); 294 } 295 296 @Override performAction(long accountId)297 protected void performAction(long accountId) { 298 sendPendingMessages(accountId); 299 } 300 } 301 getLastMailboxListRefreshTime(long accountId)302 public long getLastMailboxListRefreshTime(long accountId) { 303 return mMailboxListStatus.get(accountId).getLastRefreshTime(); 304 } 305 getLastMessageListRefreshTime(long mailboxId)306 public long getLastMessageListRefreshTime(long mailboxId) { 307 return mMessageListStatus.get(mailboxId).getLastRefreshTime(); 308 } 309 isMailboxListRefreshing(long accountId)310 public boolean isMailboxListRefreshing(long accountId) { 311 return mMailboxListStatus.get(accountId).isRefreshing(); 312 } 313 isMessageListRefreshing(long mailboxId)314 public boolean isMessageListRefreshing(long mailboxId) { 315 return mMessageListStatus.get(mailboxId).isRefreshing(); 316 } 317 isRefreshingAnyMailboxListForTest()318 public boolean isRefreshingAnyMailboxListForTest() { 319 return mMailboxListStatus.isRefreshingAny(); 320 } 321 isRefreshingAnyMessageListForTest()322 public boolean isRefreshingAnyMessageListForTest() { 323 return mMessageListStatus.isRefreshingAny(); 324 } 325 getErrorMessage()326 public String getErrorMessage() { 327 return mErrorMessage; 328 } 329 notifyRefreshStatusChanged(long accountId, long mailboxId)330 private void notifyRefreshStatusChanged(long accountId, long mailboxId) { 331 for (Listener l : mListeners) { 332 l.onRefreshStatusChanged(accountId, mailboxId); 333 } 334 } 335 reportError(long accountId, long mailboxId, String errorMessage)336 private void reportError(long accountId, long mailboxId, String errorMessage) { 337 mErrorMessage = errorMessage; 338 for (Listener l : mListeners) { 339 l.onMessagingError(accountId, mailboxId, mErrorMessage); 340 } 341 } 342 getListenersForTest()343 /* package */ Collection<Listener> getListenersForTest() { 344 return mListeners; 345 } 346 getMailboxListStatusForTest(long accountId)347 /* package */ Status getMailboxListStatusForTest(long accountId) { 348 return mMailboxListStatus.get(accountId); 349 } 350 getMessageListStatusForTest(long mailboxId)351 /* package */ Status getMessageListStatusForTest(long mailboxId) { 352 return mMessageListStatus.get(mailboxId); 353 } 354 355 private class ControllerResult extends Controller.Result { 356 private boolean mSendMailExceptionReported = false; 357 exceptionToString(MessagingException exception)358 private String exceptionToString(MessagingException exception) { 359 if (exception == null) { 360 return "(no exception)"; 361 } else { 362 return MessagingExceptionStrings.getErrorString(mContext, exception); 363 } 364 } 365 366 /** 367 * Callback for mailbox list refresh. 368 */ 369 @Override updateMailboxListCallback(MessagingException exception, long accountId, int progress)370 public void updateMailboxListCallback(MessagingException exception, long accountId, 371 int progress) { 372 if (LOG_ENABLED) { 373 Log.d(Logging.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress 374 + ", " + exceptionToString(exception)); 375 } 376 mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock); 377 if (exception != null) { 378 reportError(accountId, -1, 379 MessagingExceptionStrings.getErrorString(mContext, exception)); 380 } 381 notifyRefreshStatusChanged(accountId, -1); 382 } 383 384 /** 385 * Callback for explicit (user-driven) mailbox refresh. 386 */ 387 @Override updateMailboxCallback(MessagingException exception, long accountId, long mailboxId, int progress, int dontUseNumNewMessages, ArrayList<Long> addedMessages)388 public void updateMailboxCallback(MessagingException exception, long accountId, 389 long mailboxId, int progress, int dontUseNumNewMessages, 390 ArrayList<Long> addedMessages) { 391 if (LOG_ENABLED) { 392 Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", " 393 + mailboxId + ", " + progress + ", " + exceptionToString(exception)); 394 } 395 updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); 396 } 397 398 /** 399 * Callback for implicit (timer-based) mailbox refresh. 400 * 401 * Do the same as {@link #updateMailboxCallback}. 402 * TODO: Figure out if it's really okay to do the same as updateMailboxCallback. 403 * If both the explicit refresh and the implicit refresh can run at the same time, 404 * we need to keep track of their status separately. 405 */ 406 @Override serviceCheckMailCallback( MessagingException exception, long accountId, long mailboxId, int progress, long tag)407 public void serviceCheckMailCallback( 408 MessagingException exception, long accountId, long mailboxId, int progress, 409 long tag) { 410 if (LOG_ENABLED) { 411 Log.d(Logging.LOG_TAG, "serviceCheckMailCallback " + accountId + ", " 412 + mailboxId + ", " + progress + ", " + exceptionToString(exception)); 413 } 414 updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); 415 } 416 updateMailboxCallbackInternal(MessagingException exception, long accountId, long mailboxId, int progress, int dontUseNumNewMessages)417 private void updateMailboxCallbackInternal(MessagingException exception, long accountId, 418 long mailboxId, int progress, int dontUseNumNewMessages) { 419 // Don't use dontUseNumNewMessages. serviceCheckMailCallback() don't set it. 420 mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock); 421 if (exception != null) { 422 reportError(accountId, mailboxId, 423 MessagingExceptionStrings.getErrorString(mContext, exception)); 424 } 425 notifyRefreshStatusChanged(accountId, mailboxId); 426 } 427 428 429 /** 430 * Send message progress callback. 431 * 432 * We don't keep track of the status of outboxes, but we monitor this to catch 433 * errors. 434 */ 435 @Override sendMailCallback(MessagingException exception, long accountId, long messageId, int progress)436 public void sendMailCallback(MessagingException exception, long accountId, long messageId, 437 int progress) { 438 if (LOG_ENABLED) { 439 Log.d(Logging.LOG_TAG, "sendMailCallback " + accountId + ", " 440 + messageId + ", " + progress + ", " + exceptionToString(exception)); 441 } 442 if (progress == 0 && messageId == -1) { 443 mSendMailExceptionReported = false; 444 } 445 if (exception != null && !mSendMailExceptionReported) { 446 // Only the first error in a batch will be reported. 447 mSendMailExceptionReported = true; 448 reportError(accountId, messageId, 449 MessagingExceptionStrings.getErrorString(mContext, exception)); 450 } 451 if (progress == 100) { 452 mSendMailExceptionReported = false; 453 } 454 } 455 } 456 } 457