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