• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
actionReschedule(Context context)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 
actionCancel(Context context)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 
actionDeleteExchangeAccounts(Context context)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      */
actionSendPendingMail(Context context, long accountId)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
onStartCommand(final Intent intent, int flags, final int startId)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
onBind(Intent intent)277     public IBinder onBind(Intent intent) {
278         return null;
279     }
280 
281     @Override
onDestroy()282     public void onDestroy() {
283         super.onDestroy();
284         Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
285     }
286 
cancel()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      */
refreshSyncReports()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      */
reschedule(AlarmManager alarmMgr)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      */
setWatchdog(long accountId, AlarmManager alarmMgr)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      */
createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog)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      */
syncOneAccount(Controller controller, long checkAccountId, int startId)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          */
setNextSyncTime()450         private void setNextSyncTime() {
451             if (syncInterval > 0 && prevSyncTime != 0) {
452                 nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60);
453             }
454         }
455 
456         @Override
toString()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      */
setupSyncReports(long accountId)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
setupSyncReportsLocked(long accountId, Context context)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      */
updateAccountReport(long accountId, int newCount)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      */
restoreSyncReports(Intent restoreIntent)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
updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages)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
serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag)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
onStatusChanged(int which)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 
getPopImapAccountList(Context context)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      */
reconcilePopImapAccountsSync(Context context)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      */
hasMismatchInPopImapAccounts(Context context)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
reconcileAccountsWithAccountManager(Context context, List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, Context providerContext)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 
setupAccountManagerAccount(Context context, Account account, boolean email, boolean calendar, boolean contacts, AccountManagerCallback<Bundle> callback)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