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 android.accounts.AccountManager;
     20 import android.accounts.AccountManagerCallback;
     21 import android.app.AlarmManager;
     22 import android.app.PendingIntent;
     23 import android.app.Service;
     24 import android.content.ContentResolver;
     25 import android.content.ContentUris;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.SyncStatusObserver;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.os.IBinder;
     33 import android.os.SystemClock;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 
     37 import com.android.email.Controller;
     38 import com.android.email.Email;
     39 import com.android.email.Preferences;
     40 import com.android.email.SingleRunningTask;
     41 import com.android.email.provider.AccountReconciler;
     42 import com.android.emailcommon.AccountManagerTypes;
     43 import com.android.emailcommon.mail.MessagingException;
     44 import com.android.emailcommon.provider.Account;
     45 import com.android.emailcommon.provider.EmailContent;
     46 import com.android.emailcommon.provider.HostAuth;
     47 import com.android.emailcommon.provider.Mailbox;
     48 import com.android.emailcommon.utility.EmailAsyncTask;
     49 import com.google.common.annotations.VisibleForTesting;
     50 
     51 import java.util.ArrayList;
     52 import java.util.HashMap;
     53 import java.util.List;
     54 
     55 /**
     56  * Background service for refreshing non-push email accounts.
     57  *
     58  * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid
     59  * possible problems with out-of-order startId processing.
     60  */
     61 public class MailService extends Service {
     62     private static final String LOG_TAG = "Email-MailService";
     63 
     64     private static final String ACTION_CHECK_MAIL =
     65         "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
     66     private static final String ACTION_RESCHEDULE =
     67         "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
     68     private static final String ACTION_CANCEL =
     69         "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
     70     private static final String ACTION_SEND_PENDING_MAIL =
     71         "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
     72     private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS =
     73         "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS";
     74 
     75     private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
     76     private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
     77     private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
     78 
     79     /** Time between watchdog checks; in milliseconds */
     80     private static final long WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
     81 
     82     /** Sentinel value asking to update mSyncReports if it's currently empty */
     83     @VisibleForTesting
     84     static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
     85     /** Sentinel value asking that mSyncReports be rebuilt */
     86     @VisibleForTesting
     87     static final int SYNC_REPORTS_RESET = -2;
     88 
     89     @VisibleForTesting
     90     Controller mController;
     91     private final Controller.Result mControllerCallback = new ControllerResults();
     92     private ContentResolver mContentResolver;
     93     private Context mContext;
     94 
     95     private int mStartId;
     96 
     97     /**
     98      * Access must be synchronized, because there are accesses from the Controller callback
     99      */
    100     /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
    101         new HashMap<Long,AccountSyncReport>();
    102 
    103     public static void actionReschedule(Context context) {
    104         Intent i = new Intent();
    105         i.setClass(context, MailService.class);
    106         i.setAction(MailService.ACTION_RESCHEDULE);
    107         context.startService(i);
    108     }
    109 
    110     public static void actionCancel(Context context)  {
    111         Intent i = new Intent();
    112         i.setClass(context, MailService.class);
    113         i.setAction(MailService.ACTION_CANCEL);
    114         context.startService(i);
    115     }
    116 
    117     public static void actionDeleteExchangeAccounts(Context context)  {
    118         Intent i = new Intent();
    119         i.setClass(context, MailService.class);
    120         i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS);
    121         context.startService(i);
    122     }
    123 
    124     /**
    125      * Entry point for AttachmentDownloadService to ask that pending mail be sent
    126      * @param context the caller's context
    127      * @param accountId the account whose pending mail should be sent
    128      */
    129     public static void actionSendPendingMail(Context context, long accountId)  {
    130         Intent i = new Intent();
    131         i.setClass(context, MailService.class);
    132         i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
    133         i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
    134         context.startService(i);
    135     }
    136 
    137     @Override
    138     public int onStartCommand(final Intent intent, int flags, final int startId) {
    139         super.onStartCommand(intent, flags, startId);
    140 
    141         EmailAsyncTask.runAsyncParallel(new Runnable() {
    142             @Override
    143             public void run() {
    144                 reconcilePopImapAccountsSync(MailService.this);
    145             }
    146         });
    147 
    148         // TODO this needs to be passed through the controller and back to us
    149         mStartId = startId;
    150         String action = intent.getAction();
    151         final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
    152 
    153         mController = Controller.getInstance(this);
    154         mController.addResultCallback(mControllerCallback);
    155         mContentResolver = getContentResolver();
    156         mContext = this;
    157 
    158         final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    159 
    160         if (ACTION_CHECK_MAIL.equals(action)) {
    161             // DB access required to satisfy this intent, so offload from UI thread
    162             EmailAsyncTask.runAsyncParallel(new Runnable() {
    163                 @Override
    164                 public void run() {
    165                     // If we have the data, restore the last-sync-times for each account
    166                     // These are cached in the wakeup intent in case the process was killed.
    167                     restoreSyncReports(intent);
    168 
    169                     // Sync a specific account if given
    170                     if (Email.DEBUG) {
    171                         Log.d(LOG_TAG, "action: check mail for id=" + accountId);
    172                     }
    173                     if (accountId >= 0) {
    174                         setWatchdog(accountId, alarmManager);
    175                     }
    176 
    177                     // Start sync if account is given && auto-sync is allowed
    178                     boolean syncStarted = false;
    179                     if (accountId != -1 && ContentResolver.getMasterSyncAutomatically()) {
    180                         synchronized(mSyncReports) {
    181                             for (AccountSyncReport report: mSyncReports.values()) {
    182                                 if (report.accountId == accountId) {
    183                                     if (report.syncEnabled) {
    184                                         syncStarted = syncOneAccount(mController, accountId,
    185                                                 startId);
    186                                     }
    187                                     break;
    188                                 }
    189                             }
    190                         }
    191                     }
    192 
    193                     // Reschedule if we didn't start sync.
    194                     if (!syncStarted) {
    195                         // Prevent runaway on the current account by pretending it updated
    196                         if (accountId != -1) {
    197                             updateAccountReport(accountId, 0);
    198                         }
    199                         // Find next account to sync, and reschedule
    200                         reschedule(alarmManager);
    201                         // Stop the service, unless actually syncing (which will stop the service)
    202                         stopSelf(startId);
    203                     }
    204                 }
    205             });
    206         }
    207         else if (ACTION_CANCEL.equals(action)) {
    208             if (Email.DEBUG) {
    209                 Log.d(LOG_TAG, "action: cancel");
    210             }
    211             cancel();
    212             stopSelf(startId);
    213         }
    214         else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) {
    215             if (Email.DEBUG) {
    216                 Log.d(LOG_TAG, "action: delete exchange accounts");
    217             }
    218             EmailAsyncTask.runAsyncParallel(new Runnable() {
    219                 @Override
    220                 public void run() {
    221                     Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
    222                             null, null, null);
    223                     try {
    224                         while (c.moveToNext()) {
    225                             long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
    226                             if ("eas".equals(Account.getProtocol(mContext, accountId))) {
    227                                 // Always log this
    228                                 Log.d(LOG_TAG, "Deleting EAS account: " + accountId);
    229                                 mController.deleteAccountSync(accountId, mContext);
    230                             }
    231                        }
    232                     } finally {
    233                         c.close();
    234                     }
    235                 }
    236             });
    237             stopSelf(startId);
    238         }
    239         else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
    240             if (Email.DEBUG) {
    241                 Log.d(LOG_TAG, "action: send pending mail");
    242             }
    243             EmailAsyncTask.runAsyncParallel(new Runnable() {
    244                 @Override
    245                 public void run() {
    246                     mController.sendPendingMessages(accountId);
    247                 }
    248             });
    249             stopSelf(startId);
    250         }
    251         else if (ACTION_RESCHEDULE.equals(action)) {
    252             if (Email.DEBUG) {
    253                 Log.d(LOG_TAG, "action: reschedule");
    254             }
    255             // DB access required to satisfy this intent, so offload from UI thread
    256             EmailAsyncTask.runAsyncParallel(new Runnable() {
    257                 @Override
    258                 public void run() {
    259                     // When called externally, we refresh the sync reports table to pick up
    260                     // any changes in the account list or account settings
    261                     refreshSyncReports();
    262                     // Finally, scan for the next needing update, and set an alarm for it
    263                     reschedule(alarmManager);
    264                     stopSelf(startId);
    265                 }
    266             });
    267         }
    268 
    269         // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
    270         // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
    271         // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
    272         // will fire and get things running again.
    273         return START_NOT_STICKY;
    274     }
    275 
    276     @Override
    277     public IBinder onBind(Intent intent) {
    278         return null;
    279     }
    280 
    281     @Override
    282     public void onDestroy() {
    283         super.onDestroy();
    284         Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
    285     }
    286 
    287     private void cancel() {
    288         AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    289         PendingIntent pi = createAlarmIntent(-1, null, false);
    290         alarmMgr.cancel(pi);
    291     }
    292 
    293     /**
    294      * Refresh the sync reports, to pick up any changes in the account list or account settings.
    295      */
    296     private void refreshSyncReports() {
    297         synchronized (mSyncReports) {
    298             // Make shallow copy of sync reports so we can recover the prev sync times
    299             HashMap<Long,AccountSyncReport> oldSyncReports =
    300                 new HashMap<Long,AccountSyncReport>(mSyncReports);
    301 
    302             // Delete the sync reports to force a refresh from live account db data
    303             setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
    304 
    305             // Restore prev-sync & next-sync times for any reports in the new list
    306             for (AccountSyncReport newReport : mSyncReports.values()) {
    307                 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
    308                 if (oldReport != null) {
    309                     newReport.prevSyncTime = oldReport.prevSyncTime;
    310                     newReport.setNextSyncTime();
    311                 }
    312             }
    313         }
    314     }
    315 
    316     /**
    317      * Create and send an alarm with the entire list.  This also sends a list of known last-sync
    318      * times with the alarm, so if we are killed between alarms, we don't lose this info.
    319      *
    320      * @param alarmMgr passed in so we can mock for testing.
    321      */
    322     private void reschedule(AlarmManager alarmMgr) {
    323         // restore the reports if lost
    324         setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
    325         synchronized (mSyncReports) {
    326             int numAccounts = mSyncReports.size();
    327             long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
    328             int accountInfoIndex = 0;
    329 
    330             long nextCheckTime = Long.MAX_VALUE;
    331             AccountSyncReport nextAccount = null;
    332             long timeNow = SystemClock.elapsedRealtime();
    333 
    334             for (AccountSyncReport report : mSyncReports.values()) {
    335                 if (report.syncInterval <= 0) {                         // no timed checks - skip
    336                     continue;
    337                 }
    338                 long prevSyncTime = report.prevSyncTime;
    339                 long nextSyncTime = report.nextSyncTime;
    340 
    341                 // select next account to sync
    342                 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
    343                     nextCheckTime = 0;
    344                     nextAccount = report;
    345                 } else if (nextSyncTime < nextCheckTime) {              // next to be checked
    346                     nextCheckTime = nextSyncTime;
    347                     nextAccount = report;
    348                 }
    349                 // collect last-sync-times for all accounts
    350                 // this is using pairs of {long,long} to simplify passing in a bundle
    351                 accountInfo[accountInfoIndex++] = report.accountId;
    352                 accountInfo[accountInfoIndex++] = report.prevSyncTime;
    353             }
    354 
    355             // Clear out any unused elements in the array
    356             while (accountInfoIndex < accountInfo.length) {
    357                 accountInfo[accountInfoIndex++] = -1;
    358             }
    359 
    360             // set/clear alarm as needed
    361             long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
    362             PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
    363 
    364             if (nextAccount == null) {
    365                 alarmMgr.cancel(pi);
    366                 if (Email.DEBUG) {
    367                     Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
    368                 }
    369             } else {
    370                 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
    371                 if (Email.DEBUG) {
    372                     Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
    373                             + " for " + nextAccount);
    374                 }
    375             }
    376         }
    377     }
    378 
    379     /**
    380      * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
    381      * killed by the system due to memory pressure.)  Normally, a mail check will complete and
    382      * the watchdog will be replaced by the call to reschedule().
    383     * @param accountId the account we were trying to check
    384      * @param alarmMgr system alarm manager
    385      */
    386     private void setWatchdog(long accountId, AlarmManager alarmMgr) {
    387         PendingIntent pi = createAlarmIntent(accountId, null, true);
    388         long timeNow = SystemClock.elapsedRealtime();
    389         long nextCheckTime = timeNow + WATCHDOG_DELAY;
    390         alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
    391     }
    392 
    393     /**
    394      * Return a pending intent for use by this alarm.  Most of the fields must be the same
    395      * (in order for the intent to be recognized by the alarm manager) but the extras can
    396      * be different, and are passed in here as parameters.
    397      */
    398     private PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) {
    399         Intent i = new Intent();
    400         i.setClass(this, MailService.class);
    401         i.setAction(ACTION_CHECK_MAIL);
    402         i.putExtra(EXTRA_ACCOUNT, checkId);
    403         i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
    404         if (isWatchdog) {
    405             i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
    406         }
    407         PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
    408         return pi;
    409     }
    410 
    411     /**
    412      * Start a controller sync for a specific account
    413      *
    414      * @param controller The controller to do the sync work
    415      * @param checkAccountId the account Id to try and check
    416      * @param startId the id of this service launch
    417      * @return true if mail checking has started, false if it could not (e.g. bad account id)
    418      */
    419     private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
    420         long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
    421         if (inboxId == Mailbox.NO_MAILBOX) {
    422             return false;
    423         } else {
    424             controller.serviceCheckMail(checkAccountId, inboxId, startId);
    425             return true;
    426         }
    427     }
    428 
    429     /**
    430      * Note:  Times are relative to SystemClock.elapsedRealtime()
    431      *
    432      * TODO:  Look more closely at syncEnabled and see if we can simply coalesce it into
    433      * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
    434      */
    435     @VisibleForTesting
    436     static class AccountSyncReport {
    437         long accountId;
    438         /** The time of the last sync, or, {@code 0}, the last sync time is unknown. */
    439         long prevSyncTime;
    440         /** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */
    441         long nextSyncTime;
    442         /** Minimum time between syncs; in minutes. */
    443         int syncInterval;
    444         /** If {@code true}, auto sync is enabled. */
    445         boolean syncEnabled;
    446 
    447         /**
    448          * Sets the next sync time using the previous sync time and sync interval.
    449          */
    450         private void setNextSyncTime() {
    451             if (syncInterval > 0 && prevSyncTime != 0) {
    452                 nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60);
    453             }
    454         }
    455 
    456         @Override
    457         public String toString() {
    458             return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime;
    459         }
    460     }
    461 
    462     /**
    463      * scan accounts to create a list of { acct, prev sync, next sync, #new }
    464      * use this to create a fresh copy.  assumes all accounts need sync
    465      *
    466      * @param accountId -1 will rebuild the list if empty.  other values will force loading
    467      *   of a single account (e.g if it was created after the original list population)
    468      */
    469     private void setupSyncReports(long accountId) {
    470         synchronized (mSyncReports) {
    471             setupSyncReportsLocked(accountId, mContext);
    472         }
    473     }
    474 
    475     /**
    476      * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
    477      */
    478     @VisibleForTesting
    479     void setupSyncReportsLocked(long accountId, Context context) {
    480         ContentResolver resolver = context.getContentResolver();
    481         if (accountId == SYNC_REPORTS_RESET) {
    482             // For test purposes, force refresh of mSyncReports
    483             mSyncReports.clear();
    484             accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
    485         } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
    486             // -1 == reload the list if empty, otherwise exit immediately
    487             if (mSyncReports.size() > 0) {
    488                 return;
    489             }
    490         } else {
    491             // load a single account if it doesn't already have a sync record
    492             if (mSyncReports.containsKey(accountId)) {
    493                 return;
    494             }
    495         }
    496 
    497         // setup to add a single account or all accounts
    498         Uri uri;
    499         if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
    500             uri = Account.CONTENT_URI;
    501         } else {
    502             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
    503         }
    504 
    505         final boolean oneMinuteRefresh
    506                 = Preferences.getPreferences(this).getForceOneMinuteRefresh();
    507         if (oneMinuteRefresh) {
    508             Log.w(LOG_TAG, "One-minute refresh enabled.");
    509         }
    510 
    511         // We use a full projection here because we'll restore each account object from it
    512         Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
    513         try {
    514             while (c.moveToNext()) {
    515                 Account account = Account.getContent(c, Account.class);
    516                 // The following sanity checks are primarily for the sake of ignoring non-user
    517                 // accounts that may have been left behind e.g. by failed unit tests.
    518                 // Properly-formed accounts will always pass these simple checks.
    519                 if (TextUtils.isEmpty(account.mEmailAddress)
    520                         || account.mHostAuthKeyRecv <= 0
    521                         || account.mHostAuthKeySend <= 0) {
    522                     continue;
    523                 }
    524 
    525                 // The account is OK, so proceed
    526                 AccountSyncReport report = new AccountSyncReport();
    527                 int syncInterval = account.mSyncInterval;
    528 
    529                 // If we're not using MessagingController (EAS at this point), don't schedule syncs
    530                 if (!mController.isMessagingController(account.mId)) {
    531                     syncInterval = Account.CHECK_INTERVAL_NEVER;
    532                 } else if (oneMinuteRefresh && syncInterval >= 0) {
    533                     syncInterval = 1;
    534                 }
    535 
    536                 report.accountId = account.mId;
    537                 report.prevSyncTime = 0;
    538                 report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
    539 
    540                 report.syncInterval = syncInterval;
    541 
    542                 // See if the account is enabled for sync in AccountManager
    543                 android.accounts.Account accountManagerAccount =
    544                     new android.accounts.Account(account.mEmailAddress,
    545                             AccountManagerTypes.TYPE_POP_IMAP);
    546                 report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
    547                         EmailContent.AUTHORITY);
    548 
    549                 // TODO lookup # new in inbox
    550                 mSyncReports.put(report.accountId, report);
    551             }
    552         } finally {
    553             c.close();
    554         }
    555     }
    556 
    557     /**
    558      * Update list with a single account's sync times and unread count
    559      *
    560      * @param accountId the account being updated
    561      * @param newCount the number of new messages, or -1 if not being reported (don't update)
    562      * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
    563      */
    564     private AccountSyncReport updateAccountReport(long accountId, int newCount) {
    565         // restore the reports if lost
    566         setupSyncReports(accountId);
    567         synchronized (mSyncReports) {
    568             AccountSyncReport report = mSyncReports.get(accountId);
    569             if (report == null) {
    570                 // discard result - there is no longer an account with this id
    571                 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
    572                 return null;
    573             }
    574 
    575             // report found - update it (note - editing the report while in-place in the hashmap)
    576             report.prevSyncTime = SystemClock.elapsedRealtime();
    577             report.setNextSyncTime();
    578             if (Email.DEBUG) {
    579                 Log.d(LOG_TAG, "update account " + report.toString());
    580             }
    581             return report;
    582         }
    583     }
    584 
    585     /**
    586      * when we receive an alarm, update the account sync reports list if necessary
    587      * this will be the case when if we have restarted the process and lost the data
    588      * in the global.
    589      *
    590      * @param restoreIntent the intent with the list
    591      */
    592     private void restoreSyncReports(Intent restoreIntent) {
    593         // restore the reports if lost
    594         setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
    595         synchronized (mSyncReports) {
    596             long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
    597             if (accountInfo == null) {
    598                 Log.d(LOG_TAG, "no data in intent to restore");
    599                 return;
    600             }
    601             int accountInfoIndex = 0;
    602             int accountInfoLimit = accountInfo.length;
    603             while (accountInfoIndex < accountInfoLimit) {
    604                 long accountId = accountInfo[accountInfoIndex++];
    605                 long prevSync = accountInfo[accountInfoIndex++];
    606                 AccountSyncReport report = mSyncReports.get(accountId);
    607                 if (report != null) {
    608                     if (report.prevSyncTime == 0) {
    609                         report.prevSyncTime = prevSync;
    610                         report.setNextSyncTime();
    611                     }
    612                 }
    613             }
    614         }
    615     }
    616 
    617     class ControllerResults extends Controller.Result {
    618         @Override
    619         public void updateMailboxCallback(MessagingException result, long accountId,
    620                 long mailboxId, int progress, int numNewMessages,
    621                 ArrayList<Long> addedMessages) {
    622             // First, look for authentication failures and notify
    623            //checkAuthenticationStatus(result, accountId);
    624            if (result != null || progress == 100) {
    625                 // We only track the inbox here in the service - ignore other mailboxes
    626                 long inboxId = Mailbox.findMailboxOfType(MailService.this,
    627                         accountId, Mailbox.TYPE_INBOX);
    628                 if (mailboxId == inboxId) {
    629                     if (progress == 100) {
    630                         updateAccountReport(accountId, numNewMessages);
    631                     } else {
    632                         updateAccountReport(accountId, -1);
    633                     }
    634                 }
    635             }
    636         }
    637 
    638         @Override
    639         public void serviceCheckMailCallback(MessagingException result, long accountId,
    640                 long mailboxId, int progress, long tag) {
    641             if (result != null || progress == 100) {
    642                 if (result != null) {
    643                     // the checkmail ended in an error.  force an update of the refresh
    644                     // time, so we don't just spin on this account
    645                     updateAccountReport(accountId, -1);
    646                 }
    647                 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    648                 reschedule(alarmManager);
    649                 int serviceId = mStartId;
    650                 if (tag != 0) {
    651                     serviceId = (int) tag;
    652                 }
    653                 stopSelf(serviceId);
    654             }
    655         }
    656     }
    657 
    658     public class EmailSyncStatusObserver implements SyncStatusObserver {
    659         @Override
    660         public void onStatusChanged(int which) {
    661             // We ignore the argument (we can only get called in one case - when settings change)
    662         }
    663     }
    664 
    665     public static ArrayList<Account> getPopImapAccountList(Context context) {
    666         ArrayList<Account> providerAccounts = new ArrayList<Account>();
    667         Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
    668                 null, null, null);
    669         try {
    670             while (c.moveToNext()) {
    671                 long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
    672                 String protocol = Account.getProtocol(context, accountId);
    673                 if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
    674                     Account account = Account.restoreAccountWithId(context, accountId);
    675                     if (account != null) {
    676                         providerAccounts.add(account);
    677                     }
    678                 }
    679             }
    680         } finally {
    681             c.close();
    682         }
    683         return providerAccounts;
    684     }
    685 
    686     private static final SingleRunningTask<Context> sReconcilePopImapAccountsSyncExecutor =
    687             new SingleRunningTask<Context>("ReconcilePopImapAccountsSync") {
    688                 @Override
    689                 protected void runInternal(Context context) {
    690                     android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
    691                             .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP);
    692                     ArrayList<Account> providerAccounts = getPopImapAccountList(context);
    693                     MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
    694                             accountManagerAccounts, context);
    695 
    696                 }
    697     };
    698 
    699     /**
    700      * Reconcile POP/IMAP accounts.
    701      */
    702     public static void reconcilePopImapAccountsSync(Context context) {
    703         sReconcilePopImapAccountsSyncExecutor.run(context);
    704     }
    705 
    706     /**
    707      * Determines whether or not POP/IMAP accounts need reconciling or not. This is a safe operation
    708      * to perform on the UI thread.
    709      */
    710     public static boolean hasMismatchInPopImapAccounts(Context context) {
    711         android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
    712                 .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP);
    713         ArrayList<Account> providerAccounts = getPopImapAccountList(context);
    714         return AccountReconciler.accountsNeedReconciling(
    715                 context, providerAccounts, accountManagerAccounts);
    716     }
    717 
    718     /**
    719      * See Utility.reconcileAccounts for details
    720      * @param context The context in which to operate
    721      * @param emailProviderAccounts the exchange provider accounts to work from
    722      * @param accountManagerAccounts The account manager accounts to work from
    723      * @param providerContext the provider's context (in unit tests, this may differ from context)
    724      */
    725     @VisibleForTesting
    726     public static void reconcileAccountsWithAccountManager(Context context,
    727             List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
    728             Context providerContext) {
    729         AccountReconciler.reconcileAccounts(context, emailProviderAccounts, accountManagerAccounts,
    730                 providerContext);
    731     }
    732 
    733     public static void setupAccountManagerAccount(Context context, Account account,
    734             boolean email, boolean calendar, boolean contacts,
    735             AccountManagerCallback<Bundle> callback) {
    736         Bundle options = new Bundle();
    737         HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
    738         if (hostAuthRecv == null) return;
    739         // Set up username/password
    740         options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
    741         options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
    742         options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
    743         options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
    744         options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
    745         String accountType = hostAuthRecv.mProtocol.equals("eas") ?
    746                 AccountManagerTypes.TYPE_EXCHANGE :
    747                 AccountManagerTypes.TYPE_POP_IMAP;
    748         AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
    749                 null);
    750     }
    751 }
    752