RefreshManager.java revision 9227dbbf0f1c467762c44119d7cb1140c7191a88
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 android.content.Context;
20import android.os.AsyncTask;
21import android.os.Handler;
22import android.util.Log;
23
24import com.android.emailcommon.Logging;
25import com.android.emailcommon.mail.MessagingException;
26import com.android.emailcommon.utility.Utility;
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            // NOTE: For now, we're always allowing refresh (during service refactor)
111            return mIsRefreshRequested || mIsRefreshing;
112        }
113
114        public boolean canRefresh() {
115            // NOTE: For now, we're always allowing refresh (during service refactor)
116            return !isRefreshing();
117        }
118
119        public void onRefreshRequested() {
120            mIsRefreshRequested = true;
121        }
122
123        public long getLastRefreshTime() {
124            return mLastRefreshTime;
125        }
126
127        public void onCallback(MessagingException exception, int progress, Clock clock) {
128            if (exception == null && progress == 0) {
129                // Refresh started
130                mIsRefreshing = true;
131            } else if (exception != null || progress == 100) {
132                // Refresh finished
133                mIsRefreshing = false;
134                mIsRefreshRequested = false;
135                mLastRefreshTime = clock.getTime();
136            }
137        }
138    }
139
140    /**
141     * Map of accounts/mailboxes to {@link Status}.
142     */
143    private static class RefreshStatusMap {
144        private final HashMap<Long, Status> mMap = new HashMap<Long, Status>();
145
146        public Status get(long id) {
147            Status s = mMap.get(id);
148            if (s == null) {
149                s = new Status();
150                mMap.put(id, s);
151            }
152            return s;
153        }
154
155        public boolean isRefreshingAny() {
156            for (Status s : mMap.values()) {
157                if (s.isRefreshing()) {
158                    return true;
159                }
160            }
161            return false;
162        }
163    }
164
165    private final RefreshStatusMap mMailboxListStatus = new RefreshStatusMap();
166    private final RefreshStatusMap mMessageListStatus = new RefreshStatusMap();
167
168    /**
169     * @return the singleton instance.
170     */
171    public static synchronized RefreshManager getInstance(Context context) {
172        if (sInstance == null) {
173            sInstance = new RefreshManager(context, Controller.getInstance(context),
174                    Clock.INSTANCE, new Handler());
175        }
176        return sInstance;
177    }
178
179    protected RefreshManager(Context context, Controller controller, Clock clock,
180            Handler handler) {
181        mClock = clock;
182        mContext = context.getApplicationContext();
183        mController = controller;
184        mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(
185                handler, new ControllerResult());
186        mController.addResultCallback(mControllerResult);
187    }
188
189    /**
190     * MUST be called for mock instances.  (The actual instance is a singleton, so no cleanup
191     * is necessary.)
192     */
193    public void cleanUpForTest() {
194        mController.removeResultCallback(mControllerResult);
195    }
196
197    public void registerListener(Listener listener) {
198        if (listener == null) {
199            throw new IllegalArgumentException();
200        }
201        mListeners.add(listener);
202    }
203
204    public void unregisterListener(Listener listener) {
205        if (listener == null) {
206            throw new IllegalArgumentException();
207        }
208        mListeners.remove(listener);
209    }
210
211    /**
212     * Refresh the mailbox list of an account.
213     */
214    public boolean refreshMailboxList(long accountId) {
215        final Status status = mMailboxListStatus.get(accountId);
216        if (!status.canRefresh()) return false;
217
218        if (LOG_ENABLED) {
219            Log.d(Logging.LOG_TAG, "refreshMailboxList " + accountId);
220        }
221        status.onRefreshRequested();
222        notifyRefreshStatusChanged(accountId, -1);
223        mController.updateMailboxList(accountId);
224        return true;
225    }
226
227    public boolean isMailboxStale(long mailboxId) {
228        return mClock.getTime() >= (mMessageListStatus.get(mailboxId).getLastRefreshTime()
229                + MAILBOX_AUTO_REFRESH_INTERVAL);
230    }
231
232    public boolean isMailboxListStale(long accountId) {
233        return mClock.getTime() >= (mMailboxListStatus.get(accountId).getLastRefreshTime()
234                + MAILBOX_LIST_AUTO_REFRESH_INTERVAL);
235    }
236
237    /**
238     * Refresh messages in a mailbox.
239     */
240    public boolean refreshMessageList(long accountId, long mailboxId, boolean userRequest) {
241        return refreshMessageList(accountId, mailboxId, false, userRequest);
242    }
243
244    /**
245     * "load more messages" in a mailbox.
246     */
247    public boolean loadMoreMessages(long accountId, long mailboxId) {
248        return refreshMessageList(accountId, mailboxId, true, true);
249    }
250
251    private boolean refreshMessageList(long accountId, long mailboxId, boolean loadMoreMessages,
252            boolean userRequest) {
253        final Status status = mMessageListStatus.get(mailboxId);
254        if (!status.canRefresh()) return false;
255
256        if (LOG_ENABLED) {
257            Log.d(Logging.LOG_TAG, "refreshMessageList " + accountId + ", " + mailboxId + ", "
258                    + loadMoreMessages);
259        }
260        status.onRefreshRequested();
261        notifyRefreshStatusChanged(accountId, mailboxId);
262        if (loadMoreMessages) {
263            mController.loadMoreMessages(mailboxId);
264        } else {
265            mController.updateMailbox(accountId, mailboxId, userRequest);
266        }
267        return true;
268    }
269
270    /**
271     * Send pending messages.
272     */
273    public boolean sendPendingMessages(long accountId) {
274        if (LOG_ENABLED) {
275            Log.d(Logging.LOG_TAG, "sendPendingMessages " + accountId);
276        }
277        notifyRefreshStatusChanged(accountId, -1);
278        mController.sendPendingMessages(accountId);
279        return true;
280    }
281
282    /**
283     * Call {@link #sendPendingMessages} for all accounts.
284     */
285    public void sendPendingMessagesForAllAccounts() {
286        if (LOG_ENABLED) {
287            Log.d(Logging.LOG_TAG, "sendPendingMessagesForAllAccounts");
288        }
289        new SendPendingMessagesForAllAccountsImpl()
290                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
291    }
292
293    private class SendPendingMessagesForAllAccountsImpl extends Utility.ForEachAccount {
294        public SendPendingMessagesForAllAccountsImpl() {
295            super(mContext);
296        }
297
298        @Override
299        protected void performAction(long accountId) {
300            sendPendingMessages(accountId);
301        }
302    }
303
304    public long getLastMailboxListRefreshTime(long accountId) {
305        return mMailboxListStatus.get(accountId).getLastRefreshTime();
306    }
307
308    public long getLastMessageListRefreshTime(long mailboxId) {
309        return mMessageListStatus.get(mailboxId).getLastRefreshTime();
310    }
311
312    public boolean isMailboxListRefreshing(long accountId) {
313        return mMailboxListStatus.get(accountId).isRefreshing();
314    }
315
316    public boolean isMessageListRefreshing(long mailboxId) {
317        return mMessageListStatus.get(mailboxId).isRefreshing();
318    }
319
320    public boolean isRefreshingAnyMailboxListForTest() {
321        return mMailboxListStatus.isRefreshingAny();
322    }
323
324    public boolean isRefreshingAnyMessageListForTest() {
325        return mMessageListStatus.isRefreshingAny();
326    }
327
328    public String getErrorMessage() {
329        return mErrorMessage;
330    }
331
332    private void notifyRefreshStatusChanged(long accountId, long mailboxId) {
333        for (Listener l : mListeners) {
334            l.onRefreshStatusChanged(accountId, mailboxId);
335        }
336    }
337
338    private void reportError(long accountId, long mailboxId, String errorMessage) {
339        mErrorMessage = errorMessage;
340        for (Listener l : mListeners) {
341            l.onMessagingError(accountId, mailboxId, mErrorMessage);
342        }
343    }
344
345    /* package */ Collection<Listener> getListenersForTest() {
346        return mListeners;
347    }
348
349    /* package */ Status getMailboxListStatusForTest(long accountId) {
350        return mMailboxListStatus.get(accountId);
351    }
352
353    /* package */ Status getMessageListStatusForTest(long mailboxId) {
354        return mMessageListStatus.get(mailboxId);
355    }
356
357    private class ControllerResult extends Controller.Result {
358
359        private String exceptionToString(MessagingException exception) {
360            if (exception == null) {
361                return "(no exception)";
362            } else {
363                return MessagingExceptionStrings.getErrorString(mContext, exception);
364            }
365        }
366
367        /**
368         * Callback for mailbox list refresh.
369         */
370        @Override
371        public void updateMailboxListCallback(MessagingException exception, long accountId,
372                int progress) {
373            if (LOG_ENABLED) {
374                Log.d(Logging.LOG_TAG, "updateMailboxListCallback " + accountId + ", " + progress
375                        + ", " + exceptionToString(exception));
376            }
377            mMailboxListStatus.get(accountId).onCallback(exception, progress, mClock);
378            if (exception != null) {
379                reportError(accountId, -1,
380                        MessagingExceptionStrings.getErrorString(mContext, exception));
381            }
382            notifyRefreshStatusChanged(accountId, -1);
383        }
384
385        /**
386         * Callback for explicit (user-driven) mailbox refresh.
387         */
388        @Override
389        public void updateMailboxCallback(MessagingException exception, long accountId,
390                long mailboxId, int progress, int dontUseNumNewMessages,
391                ArrayList<Long> addedMessages) {
392            if (LOG_ENABLED) {
393                Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", "
394                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
395            }
396            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
397        }
398
399        /**
400         * Callback for implicit (timer-based) mailbox refresh.
401         *
402         * Do the same as {@link #updateMailboxCallback}.
403         * TODO: Figure out if it's really okay to do the same as updateMailboxCallback.
404         * If both the explicit refresh and the implicit refresh can run at the same time,
405         * we need to keep track of their status separately.
406         */
407        @Override
408        public void serviceCheckMailCallback(
409                MessagingException exception, long accountId, long mailboxId, int progress,
410                long tag) {
411            if (LOG_ENABLED) {
412                Log.d(Logging.LOG_TAG, "serviceCheckMailCallback " + accountId + ", "
413                        + mailboxId + ", " + progress + ", " + exceptionToString(exception));
414            }
415            updateMailboxCallbackInternal(exception, accountId, mailboxId, progress, 0);
416        }
417
418        private void updateMailboxCallbackInternal(MessagingException exception, long accountId,
419                long mailboxId, int progress, int dontUseNumNewMessages) {
420            // Don't use dontUseNumNewMessages.  serviceCheckMailCallback() don't set it.
421            mMessageListStatus.get(mailboxId).onCallback(exception, progress, mClock);
422            if (exception != null) {
423                reportError(accountId, mailboxId,
424                        MessagingExceptionStrings.getErrorString(mContext, exception));
425            }
426            notifyRefreshStatusChanged(accountId, mailboxId);
427        }
428    }
429}
430