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