RefreshManager.java revision 0e6d972641c19467d6b26351ce14a3f44c9fd6f4
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 method musb 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(),
155                    Clock.INSTANCE, new Handler());
156        }
157        return sInstance;
158    }
159
160    /* package */ 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    public void registerListener(Listener listener) {
171        if (listener == null) {
172            throw new InvalidParameterException();
173        }
174        mListeners.add(listener);
175    }
176
177    public void unregisterListener(Listener listener) {
178        if (listener == null) {
179            throw new InvalidParameterException();
180        }
181        mListeners.remove(listener);
182    }
183
184    /**
185     * Refresh the mailbox list of an account.
186     */
187    public boolean refreshMailboxList(long accountId) {
188        final Status status = mMailboxListStatus.get(accountId);
189        if (!status.canRefresh()) return false;
190
191        Log.i(Email.LOG_TAG, "refreshMailboxList " + accountId);
192        status.onRefreshRequested();
193        notifyRefreshStatusChanged(accountId, -1);
194        mController.updateMailboxList(accountId);
195        return true;
196    }
197
198    public boolean isMailboxStale(long mailboxId) {
199        return mClock.getTime() >= (mMessageListStatus.get(mailboxId).getLastRefreshTime()
200                + MAILBOX_AUTO_REFRESH_INTERVAL);
201    }
202
203    /**
204     * Refresh messages in a mailbox.
205     */
206    public boolean refreshMessageList(long accountId, long mailboxId) {
207        return refreshMessageList(accountId, mailboxId, false);
208    }
209
210    /**
211     * "load more messages" in a mailbox.
212     */
213    public boolean loadMoreMessages(long accountId, long mailboxId) {
214        return refreshMessageList(accountId, mailboxId, true);
215    }
216
217    private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages) {
218        final Status status = mMessageListStatus.get(mailboxId);
219        if (!status.canRefresh()) return false;
220
221        Log.i(Email.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", "
222                + loadMoreMessages);
223        status.onRefreshRequested();
224        notifyRefreshStatusChanged(accountId, mailboxId);
225        mController.updateMailbox(accountId, mailboxId);
226        return true;
227    }
228
229    /**
230     * Send pending messages.
231     */
232    public boolean sendPendingMessages(long accountId) {
233        final Status status = mOutboxStatus.get(accountId);
234        if (!status.canRefresh()) return false;
235
236        Log.i(Email.LOG_TAG, "sendPendingMessages " + accountId);
237        status.onRefreshRequested();
238        notifyRefreshStatusChanged(accountId, -1);
239        mController.sendPendingMessages(accountId);
240        return true;
241    }
242
243    /**
244     * Call {@link #sendPendingMessages} for all accounts.
245     */
246    public void sendPendingMessagesForAllAccounts() {
247        Log.i(Email.LOG_TAG, "sendPendingMessagesForAllAccounts");
248        new SendPendingMessagesForAllAccountsImpl().execute();
249    }
250
251    private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount {
252        public SendPendingMessagesForAllAccountsImpl() {
253            super(mContext);
254        }
255
256        @Override
257        protected void performAction(long accountId) {
258            sendPendingMessages(accountId);
259        }
260    }
261
262    public boolean isMailboxListRefreshing(long accountId) {
263        return mMailboxListStatus.get(accountId).isRefreshing();
264    }
265
266    public boolean isMessageListRefreshing(long mailboxId) {
267        return mMessageListStatus.get(mailboxId).isRefreshing();
268    }
269
270    public boolean isSendingMessage(long accountId) {
271        return mOutboxStatus.get(accountId).isRefreshing();
272    }
273
274    public boolean isRefreshingAnyMailboxList() {
275        return mMailboxListStatus.isRefreshingAny();
276    }
277
278    public boolean isRefreshingAnyMessageList() {
279        return mMessageListStatus.isRefreshingAny();
280    }
281
282    public boolean isSendingAnyMessage() {
283        return mOutboxStatus.isRefreshingAny();
284    }
285
286    public boolean isRefreshingOrSendingAny() {
287        return isRefreshingAnyMailboxList() || isRefreshingAnyMessageList()
288                || isSendingAnyMessage();
289    }
290
291    public String getErrorMessage() {
292        return mErrorMessage;
293    }
294
295    private void notifyRefreshStatusChanged(long accountId, long mailboxId) {
296        for (Listener l : mListeners) {
297            l.onRefreshStatusChanged(accountId, mailboxId);
298        }
299    }
300
301    private void reportError(long accountId, long mailboxId, String errorMessage) {
302        mErrorMessage = errorMessage;
303        for (Listener l : mListeners) {
304            l.onMessagingError(accountId, mailboxId, mErrorMessage);
305        }
306    }
307
308    /* package */ Collection<Listener> getListenersForTest() {
309        return mListeners;
310    }
311
312    /* package */ Status getMailboxListStatusForTest(long accountId) {
313        return mMailboxListStatus.get(accountId);
314    }
315
316    /* package */ Status getMessageListStatusForTest(long mailboxId) {
317        return mMessageListStatus.get(mailboxId);
318    }
319
320    /* package */ Status getOutboxStatusForTest(long acountId) {
321        return mOutboxStatus.get(acountId);
322    }
323
324    private class ControllerResult extends Controller.Result {
325        private boolean mSendMailExceptionReported = false;
326
327        private String exceptionToString(MessagingException exception) {
328            if (exception == null) {
329                return "(no exception)";
330            } else {
331                return exception.getUiErrorMessage(mContext);
332            }
333        }
334
335        /**
336         * Callback for mailbox list refresh.
337         */
338        @Override
339        public void updateMailboxListCallback(MessagingException exception, long accountId,
340                int progress) {
341            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
342                Log.d(Email.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress
343                        + ", " + exceptionToString(exception));
344            }
345            mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock);
346            if (exception != null) {
347                reportError(accountId, -1, exception.getUiErrorMessage(mContext));
348            }
349            notifyRefreshStatusChanged(accountId, -1);
350        }
351
352        /**
353         * Callback for explicit (user-driven) mailbox refresh.
354         */
355        @Override
356        public void updateMailboxCallback(MessagingException exception, long accountId,
357                long mailboxId, int progress, int dontUseNumNewMessages) {
358            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
359                Log.d(Email.LOG_TAG, "updateMailboxCallback " + accountId + ", "
360                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
361            }
362            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
363        }
364
365        /**
366         * Callback for implicit (timer-based) mailbox refresh.
367         *
368         * Do the same as {@link #updateMailboxCallback}.
369         * TODO: Figure out if it's really okay to do the same as updateMailboxCallback.
370         * If both the explicit refresh and the implicit refresh can run at the same time,
371         * we need to keep track of their status separately.
372         */
373        @Override
374        public void serviceCheckMailCallback(
375                MessagingException exception, long accountId, long mailboxId, int progress,
376                long tag) {
377            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
378                Log.d(Email.LOG_TAG, "serviceCheckMailCallback " + accountId + ", "
379                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
380            }
381            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
382        }
383
384        private void updateMailboxCallbackInternal(MessagingException exception, long accountId,
385                long mailboxId, int progress, int dontUseNumNewMessages) {
386            // Don't use dontUseNumNewMessages.  serviceCheckMailCallback() don't set it.
387            mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock);
388            if (exception != null) {
389                reportError(accountId, mailboxId, exception.getUiErrorMessage(mContext));
390            }
391            notifyRefreshStatusChanged(accountId, mailboxId);
392        }
393
394
395        /**
396         * Send message progress callback.
397         *
398         * This callback is overly overloaded:
399         *
400         * First, we get this.
401         *  result == null, messageId == -1, progress == 0:     start batch send
402         *
403         * Then we get these callbacks per message.
404         * (Exchange backend may skip "start sending one message".)
405         *  result == null, messageId == xx, progress == 0:     start sending one message
406         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
407         *
408         * Finally we get this.
409         *  result == null, messageId == -1, progres == 100;    finish sending batch
410         *
411         * So, let's just report the first exception we get, and ignore the rest.
412         */
413        @Override
414        public void sendMailCallback(MessagingException exception, long accountId, long messageId,
415                int progress) {
416            if (Email.DEBUG && DEBUG_CALLBACK_LOG) {
417                Log.d(Email.LOG_TAG, "sendMailCallback " + accountId + ", "
418                        + messageId + ", " + progress + ", " + exceptionToString(exception));
419            }
420            if (progress == 0 && messageId == -1) {
421                mSendMailExceptionReported = false;
422            }
423            if (messageId == -1) {
424                // Update the status only for the batch start/end.
425                // (i.e. don't report for each message.)
426                mOutboxStatus.get(accountId).onCallback(exception, progress, mClock);
427                notifyRefreshStatusChanged(accountId, -1);
428            }
429            if (exception != null && !mSendMailExceptionReported) {
430                // Only the first error in a batch will be reported.
431                mSendMailExceptionReported = true;
432                reportError(accountId, messageId, exception.getUiErrorMessage(mContext));
433            }
434        }
435    }
436}
437