RefreshManager.java revision 9227dbbf0f1c467762c44119d7cb1140c7191a88
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 17package com.android.email; 18 19import android.content.Context; 20import android.os.AsyncTask; 21import android.os.Handler; 22import android.util.Log; 23 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.mail.MessagingException; 26import com.android.emailcommon.utility.Utility; 27 28import java.util.ArrayList; 29import java.util.Collection; 30import 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 */ 51public 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 */ 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 */ 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 109 public boolean isRefreshing() { 110 // NOTE: For now, we're always allowing refresh (during service refactor) 111 return mIsRefreshRequested || mIsRefreshing; 112 } 113 114 public boolean canRefresh() { 115 // NOTE: For now, we're always allowing refresh (during service refactor) 116 return !isRefreshing(); 117 } 118 119 public void onRefreshRequested() { 120 mIsRefreshRequested = true; 121 } 122 123 public long getLastRefreshTime() { 124 return mLastRefreshTime; 125 } 126 127 public void onCallback(MessagingException exception, int progress, Clock clock) { 128 if (exception == null && progress == 0) { 129 // Refresh started 130 mIsRefreshing = true; 131 } else if (exception != null || progress == 100) { 132 // Refresh finished 133 mIsRefreshing = false; 134 mIsRefreshRequested = false; 135 mLastRefreshTime = clock.getTime(); 136 } 137 } 138 } 139 140 /** 141 * Map of accounts/mailboxes to {@link Status}. 142 */ 143 private static class RefreshStatusMap { 144 private final HashMap<Long, Status> mMap = new HashMap<Long, Status>(); 145 146 public Status get(long id) { 147 Status s = mMap.get(id); 148 if (s == null) { 149 s = new Status(); 150 mMap.put(id, s); 151 } 152 return s; 153 } 154 155 public boolean isRefreshingAny() { 156 for (Status s : mMap.values()) { 157 if (s.isRefreshing()) { 158 return true; 159 } 160 } 161 return false; 162 } 163 } 164 165 private final RefreshStatusMap mMailboxListStatus = new RefreshStatusMap(); 166 private final RefreshStatusMap mMessageListStatus = new RefreshStatusMap(); 167 168 /** 169 * @return the singleton instance. 170 */ 171 public static synchronized RefreshManager getInstance(Context context) { 172 if (sInstance == null) { 173 sInstance = new RefreshManager(context, Controller.getInstance(context), 174 Clock.INSTANCE, new Handler()); 175 } 176 return sInstance; 177 } 178 179 protected RefreshManager(Context context, Controller controller, Clock clock, 180 Handler handler) { 181 mClock = clock; 182 mContext = context.getApplicationContext(); 183 mController = controller; 184 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>( 185 handler, new ControllerResult()); 186 mController.addResultCallback(mControllerResult); 187 } 188 189 /** 190 * MUST be called for mock instances. (The actual instance is a singleton, so no cleanup 191 * is necessary.) 192 */ 193 public void cleanUpForTest() { 194 mController.removeResultCallback(mControllerResult); 195 } 196 197 public void registerListener(Listener listener) { 198 if (listener == null) { 199 throw new IllegalArgumentException(); 200 } 201 mListeners.add(listener); 202 } 203 204 public void unregisterListener(Listener listener) { 205 if (listener == null) { 206 throw new IllegalArgumentException(); 207 } 208 mListeners.remove(listener); 209 } 210 211 /** 212 * Refresh the mailbox list of an account. 213 */ 214 public boolean refreshMailboxList(long accountId) { 215 final Status status = mMailboxListStatus.get(accountId); 216 if (!status.canRefresh()) return false; 217 218 if (LOG_ENABLED) { 219 Log.d(Logging.LOG_TAG, "refreshMailboxList " + accountId); 220 } 221 status.onRefreshRequested(); 222 notifyRefreshStatusChanged(accountId, -1); 223 mController.updateMailboxList(accountId); 224 return true; 225 } 226 227 public boolean isMailboxStale(long mailboxId) { 228 return mClock.getTime() >= (mMessageListStatus.get(mailboxId).getLastRefreshTime() 229 + MAILBOX_AUTO_REFRESH_INTERVAL); 230 } 231 232 public boolean isMailboxListStale(long accountId) { 233 return mClock.getTime() >= (mMailboxListStatus.get(accountId).getLastRefreshTime() 234 + MAILBOX_LIST_AUTO_REFRESH_INTERVAL); 235 } 236 237 /** 238 * Refresh messages in a mailbox. 239 */ 240 public boolean refreshMessageList(long accountId, long mailboxId, boolean userRequest) { 241 return refreshMessageList(accountId, mailboxId, false, userRequest); 242 } 243 244 /** 245 * "load more messages" in a mailbox. 246 */ 247 public boolean loadMoreMessages(long accountId, long mailboxId) { 248 return refreshMessageList(accountId, mailboxId, true, true); 249 } 250 251 private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages, 252 boolean userRequest) { 253 final Status status = mMessageListStatus.get(mailboxId); 254 if (!status.canRefresh()) return false; 255 256 if (LOG_ENABLED) { 257 Log.d(Logging.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", " 258 + loadMoreMessages); 259 } 260 status.onRefreshRequested(); 261 notifyRefreshStatusChanged(accountId, mailboxId); 262 if (loadMoreMessages) { 263 mController.loadMoreMessages(mailboxId); 264 } else { 265 mController.updateMailbox(accountId, mailboxId, userRequest); 266 } 267 return true; 268 } 269 270 /** 271 * Send pending messages. 272 */ 273 public boolean sendPendingMessages(long accountId) { 274 if (LOG_ENABLED) { 275 Log.d(Logging.LOG_TAG, "sendPendingMessages " + accountId); 276 } 277 notifyRefreshStatusChanged(accountId, -1); 278 mController.sendPendingMessages(accountId); 279 return true; 280 } 281 282 /** 283 * Call {@link #sendPendingMessages} for all accounts. 284 */ 285 public void sendPendingMessagesForAllAccounts() { 286 if (LOG_ENABLED) { 287 Log.d(Logging.LOG_TAG, "sendPendingMessagesForAllAccounts"); 288 } 289 new SendPendingMessagesForAllAccountsImpl() 290 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 291 } 292 293 private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount { 294 public SendPendingMessagesForAllAccountsImpl() { 295 super(mContext); 296 } 297 298 @Override 299 protected void performAction(long accountId) { 300 sendPendingMessages(accountId); 301 } 302 } 303 304 public long getLastMailboxListRefreshTime(long accountId) { 305 return mMailboxListStatus.get(accountId).getLastRefreshTime(); 306 } 307 308 public long getLastMessageListRefreshTime(long mailboxId) { 309 return mMessageListStatus.get(mailboxId).getLastRefreshTime(); 310 } 311 312 public boolean isMailboxListRefreshing(long accountId) { 313 return mMailboxListStatus.get(accountId).isRefreshing(); 314 } 315 316 public boolean isMessageListRefreshing(long mailboxId) { 317 return mMessageListStatus.get(mailboxId).isRefreshing(); 318 } 319 320 public boolean isRefreshingAnyMailboxListForTest() { 321 return mMailboxListStatus.isRefreshingAny(); 322 } 323 324 public boolean isRefreshingAnyMessageListForTest() { 325 return mMessageListStatus.isRefreshingAny(); 326 } 327 328 public String getErrorMessage() { 329 return mErrorMessage; 330 } 331 332 private void notifyRefreshStatusChanged(long accountId, long mailboxId) { 333 for (Listener l : mListeners) { 334 l.onRefreshStatusChanged(accountId, mailboxId); 335 } 336 } 337 338 private void reportError(long accountId, long mailboxId, String errorMessage) { 339 mErrorMessage = errorMessage; 340 for (Listener l : mListeners) { 341 l.onMessagingError(accountId, mailboxId, mErrorMessage); 342 } 343 } 344 345 /* package */ Collection<Listener> getListenersForTest() { 346 return mListeners; 347 } 348 349 /* package */ Status getMailboxListStatusForTest(long accountId) { 350 return mMailboxListStatus.get(accountId); 351 } 352 353 /* package */ Status getMessageListStatusForTest(long mailboxId) { 354 return mMessageListStatus.get(mailboxId); 355 } 356 357 private class ControllerResult extends Controller.Result { 358 359 private String exceptionToString(MessagingException exception) { 360 if (exception == null) { 361 return "(no exception)"; 362 } else { 363 return MessagingExceptionStrings.getErrorString(mContext, exception); 364 } 365 } 366 367 /** 368 * Callback for mailbox list refresh. 369 */ 370 @Override 371 public void updateMailboxListCallback(MessagingException exception, long accountId, 372 int progress) { 373 if (LOG_ENABLED) { 374 Log.d(Logging.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress 375 + ", " + exceptionToString(exception)); 376 } 377 mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock); 378 if (exception != null) { 379 reportError(accountId, -1, 380 MessagingExceptionStrings.getErrorString(mContext, exception)); 381 } 382 notifyRefreshStatusChanged(accountId, -1); 383 } 384 385 /** 386 * Callback for explicit (user-driven) mailbox refresh. 387 */ 388 @Override 389 public void updateMailboxCallback(MessagingException exception, long accountId, 390 long mailboxId, int progress, int dontUseNumNewMessages, 391 ArrayList<Long> addedMessages) { 392 if (LOG_ENABLED) { 393 Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", " 394 + mailboxId + ", " + progress + ", " + exceptionToString(exception)); 395 } 396 updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); 397 } 398 399 /** 400 * Callback for implicit (timer-based) mailbox refresh. 401 * 402 * Do the same as {@link #updateMailboxCallback}. 403 * TODO: Figure out if it's really okay to do the same as updateMailboxCallback. 404 * If both the explicit refresh and the implicit refresh can run at the same time, 405 * we need to keep track of their status separately. 406 */ 407 @Override 408 public void serviceCheckMailCallback( 409 MessagingException exception, long accountId, long mailboxId, int progress, 410 long tag) { 411 if (LOG_ENABLED) { 412 Log.d(Logging.LOG_TAG, "serviceCheckMailCallback " + accountId + ", " 413 + mailboxId + ", " + progress + ", " + exceptionToString(exception)); 414 } 415 updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0); 416 } 417 418 private void updateMailboxCallbackInternal(MessagingException exception, long accountId, 419 long mailboxId, int progress, int dontUseNumNewMessages) { 420 // Don't use dontUseNumNewMessages. serviceCheckMailCallback() don't set it. 421 mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock); 422 if (exception != null) { 423 reportError(accountId, mailboxId, 424 MessagingExceptionStrings.getErrorString(mContext, exception)); 425 } 426 notifyRefreshStatusChanged(accountId, mailboxId); 427 } 428 } 429} 430