• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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