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