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