Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2008 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.service;
     18 
     19 import com.android.email.AccountBackupRestore;
     20 import com.android.email.Controller;
     21 import com.android.email.Email;
     22 import com.android.email.R;
     23 import com.android.email.activity.MessageList;
     24 import com.android.email.mail.MessagingException;
     25 import com.android.email.provider.EmailContent.Account;
     26 import com.android.email.provider.EmailContent.AccountColumns;
     27 import com.android.email.provider.EmailContent.HostAuth;
     28 import com.android.email.provider.EmailContent.Mailbox;
     29 
     30 import android.app.AlarmManager;
     31 import android.app.Notification;
     32 import android.app.NotificationManager;
     33 import android.app.PendingIntent;
     34 import android.app.Service;
     35 import android.content.ContentUris;
     36 import android.content.ContentValues;
     37 import android.content.Context;
     38 import android.content.Intent;
     39 import android.database.Cursor;
     40 import android.media.AudioManager;
     41 import android.net.Uri;
     42 import android.os.IBinder;
     43 import android.os.SystemClock;
     44 import android.util.Config;
     45 import android.util.Log;
     46 
     47 import java.util.HashMap;
     48 
     49 /**
     50  * Background service for refreshing non-push email accounts.
     51  */
     52 public class MailService extends Service {
     53     /** DO NOT CHECK IN "TRUE" */
     54     private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;     // force 1-minute refresh
     55 
     56     private static final String LOG_TAG = "Email-MailService";
     57 
     58     public static final int NOTIFICATION_ID_NEW_MESSAGES = 1;
     59     public static final int NOTIFICATION_ID_SECURITY_NEEDED = 2;
     60     public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 3;
     61 
     62     private static final String ACTION_CHECK_MAIL =
     63         "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
     64     private static final String ACTION_RESCHEDULE =
     65         "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
     66     private static final String ACTION_CANCEL =
     67         "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
     68     private static final String ACTION_NOTIFY_MAIL =
     69         "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
     70 
     71     private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
     72     private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
     73     private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
     74 
     75     private static final int WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
     76 
     77     private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
     78         new String[] {AccountColumns.NEW_MESSAGE_COUNT};
     79 
     80     private final Controller.Result mControllerCallback = new ControllerResults();
     81 
     82     private int mStartId;
     83 
     84     /**
     85      * Access must be synchronized, because there are accesses from the Controller callback
     86      */
     87     private static HashMap<Long,AccountSyncReport> mSyncReports =
     88         new HashMap<Long,AccountSyncReport>();
     89 
     90     /**
     91      * Simple template used for clearing new message count in accounts
     92      */
     93     private static final ContentValues CLEAR_NEW_MESSAGES;
     94     static {
     95         CLEAR_NEW_MESSAGES = new ContentValues();
     96         CLEAR_NEW_MESSAGES.put(Account.NEW_MESSAGE_COUNT, 0);
     97     }
     98 
     99     public static void actionReschedule(Context context) {
    100         Intent i = new Intent();
    101         i.setClass(context, MailService.class);
    102         i.setAction(MailService.ACTION_RESCHEDULE);
    103         context.startService(i);
    104     }
    105 
    106     public static void actionCancel(Context context)  {
    107         Intent i = new Intent();
    108         i.setClass(context, MailService.class);
    109         i.setAction(MailService.ACTION_CANCEL);
    110         context.startService(i);
    111     }
    112 
    113     /**
    114      * Reset new message counts for one or all accounts.  This clears both our local copy and
    115      * the values (if any) stored in the account records.
    116      *
    117      * @param accountId account to clear, or -1 for all accounts
    118      */
    119     public static void resetNewMessageCount(Context context, long accountId) {
    120         synchronized (mSyncReports) {
    121             for (AccountSyncReport report : mSyncReports.values()) {
    122                 if (accountId == -1 || accountId == report.accountId) {
    123                     report.numNewMessages = 0;
    124                 }
    125             }
    126         }
    127         // now do the database - all accounts, or just one of them
    128         Uri uri;
    129         if (accountId == -1) {
    130             uri = Account.CONTENT_URI;
    131         } else {
    132             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
    133         }
    134         context.getContentResolver().update(uri, CLEAR_NEW_MESSAGES, null, null);
    135     }
    136 
    137     /**
    138      * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
    139      * messages.  This assumes that the push provider has already synced the messages into the
    140      * appropriate database - this simply triggers the notification mechanism.
    141      *
    142      * @param context a context
    143      * @param accountId the id of the account that is reporting new messages
    144      * @param newCount the number of new messages
    145      */
    146     public static void actionNotifyNewMessages(Context context, long accountId) {
    147         Intent i = new Intent(ACTION_NOTIFY_MAIL);
    148         i.setClass(context, MailService.class);
    149         i.putExtra(EXTRA_CHECK_ACCOUNT, accountId);
    150         context.startService(i);
    151     }
    152 
    153     @Override
    154     public int onStartCommand(Intent intent, int flags, int startId) {
    155         super.onStartCommand(intent, flags, startId);
    156 
    157         // Restore accounts, if it has not happened already
    158         AccountBackupRestore.restoreAccountsIfNeeded(this);
    159 
    160         // TODO this needs to be passed through the controller and back to us
    161         this.mStartId = startId;
    162         String action = intent.getAction();
    163 
    164         Controller controller = Controller.getInstance(getApplication());
    165         controller.addResultCallback(mControllerCallback);
    166 
    167         if (ACTION_CHECK_MAIL.equals(action)) {
    168             // If we have the data, restore the last-sync-times for each account
    169             // These are cached in the wakeup intent in case the process was killed.
    170             restoreSyncReports(intent);
    171 
    172             // Sync a specific account if given
    173             AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    174             long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
    175             if (Config.LOGD && Email.DEBUG) {
    176                 Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId);
    177             }
    178             if (checkAccountId >= 0) {
    179                 setWatchdog(checkAccountId, alarmManager);
    180             }
    181             // if no account given, or the given account cannot be synced - reschedule
    182             if (checkAccountId == -1 || !syncOneAccount(controller, checkAccountId, startId)) {
    183                 // Prevent runaway on the current account by pretending it updated
    184                 if (checkAccountId != -1) {
    185                     updateAccountReport(checkAccountId, 0);
    186                 }
    187                 // Find next account to sync, and reschedule
    188                 reschedule(alarmManager);
    189                 stopSelf(startId);
    190             }
    191         }
    192         else if (ACTION_CANCEL.equals(action)) {
    193             if (Config.LOGD && Email.DEBUG) {
    194                 Log.d(LOG_TAG, "action: cancel");
    195             }
    196             cancel();
    197             stopSelf(startId);
    198         }
    199         else if (ACTION_RESCHEDULE.equals(action)) {
    200             if (Config.LOGD && Email.DEBUG) {
    201                 Log.d(LOG_TAG, "action: reschedule");
    202             }
    203             // As a precaution, clear any outstanding Email notifications
    204             // We could be smarter and only do this when the list of accounts changes,
    205             // but that's an edge condition and this is much safer.
    206             NotificationManager notificationManager =
    207                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    208             notificationManager.cancel(NOTIFICATION_ID_NEW_MESSAGES);
    209 
    210             // When called externally, we refresh the sync reports table to pick up
    211             // any changes in the account list or account settings
    212             refreshSyncReports();
    213             // Finally, scan for the next needing update, and set an alarm for it
    214             AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    215             reschedule(alarmManager);
    216             stopSelf(startId);
    217         } else if (ACTION_NOTIFY_MAIL.equals(action)) {
    218             long accountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
    219             // Get the current new message count
    220             Cursor c = getContentResolver().query(
    221                     ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
    222                     NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
    223             int newMessageCount = 0;
    224             try {
    225                 if (c.moveToFirst()) {
    226                     newMessageCount = c.getInt(0);
    227                 } else {
    228                     // If the account no longer exists, set to -1 (which is handled below)
    229                     accountId = -1;
    230                 }
    231             } finally {
    232                 c.close();
    233             }
    234             if (Config.LOGD && Email.DEBUG) {
    235                 Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
    236                         + " count=" + newMessageCount);
    237             }
    238             if (accountId != -1) {
    239                 updateAccountReport(accountId, newMessageCount);
    240                 notifyNewMessages(accountId);
    241             }
    242             stopSelf(startId);
    243         }
    244 
    245         // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
    246         // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
    247         // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
    248         // will fire and get things running again.
    249         return START_NOT_STICKY;
    250     }
    251 
    252     @Override
    253     public IBinder onBind(Intent intent) {
    254         return null;
    255     }
    256 
    257     @Override
    258     public void onDestroy() {
    259         super.onDestroy();
    260         Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
    261     }
    262 
    263     private void cancel() {
    264         AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    265         PendingIntent pi = createAlarmIntent(-1, null, false);
    266         alarmMgr.cancel(pi);
    267     }
    268 
    269     /**
    270      * Refresh the sync reports, to pick up any changes in the account list or account settings.
    271      */
    272     private void refreshSyncReports() {
    273         synchronized (mSyncReports) {
    274             // Make shallow copy of sync reports so we can recover the prev sync times
    275             HashMap<Long,AccountSyncReport> oldSyncReports =
    276                 new HashMap<Long,AccountSyncReport>(mSyncReports);
    277 
    278             // Delete the sync reports to force a refresh from live account db data
    279             mSyncReports.clear();
    280             setupSyncReportsLocked(-1);
    281 
    282             // Restore prev-sync & next-sync times for any reports in the new list
    283             for (AccountSyncReport newReport : mSyncReports.values()) {
    284                 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
    285                 if (oldReport != null) {
    286                     newReport.prevSyncTime = oldReport.prevSyncTime;
    287                     if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
    288                         newReport.nextSyncTime =
    289                             newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
    290                     }
    291                 }
    292             }
    293         }
    294     }
    295 
    296     /**
    297      * Create and send an alarm with the entire list.  This also sends a list of known last-sync
    298      * times with the alarm, so if we are killed between alarms, we don't lose this info.
    299      *
    300      * @param alarmMgr passed in so we can mock for testing.
    301      */
    302     /* package */ void reschedule(AlarmManager alarmMgr) {
    303         // restore the reports if lost
    304         setupSyncReports(-1);
    305         synchronized (mSyncReports) {
    306             int numAccounts = mSyncReports.size();
    307             long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
    308             int accountInfoIndex = 0;
    309 
    310             long nextCheckTime = Long.MAX_VALUE;
    311             AccountSyncReport nextAccount = null;
    312             long timeNow = SystemClock.elapsedRealtime();
    313 
    314             for (AccountSyncReport report : mSyncReports.values()) {
    315                 if (report.syncInterval <= 0) {                         // no timed checks - skip
    316                     continue;
    317                 }
    318                 if ("eas".equals(report.protocol)) {                    // no checks for eas accts
    319                     continue;
    320                 }
    321                 long prevSyncTime = report.prevSyncTime;
    322                 long nextSyncTime = report.nextSyncTime;
    323 
    324                 // select next account to sync
    325                 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
    326                     nextCheckTime = 0;
    327                     nextAccount = report;
    328                 } else if (nextSyncTime < nextCheckTime) {              // next to be checked
    329                     nextCheckTime = nextSyncTime;
    330                     nextAccount = report;
    331                 }
    332                 // collect last-sync-times for all accounts
    333                 // this is using pairs of {long,long} to simplify passing in a bundle
    334                 accountInfo[accountInfoIndex++] = report.accountId;
    335                 accountInfo[accountInfoIndex++] = report.prevSyncTime;
    336             }
    337 
    338             // Clear out any unused elements in the array
    339             while (accountInfoIndex < accountInfo.length) {
    340                 accountInfo[accountInfoIndex++] = -1;
    341             }
    342 
    343             // set/clear alarm as needed
    344             long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
    345             PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
    346 
    347             if (nextAccount == null) {
    348                 alarmMgr.cancel(pi);
    349                 if (Config.LOGD && Email.DEBUG) {
    350                     Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
    351                 }
    352             } else {
    353                 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
    354                 if (Config.LOGD && Email.DEBUG) {
    355                     Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
    356                             + " for " + nextAccount);
    357                 }
    358             }
    359         }
    360     }
    361 
    362     /**
    363      * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
    364      * killed by the system due to memory pressure.)  Normally, a mail check will complete and
    365      * the watchdog will be replaced by the call to reschedule().
    366      * @param accountId the account we were trying to check
    367      * @param alarmMgr system alarm manager
    368      */
    369     private void setWatchdog(long accountId, AlarmManager alarmMgr) {
    370         PendingIntent pi = createAlarmIntent(accountId, null, true);
    371         long timeNow = SystemClock.elapsedRealtime();
    372         long nextCheckTime = timeNow + WATCHDOG_DELAY;
    373         alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
    374     }
    375 
    376     /**
    377      * Return a pending intent for use by this alarm.  Most of the fields must be the same
    378      * (in order for the intent to be recognized by the alarm manager) but the extras can
    379      * be different, and are passed in here as parameters.
    380      */
    381     /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
    382             boolean isWatchdog) {
    383         Intent i = new Intent();
    384         i.setClass(this, MailService.class);
    385         i.setAction(ACTION_CHECK_MAIL);
    386         i.putExtra(EXTRA_CHECK_ACCOUNT, checkId);
    387         i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
    388         if (isWatchdog) {
    389             i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
    390         }
    391         PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
    392         return pi;
    393     }
    394 
    395     /**
    396      * Start a controller sync for a specific account
    397      *
    398      * @param controller The controller to do the sync work
    399      * @param checkAccountId the account Id to try and check
    400      * @param startId the id of this service launch
    401      * @return true if mail checking has started, false if it could not (e.g. bad account id)
    402      */
    403     private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
    404         long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
    405         if (inboxId == Mailbox.NO_MAILBOX) {
    406             return false;
    407         } else {
    408             controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback);
    409             return true;
    410         }
    411     }
    412 
    413     /**
    414      * Note:  Times are relative to SystemClock.elapsedRealtime()
    415      */
    416     private static class AccountSyncReport {
    417         long accountId;
    418         String protocol;
    419         long prevSyncTime;      // 0 == unknown
    420         long nextSyncTime;      // 0 == ASAP  -1 == don't sync
    421         int numNewMessages;
    422 
    423         int syncInterval;
    424         boolean notify;
    425         boolean vibrate;
    426         boolean vibrateWhenSilent;
    427         Uri ringtoneUri;
    428 
    429         String displayName;     // temporary, for debug logging
    430 
    431 
    432         @Override
    433         public String toString() {
    434             return displayName + ": id=" + accountId + " prevSync=" + prevSyncTime
    435                     + " nextSync=" + nextSyncTime + " numNew=" + numNewMessages;
    436         }
    437     }
    438 
    439     /**
    440      * scan accounts to create a list of { acct, prev sync, next sync, #new }
    441      * use this to create a fresh copy.  assumes all accounts need sync
    442      *
    443      * @param accountId -1 will rebuild the list if empty.  other values will force loading
    444      *   of a single account (e.g if it was created after the original list population)
    445      */
    446     /* package */ void setupSyncReports(long accountId) {
    447         synchronized (mSyncReports) {
    448             setupSyncReportsLocked(accountId);
    449         }
    450     }
    451 
    452     /**
    453      * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
    454      */
    455     private void setupSyncReportsLocked(long accountId) {
    456         if (accountId == -1) {
    457             // -1 == reload the list if empty, otherwise exit immediately
    458             if (mSyncReports.size() > 0) {
    459                 return;
    460             }
    461         } else {
    462             // load a single account if it doesn't already have a sync record
    463             if (mSyncReports.containsKey(accountId)) {
    464                 return;
    465             }
    466         }
    467 
    468         // setup to add a single account or all accounts
    469         Uri uri;
    470         if (accountId == -1) {
    471             uri = Account.CONTENT_URI;
    472         } else {
    473             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
    474         }
    475 
    476         // TODO use a narrower projection here
    477         Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION,
    478                 null, null, null);
    479         try {
    480             while (c.moveToNext()) {
    481                 AccountSyncReport report = new AccountSyncReport();
    482                 int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN);
    483                 int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
    484                 String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN);
    485 
    486                 // For debugging only
    487                 if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) {
    488                     syncInterval = 1;
    489                 }
    490 
    491                 long acctId = c.getLong(Account.CONTENT_ID_COLUMN);
    492                 Account account = Account.restoreAccountWithId(this, acctId);
    493                 if (account == null) continue;
    494                 HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv);
    495                 if (hostAuth == null) continue;
    496                 report.accountId = acctId;
    497                 report.protocol = hostAuth.mProtocol;
    498                 report.prevSyncTime = 0;
    499                 report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
    500                 report.numNewMessages = 0;
    501 
    502                 report.syncInterval = syncInterval;
    503                 report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
    504                 report.vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
    505                 report.vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
    506                 report.ringtoneUri = (ringtoneString == null) ? null
    507                         : Uri.parse(ringtoneString);
    508 
    509                 report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
    510 
    511                 // TODO lookup # new in inbox
    512                 mSyncReports.put(report.accountId, report);
    513             }
    514         } finally {
    515             c.close();
    516         }
    517     }
    518 
    519     /**
    520      * Update list with a single account's sync times and unread count
    521      *
    522      * @param accountId the account being updated
    523      * @param newCount the number of new messages, or -1 if not being reported (don't update)
    524      * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
    525      */
    526     /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
    527         // restore the reports if lost
    528         setupSyncReports(accountId);
    529         synchronized (mSyncReports) {
    530             AccountSyncReport report = mSyncReports.get(accountId);
    531             if (report == null) {
    532                 // discard result - there is no longer an account with this id
    533                 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
    534                 return null;
    535             }
    536 
    537             // report found - update it (note - editing the report while in-place in the hashmap)
    538             report.prevSyncTime = SystemClock.elapsedRealtime();
    539             if (report.syncInterval > 0) {
    540                 report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
    541             }
    542             if (newCount != -1) {
    543                 report.numNewMessages = newCount;
    544             }
    545             if (Config.LOGD && Email.DEBUG) {
    546                 Log.d(LOG_TAG, "update account " + report.toString());
    547             }
    548             return report;
    549         }
    550     }
    551 
    552     /**
    553      * when we receive an alarm, update the account sync reports list if necessary
    554      * this will be the case when if we have restarted the process and lost the data
    555      * in the global.
    556      *
    557      * @param restoreIntent the intent with the list
    558      */
    559     /* package */ void restoreSyncReports(Intent restoreIntent) {
    560         // restore the reports if lost
    561         setupSyncReports(-1);
    562         synchronized (mSyncReports) {
    563             long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
    564             if (accountInfo == null) {
    565                 Log.d(LOG_TAG, "no data in intent to restore");
    566                 return;
    567             }
    568             int accountInfoIndex = 0;
    569             int accountInfoLimit = accountInfo.length;
    570             while (accountInfoIndex < accountInfoLimit) {
    571                 long accountId = accountInfo[accountInfoIndex++];
    572                 long prevSync = accountInfo[accountInfoIndex++];
    573                 AccountSyncReport report = mSyncReports.get(accountId);
    574                 if (report != null) {
    575                     if (report.prevSyncTime == 0) {
    576                         report.prevSyncTime = prevSync;
    577                         if (report.syncInterval > 0 && report.prevSyncTime != 0) {
    578                             report.nextSyncTime =
    579                                 report.prevSyncTime + (report.syncInterval * 1000 * 60);
    580                         }
    581                     }
    582                 }
    583             }
    584         }
    585     }
    586 
    587     class ControllerResults implements Controller.Result {
    588 
    589         public void loadMessageForViewCallback(MessagingException result, long messageId,
    590                 int progress) {
    591         }
    592 
    593         public void loadAttachmentCallback(MessagingException result, long messageId,
    594                 long attachmentId, int progress) {
    595         }
    596 
    597         public void updateMailboxCallback(MessagingException result, long accountId,
    598                 long mailboxId, int progress, int numNewMessages) {
    599             if (result != null || progress == 100) {
    600                 // We only track the inbox here in the service - ignore other mailboxes
    601                 long inboxId = Mailbox.findMailboxOfType(MailService.this,
    602                         accountId, Mailbox.TYPE_INBOX);
    603                 if (mailboxId == inboxId) {
    604                     if (progress == 100) {
    605                         updateAccountReport(accountId, numNewMessages);
    606                         if (numNewMessages > 0) {
    607                             notifyNewMessages(accountId);
    608                         }
    609                     } else {
    610                         updateAccountReport(accountId, -1);
    611                     }
    612                 }
    613                 // Call the global refresh tracker for all mailboxes
    614                 Email.updateMailboxRefreshTime(mailboxId);
    615             }
    616         }
    617 
    618         public void updateMailboxListCallback(MessagingException result, long accountId,
    619                 int progress) {
    620         }
    621 
    622         public void serviceCheckMailCallback(MessagingException result, long accountId,
    623                 long mailboxId, int progress, long tag) {
    624             if (result != null || progress == 100) {
    625                 if (result != null) {
    626                     // the checkmail ended in an error.  force an update of the refresh
    627                     // time, so we don't just spin on this account
    628                     updateAccountReport(accountId, -1);
    629                 }
    630                 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    631                 reschedule(alarmManager);
    632                 int serviceId = MailService.this.mStartId;
    633                 if (tag != 0) {
    634                     serviceId = (int) tag;
    635                 }
    636                 stopSelf(serviceId);
    637             }
    638         }
    639 
    640         public void sendMailCallback(MessagingException result, long accountId, long messageId,
    641                 int progress) {
    642         }
    643     }
    644 
    645     /**
    646      * Prepare notifications for a given new account having received mail
    647      * The notification is organized around the account that has the new mail (e.g. selecting
    648      * the alert preferences) but the notification will include a summary if other
    649      * accounts also have new mail.
    650      */
    651     private void notifyNewMessages(long accountId) {
    652         boolean notify = false;
    653         boolean vibrate = false;
    654         boolean vibrateWhenSilent = false;
    655         Uri ringtone = null;
    656         int accountsWithNewMessages = 0;
    657         int numNewMessages = 0;
    658         String reportName = null;
    659         synchronized (mSyncReports) {
    660             for (AccountSyncReport report : mSyncReports.values()) {
    661                 if (report.numNewMessages == 0) {
    662                     continue;
    663                 }
    664                 numNewMessages += report.numNewMessages;
    665                 accountsWithNewMessages += 1;
    666                 if (report.accountId == accountId) {
    667                     notify = report.notify;
    668                     vibrate = report.vibrate;
    669                     vibrateWhenSilent = report.vibrateWhenSilent;
    670                     ringtone = report.ringtoneUri;
    671                     reportName = report.displayName;
    672                 }
    673             }
    674         }
    675         if (!notify) {
    676             return;
    677         }
    678 
    679         // set up to post a notification
    680         Intent intent;
    681         String reportString;
    682 
    683         if (accountsWithNewMessages == 1) {
    684             // Prepare a report for a single account
    685             // "12 unread (gmail)"
    686             reportString = getResources().getQuantityString(
    687                     R.plurals.notification_new_one_account_fmt, numNewMessages,
    688                     numNewMessages, reportName);
    689             intent = MessageList.createIntent(this, accountId, -1, Mailbox.TYPE_INBOX);
    690         } else {
    691             // Prepare a report for multiple accounts
    692             // "4 accounts"
    693             reportString = getResources().getQuantityString(
    694                     R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages,
    695                     accountsWithNewMessages);
    696             intent = MessageList.createIntent(this, -1, Mailbox.QUERY_ALL_INBOXES, -1);
    697         }
    698 
    699         // prepare appropriate pending intent, set up notification, and send
    700         PendingIntent pending =
    701             PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    702 
    703         Notification notification = new Notification(
    704                 R.drawable.stat_notify_email_generic,
    705                 getString(R.string.notification_new_title),
    706                 System.currentTimeMillis());
    707         notification.setLatestEventInfo(this,
    708                 getString(R.string.notification_new_title),
    709                 reportString,
    710                 pending);
    711 
    712         notification.sound = ringtone;
    713         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    714         boolean nowSilent = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
    715 
    716         // Use same code here as in Gmail and GTalk for vibration
    717         if (vibrate || (vibrateWhenSilent && nowSilent)) {
    718             notification.defaults |= Notification.DEFAULT_VIBRATE;
    719         }
    720 
    721         // This code is identical to that used by Gmail and GTalk for notifications
    722         notification.flags |= Notification.FLAG_SHOW_LIGHTS;
    723         notification.defaults |= Notification.DEFAULT_LIGHTS;
    724 
    725         NotificationManager notificationManager =
    726             (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    727         notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification);
    728     }
    729 }
    730