Home | History | Annotate | Download | only in email
      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 
     17 package com.android.email;
     18 
     19 import com.android.emailcommon.Logging;
     20 import com.android.emailcommon.mail.MessagingException;
     21 import com.android.emailcommon.utility.Utility;
     22 
     23 import android.content.Context;
     24 import android.os.AsyncTask;
     25 import android.os.Handler;
     26 import android.util.Log;
     27 
     28 import java.util.ArrayList;
     29 import java.util.Collection;
     30 import 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  */
     51 public 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