RefreshManager.java revision e357f5879187124c7af5c2ece5d7d3e4f60f07d2
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        mController.updateMailbox(accountId, mailboxId);
234        return true;
235    }
236
237    /**
238     * Send pending messages.
239     */
240    public boolean sendPendingMessages(long accountId) {
241        final Status status = mOutboxStatus.get(accountId);
242        if (!status.canRefresh()) return false;
243
244        Log.i(Email.LOG_TAG, "sendPendingMessages " + accountId);
245        status.onRefreshRequested();
246        notifyRefreshStatusChanged(accountId, -1);
247        mController.sendPendingMessages(accountId);
248        return true;
249    }
250
251    /**
252     * Call {@link #sendPendingMessages} for all accounts.
253     */
254    public void sendPendingMessagesForAllAccounts() {
255        Log.i(Email.LOG_TAG, "sendPendingMessagesForAllAccounts");
256        new SendPendingMessagesForAllAccountsImpl().execute();
257    }
258
259    private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount {
260        public SendPendingMessagesForAllAccountsImpl() {
261            super(mContext);
262        }
263
264        @Override
265        protected void performAction(long accountId) {
266            sendPendingMessages(accountId);
267        }
268    }
269
270    public long getLastMailboxListRefreshTime(long accountId) {
271        return mMailboxListStatus.get(accountId).getLastRefreshTime();
272    }
273
274    public long getLastMessageListRefreshTime(long mailboxId) {
275        return mMessageListStatus.get(mailboxId).getLastRefreshTime();
276    }
277
278    public long getLastSendMessageTime(long accountId) {
279        return mOutboxStatus.get(accountId).getLastRefreshTime();
280    }
281
282    public boolean isMailboxListRefreshing(long accountId) {
283        return mMailboxListStatus.get(accountId).isRefreshing();
284    }
285
286    public boolean isMessageListRefreshing(long mailboxId) {
287        return mMessageListStatus.get(mailboxId).isRefreshing();
288    }
289
290    public boolean isSendingMessage(long accountId) {
291        return mOutboxStatus.get(accountId).isRefreshing();
292    }
293
294    public boolean isRefreshingAnyMailboxList() {
295        return mMailboxListStatus.isRefreshingAny();
296    }
297
298    public boolean isRefreshingAnyMessageList() {
299        return mMessageListStatus.isRefreshingAny();
300    }
301
302    public boolean isSendingAnyMessage() {
303        return mOutboxStatus.isRefreshingAny();
304    }
305
306    public boolean isRefreshingOrSendingAny() {
307        return isRefreshingAnyMailboxList() || isRefreshingAnyMessageList()
308                || isSendingAnyMessage();
309    }
310
311    public String getErrorMessage() {
312        return mErrorMessage;
313    }
314
315    private void notifyRefreshStatusChanged(long accountId, long mailboxId) {
316        for (Listener l : mListeners) {
317            l.onRefreshStatusChanged(accountId, mailboxId);
318        }
319    }
320
321    private void reportError(long accountId, long mailboxId, String errorMessage) {
322        mErrorMessage = errorMessage;
323        for (Listener l : mListeners) {
324            l.onMessagingError(accountId, mailboxId, mErrorMessage);
325        }
326    }
327
328    /* package */ Collection<Listener> getListenersForTest() {
329        return mListeners;
330    }
331
332    /* package */ Status getMailboxListStatusForTest(long accountId) {
333        return mMailboxListStatus.get(accountId);
334    }
335
336    /* package */ Status getMessageListStatusForTest(long mailboxId) {
337        return mMessageListStatus.get(mailboxId);
338    }
339
340    /* package */ Status getOutboxStatusForTest(long acountId) {
341        return mOutboxStatus.get(acountId);
342    }
343
344    private class ControllerResult extends Controller.Result {
345        private boolean mSendMailExceptionReported = false;
346
347        private String exceptionToString(MessagingException exception) {
348            if (exception == null) {
349                return "(no exception)";
350            } else {
351                return exception.getUiErrorMessage(mContext);
352            }
353        }
354
355        /**
356         * Callback for mailbox list refresh.
357         */
358        @Override
359        public void updateMailboxListCallback(MessagingException exception, long accountId,
360                int progress) {
361            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
362                Log.d(Email.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress
363                        + ", " + exceptionToString(exception));
364            }
365            mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock);
366            if (exception != null) {
367                reportError(accountId, -1, exception.getUiErrorMessage(mContext));
368            }
369            notifyRefreshStatusChanged(accountId, -1);
370        }
371
372        /**
373         * Callback for explicit (user-driven) mailbox refresh.
374         */
375        @Override
376        public void updateMailboxCallback(MessagingException exception, long accountId,
377                long mailboxId, int progress, int dontUseNumNewMessages) {
378            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
379                Log.d(Email.LOG_TAG, "updateMailboxCallback " + accountId + ", "
380                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
381            }
382            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
383        }
384
385        /**
386         * Callback for implicit (timer-based) mailbox refresh.
387         *
388         * Do the same as {@link #updateMailboxCallback}.
389         * TODO: Figure out if it's really okay to do the same as updateMailboxCallback.
390         * If both the explicit refresh and the implicit refresh can run at the same time,
391         * we need to keep track of their status separately.
392         */
393        @Override
394        public void serviceCheckMailCallback(
395                MessagingException exception, long accountId, long mailboxId, int progress,
396                long tag) {
397            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
398                Log.d(Email.LOG_TAG, "serviceCheckMailCallback " + accountId + ", "
399                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
400            }
401            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
402        }
403
404        private void updateMailboxCallbackInternal(MessagingException exception, long accountId,
405                long mailboxId, int progress, int dontUseNumNewMessages) {
406            // Don't use dontUseNumNewMessages.  serviceCheckMailCallback() don't set it.
407            mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock);
408            if (exception != null) {
409                reportError(accountId, mailboxId, exception.getUiErrorMessage(mContext));
410            }
411            notifyRefreshStatusChanged(accountId, mailboxId);
412        }
413
414
415        /**
416         * Send message progress callback.
417         *
418         * This callback is overly overloaded:
419         *
420         * First, we get this.
421         *  result == null, messageId == -1, progress == 0:     start batch send
422         *
423         * Then we get these callbacks per message.
424         * (Exchange backend may skip "start sending one message".)
425         *  result == null, messageId == xx, progress == 0:     start sending one message
426         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
427         *
428         * Finally we get this.
429         *  result == null, messageId == -1, progres == 100;    finish sending batch
430         *
431         * So, let's just report the first exception we get, and ignore the rest.
432         */
433        @Override
434        public void sendMailCallback(MessagingException exception, long accountId, long messageId,
435                int progress) {
436            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
437                Log.d(Email.LOG_TAG, "sendMailCallback " + accountId + ", "
438                        + messageId + ", " + progress + ", " + exceptionToString(exception));
439            }
440            if (progress == 0 && messageId == -1) {
441                mSendMailExceptionReported = false;
442            }
443            if (messageId == -1) {
444                // Update the status only for the batch start/end.
445                // (i.e. don't report for each message.)
446                mOutboxStatus.get(accountId).onCallback(exception, progress, mClock);
447                notifyRefreshStatusChanged(accountId, -1);
448            }
449            if (exception != null && !mSendMailExceptionReported) {
450                // Only the first error in a batch will be reported.
451                mSendMailExceptionReported = true;
452                reportError(accountId, messageId, exception.getUiErrorMessage(mContext));
453            }
454        }
455    }
456}
457