/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.email.service; import com.android.email.AccountBackupRestore; import com.android.email.Controller; import com.android.email.Email; import com.android.email.NotificationController; import com.android.email.Preferences; import com.android.email.SecurityPolicy; import com.android.email.SingleRunningTask; import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.HostAuth; import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.utility.AccountReconciler; import com.android.emailcommon.utility.Utility; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.SyncStatusObserver; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * Background service for refreshing non-push email accounts. * * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid * possible problems with out-of-order startId processing. */ public class MailService extends Service { private static final String LOG_TAG = "Email-MailService"; private static final String ACTION_CHECK_MAIL = "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; private static final String ACTION_RESCHEDULE = "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; private static final String ACTION_CANCEL = "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; private static final String ACTION_NOTIFY_MAIL = "com.android.email.intent.action.MAIL_SERVICE_NOTIFY"; private static final String ACTION_SEND_PENDING_MAIL = "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING"; private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS = "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS"; private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes // Sentinel value asking to update mSyncReports if it's currently empty /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; // Sentinel value asking that mSyncReports be rebuilt /*package*/ static final int SYNC_REPORTS_RESET = -2; private static final String[] NEW_MESSAGE_COUNT_PROJECTION = new String[] {AccountColumns.NEW_MESSAGE_COUNT}; private static MailService sMailService; /*package*/ Controller mController; private final Controller.Result mControllerCallback = new ControllerResults(); private ContentResolver mContentResolver; private Context mContext; private Handler mHandler = new Handler(); private int mStartId; /** * Access must be synchronized, because there are accesses from the Controller callback */ /*package*/ static HashMap mSyncReports = new HashMap(); public static void actionReschedule(Context context) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_RESCHEDULE); context.startService(i); } public static void actionCancel(Context context) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_CANCEL); context.startService(i); } public static void actionDeleteExchangeAccounts(Context context) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS); context.startService(i); } /** * Entry point for AttachmentDownloadService to ask that pending mail be sent * @param context the caller's context * @param accountId the account whose pending mail should be sent */ public static void actionSendPendingMail(Context context, long accountId) { Intent i = new Intent(); i.setClass(context, MailService.class); i.setAction(MailService.ACTION_SEND_PENDING_MAIL); i.putExtra(MailService.EXTRA_ACCOUNT, accountId); context.startService(i); } /** * Reset new message counts for one or all accounts. This clears both our local copy and * the values (if any) stored in the account records. * * @param accountId account to clear, or -1 for all accounts */ public static void resetNewMessageCount(final Context context, final long accountId) { synchronized (mSyncReports) { for (AccountSyncReport report : mSyncReports.values()) { if (accountId == -1 || accountId == report.accountId) { report.unseenMessageCount = 0; report.lastUnseenMessageCount = 0; } } } // Clear notification NotificationController.getInstance(context).cancelNewMessageNotification(accountId); // now do the database - all accounts, or just one of them Utility.runAsync(new Runnable() { @Override public void run() { Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI; if (accountId != -1) { uri = ContentUris.withAppendedId(uri, accountId); } context.getContentResolver().update(uri, null, null, null); } }); } /** * Entry point for asynchronous message services (e.g. push mode) to post notifications of new * messages. This assumes that the push provider has already synced the messages into the * appropriate database - this simply triggers the notification mechanism. * * @param context a context * @param accountId the id of the account that is reporting new messages */ public static void actionNotifyNewMessages(Context context, long accountId) { Intent i = new Intent(ACTION_NOTIFY_MAIL); i.setClass(context, MailService.class); i.putExtra(EXTRA_ACCOUNT, accountId); context.startService(i); } /*package*/ static MailService getMailServiceForTest() { return sMailService; } @Override public int onStartCommand(final Intent intent, int flags, final int startId) { super.onStartCommand(intent, flags, startId); // Save the service away (for unit tests) sMailService = this; // Restore accounts, if it has not happened already AccountBackupRestore.restoreAccountsIfNeeded(this); Utility.runAsync(new Runnable() { @Override public void run() { reconcilePopImapAccountsSync(MailService.this); } }); // TODO this needs to be passed through the controller and back to us mStartId = startId; String action = intent.getAction(); final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); mController = Controller.getInstance(this); mController.addResultCallback(mControllerCallback); mContentResolver = getContentResolver(); mContext = this; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (ACTION_CHECK_MAIL.equals(action)) { // DB access required to satisfy this intent, so offload from UI thread Utility.runAsync(new Runnable() { @Override public void run() { // If we have the data, restore the last-sync-times for each account // These are cached in the wakeup intent in case the process was killed. restoreSyncReports(intent); // Sync a specific account if given if (Email.DEBUG) { Log.d(LOG_TAG, "action: check mail for id=" + accountId); } if (accountId >= 0) { setWatchdog(accountId, alarmManager); } // Start sync if account is given && bg data enabled && account has sync enabled boolean syncStarted = false; if (accountId != -1 && isBackgroundDataEnabled()) { synchronized(mSyncReports) { for (AccountSyncReport report: mSyncReports.values()) { if (report.accountId == accountId) { if (report.syncEnabled) { syncStarted = syncOneAccount(mController, accountId, startId); } break; } } } } // Reschedule if we didn't start sync. if (!syncStarted) { // Prevent runaway on the current account by pretending it updated if (accountId != -1) { updateAccountReport(accountId, 0); } // Find next account to sync, and reschedule reschedule(alarmManager); // Stop the service, unless actually syncing (which will stop the service) stopSelf(startId); } } }); } else if (ACTION_CANCEL.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: cancel"); } cancel(); stopSelf(startId); } else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: delete exchange accounts"); } Utility.runAsync(new Runnable() { public void run() { Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, null, null, null); try { while (c.moveToNext()) { long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); if ("eas".equals(Account.getProtocol(mContext, accountId))) { // Always log this Log.d(LOG_TAG, "Deleting EAS account: " + accountId); mController.deleteAccountSync(accountId, mContext); } } } finally { c.close(); } } }); stopSelf(startId); } else if (ACTION_SEND_PENDING_MAIL.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: send pending mail"); } Utility.runAsync(new Runnable() { public void run() { mController.sendPendingMessages(accountId); } }); stopSelf(startId); } else if (ACTION_RESCHEDULE.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: reschedule"); } final NotificationController nc = NotificationController.getInstance(this); // DB access required to satisfy this intent, so offload from UI thread Utility.runAsync(new Runnable() { @Override public void run() { // Clear all notifications, in case account list has changed. // // TODO Clear notifications for non-existing accounts. Now that we have // separate notifications for each account, NotificationController should be // able to do that. nc.cancelNewMessageNotification(-1); // When called externally, we refresh the sync reports table to pick up // any changes in the account list or account settings refreshSyncReports(); // Finally, scan for the next needing update, and set an alarm for it reschedule(alarmManager); stopSelf(startId); } }); } else if (ACTION_NOTIFY_MAIL.equals(action)) { // DB access required to satisfy this intent, so offload from UI thread Utility.runAsync(new Runnable() { @Override public void run() { // Get the current new message count Cursor c = mContentResolver.query( ContentUris.withAppendedId(Account.CONTENT_URI, accountId), NEW_MESSAGE_COUNT_PROJECTION, null, null, null); int newMessageCount = 0; try { if (c.moveToFirst()) { newMessageCount = c.getInt(0); updateAccountReport(accountId, newMessageCount); notifyNewMessages(accountId); } } finally { c.close(); } if (Email.DEBUG) { Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) + " count=" + newMessageCount); } stopSelf(startId); } }); } // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog // alarm before each mailbox check. If the mailbox check never completes, the watchdog // will fire and get things running again. return START_NOT_STICKY; } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { super.onDestroy(); Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); } private void cancel() { AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); PendingIntent pi = createAlarmIntent(-1, null, false); alarmMgr.cancel(pi); } /** * Refresh the sync reports, to pick up any changes in the account list or account settings. */ /*package*/ void refreshSyncReports() { synchronized (mSyncReports) { // Make shallow copy of sync reports so we can recover the prev sync times HashMap oldSyncReports = new HashMap(mSyncReports); // Delete the sync reports to force a refresh from live account db data setupSyncReportsLocked(SYNC_REPORTS_RESET, this); // Restore prev-sync & next-sync times for any reports in the new list for (AccountSyncReport newReport : mSyncReports.values()) { AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); if (oldReport != null) { newReport.prevSyncTime = oldReport.prevSyncTime; if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { newReport.nextSyncTime = newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60); } } } } } /** * Create and send an alarm with the entire list. This also sends a list of known last-sync * times with the alarm, so if we are killed between alarms, we don't lose this info. * * @param alarmMgr passed in so we can mock for testing. */ /* package */ void reschedule(AlarmManager alarmMgr) { // restore the reports if lost setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); synchronized (mSyncReports) { int numAccounts = mSyncReports.size(); long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } int accountInfoIndex = 0; long nextCheckTime = Long.MAX_VALUE; AccountSyncReport nextAccount = null; long timeNow = SystemClock.elapsedRealtime(); for (AccountSyncReport report : mSyncReports.values()) { if (report.syncInterval <= 0) { // no timed checks - skip continue; } long prevSyncTime = report.prevSyncTime; long nextSyncTime = report.nextSyncTime; // select next account to sync if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue nextCheckTime = 0; nextAccount = report; } else if (nextSyncTime < nextCheckTime) { // next to be checked nextCheckTime = nextSyncTime; nextAccount = report; } // collect last-sync-times for all accounts // this is using pairs of {long,long} to simplify passing in a bundle accountInfo[accountInfoIndex++] = report.accountId; accountInfo[accountInfoIndex++] = report.prevSyncTime; } // Clear out any unused elements in the array while (accountInfoIndex < accountInfo.length) { accountInfo[accountInfoIndex++] = -1; } // set/clear alarm as needed long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); if (nextAccount == null) { alarmMgr.cancel(pi); if (Email.DEBUG) { Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); } } else { alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); if (Email.DEBUG) { Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime + " for " + nextAccount); } } } } /** * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are * killed by the system due to memory pressure.) Normally, a mail check will complete and * the watchdog will be replaced by the call to reschedule(). * @param accountId the account we were trying to check * @param alarmMgr system alarm manager */ private void setWatchdog(long accountId, AlarmManager alarmMgr) { PendingIntent pi = createAlarmIntent(accountId, null, true); long timeNow = SystemClock.elapsedRealtime(); long nextCheckTime = timeNow + WATCHDOG_DELAY; alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); } /** * Return a pending intent for use by this alarm. Most of the fields must be the same * (in order for the intent to be recognized by the alarm manager) but the extras can * be different, and are passed in here as parameters. */ /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) { Intent i = new Intent(); i.setClass(this, MailService.class); i.setAction(ACTION_CHECK_MAIL); i.putExtra(EXTRA_ACCOUNT, checkId); i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); if (isWatchdog) { i.putExtra(EXTRA_DEBUG_WATCHDOG, true); } PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); return pi; } /** * Start a controller sync for a specific account * * @param controller The controller to do the sync work * @param checkAccountId the account Id to try and check * @param startId the id of this service launch * @return true if mail checking has started, false if it could not (e.g. bad account id) */ private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); if (inboxId == Mailbox.NO_MAILBOX) { return false; } else { controller.serviceCheckMail(checkAccountId, inboxId, startId); return true; } } /** * Note: Times are relative to SystemClock.elapsedRealtime() * * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). */ /*package*/ static class AccountSyncReport { long accountId; long prevSyncTime; // 0 == unknown long nextSyncTime; // 0 == ASAP -1 == don't sync /** # of "unseen" messages to show in notification */ int unseenMessageCount; /** * # of unseen, the value shown on the last notification. Used to * calculate "the number of messages that have just been fetched". * * TODO It's a sort of cheating. Should we use the "real" number? The only difference * is the first notification after reboot / process restart. */ int lastUnseenMessageCount; int syncInterval; boolean notify; boolean syncEnabled; // whether auto sync is enabled for this account /** # of messages that have just been fetched */ int getJustFetchedMessageCount() { return unseenMessageCount - lastUnseenMessageCount; } @Override public String toString() { return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen=" + unseenMessageCount; } } /** * scan accounts to create a list of { acct, prev sync, next sync, #new } * use this to create a fresh copy. assumes all accounts need sync * * @param accountId -1 will rebuild the list if empty. other values will force loading * of a single account (e.g if it was created after the original list population) */ /* package */ void setupSyncReports(long accountId) { synchronized (mSyncReports) { setupSyncReportsLocked(accountId, mContext); } } /** * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. */ /*package*/ void setupSyncReportsLocked(long accountId, Context context) { ContentResolver resolver = context.getContentResolver(); if (accountId == SYNC_REPORTS_RESET) { // For test purposes, force refresh of mSyncReports mSyncReports.clear(); accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY; } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { // -1 == reload the list if empty, otherwise exit immediately if (mSyncReports.size() > 0) { return; } } else { // load a single account if it doesn't already have a sync record if (mSyncReports.containsKey(accountId)) { return; } } // setup to add a single account or all accounts Uri uri; if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { uri = Account.CONTENT_URI; } else { uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); } final boolean oneMinuteRefresh = Preferences.getPreferences(this).getForceOneMinuteRefresh(); if (oneMinuteRefresh) { Log.w(LOG_TAG, "One-minute refresh enabled."); } // We use a full projection here because we'll restore each account object from it Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null); try { while (c.moveToNext()) { Account account = Account.getContent(c, Account.class); // The following sanity checks are primarily for the sake of ignoring non-user // accounts that may have been left behind e.g. by failed unit tests. // Properly-formed accounts will always pass these simple checks. if (TextUtils.isEmpty(account.mEmailAddress) || account.mHostAuthKeyRecv <= 0 || account.mHostAuthKeySend <= 0) { continue; } // The account is OK, so proceed AccountSyncReport report = new AccountSyncReport(); int syncInterval = account.mSyncInterval; // If we're not using MessagingController (EAS at this point), don't schedule syncs if (!mController.isMessagingController(account.mId)) { syncInterval = Account.CHECK_INTERVAL_NEVER; } else if (oneMinuteRefresh && syncInterval >= 0) { syncInterval = 1; } report.accountId = account.mId; report.prevSyncTime = 0; report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync report.unseenMessageCount = 0; report.lastUnseenMessageCount = 0; report.syncInterval = syncInterval; report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; // See if the account is enabled for sync in AccountManager android.accounts.Account accountManagerAccount = new android.accounts.Account(account.mEmailAddress, AccountManagerTypes.TYPE_POP_IMAP); report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount, EmailContent.AUTHORITY); // TODO lookup # new in inbox mSyncReports.put(report.accountId, report); } } finally { c.close(); } } /** * Update list with a single account's sync times and unread count * * @param accountId the account being updated * @param newCount the number of new messages, or -1 if not being reported (don't update) * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) */ /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) { // restore the reports if lost setupSyncReports(accountId); synchronized (mSyncReports) { AccountSyncReport report = mSyncReports.get(accountId); if (report == null) { // discard result - there is no longer an account with this id Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); return null; } // report found - update it (note - editing the report while in-place in the hashmap) report.prevSyncTime = SystemClock.elapsedRealtime(); if (report.syncInterval > 0) { report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); } if (newCount != -1) { report.unseenMessageCount = newCount; } if (Email.DEBUG) { Log.d(LOG_TAG, "update account " + report.toString()); } return report; } } /** * when we receive an alarm, update the account sync reports list if necessary * this will be the case when if we have restarted the process and lost the data * in the global. * * @param restoreIntent the intent with the list */ /* package */ void restoreSyncReports(Intent restoreIntent) { // restore the reports if lost setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); synchronized (mSyncReports) { long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); if (accountInfo == null) { Log.d(LOG_TAG, "no data in intent to restore"); return; } int accountInfoIndex = 0; int accountInfoLimit = accountInfo.length; while (accountInfoIndex < accountInfoLimit) { long accountId = accountInfo[accountInfoIndex++]; long prevSync = accountInfo[accountInfoIndex++]; AccountSyncReport report = mSyncReports.get(accountId); if (report != null) { if (report.prevSyncTime == 0) { report.prevSyncTime = prevSync; if (report.syncInterval > 0 && report.prevSyncTime != 0) { report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); } } } } } } class ControllerResults extends Controller.Result { @Override public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages) { // First, look for authentication failures and notify //checkAuthenticationStatus(result, accountId); if (result != null || progress == 100) { // We only track the inbox here in the service - ignore other mailboxes long inboxId = Mailbox.findMailboxOfType(MailService.this, accountId, Mailbox.TYPE_INBOX); if (mailboxId == inboxId) { if (progress == 100) { updateAccountReport(accountId, numNewMessages); if (numNewMessages > 0) { notifyNewMessages(accountId); } } else { updateAccountReport(accountId, -1); } } } } @Override public void serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag) { if (result != null || progress == 100) { if (result != null) { // the checkmail ended in an error. force an update of the refresh // time, so we don't just spin on this account updateAccountReport(accountId, -1); } AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); reschedule(alarmManager); int serviceId = MailService.this.mStartId; if (tag != 0) { serviceId = (int) tag; } stopSelf(serviceId); } } } /** * Show "new message" notification for an account. (Notification is shown per account.) */ private void notifyNewMessages(final long accountId) { final int unseenMessageCount; final int justFetchedCount; synchronized (mSyncReports) { AccountSyncReport report = mSyncReports.get(accountId); if (report == null || report.unseenMessageCount == 0 || !report.notify) { return; } unseenMessageCount = report.unseenMessageCount; justFetchedCount = report.getJustFetchedMessageCount(); report.lastUnseenMessageCount = report.unseenMessageCount; } NotificationController.getInstance(this).showNewMessageNotification(accountId, unseenMessageCount, justFetchedCount); } /** * @see ConnectivityManager#getBackgroundDataSetting() */ private boolean isBackgroundDataEnabled() { ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); return cm.getBackgroundDataSetting(); } public class EmailSyncStatusObserver implements SyncStatusObserver { public void onStatusChanged(int which) { // We ignore the argument (we can only get called in one case - when settings change) } } public static ArrayList getPopImapAccountList(Context context) { ArrayList providerAccounts = new ArrayList(); Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, null, null, null); try { while (c.moveToNext()) { long accountId = c.getLong(Account.CONTENT_ID_COLUMN); String protocol = Account.getProtocol(context, accountId); if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) { Account account = Account.restoreAccountWithId(context, accountId); if (account != null) { providerAccounts.add(account); } } } } finally { c.close(); } return providerAccounts; } private static final SingleRunningTask sReconcilePopImapAccountsSyncExecutor = new SingleRunningTask("ReconcilePopImapAccountsSync") { @Override protected void runInternal(Context context) { android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); ArrayList providerAccounts = getPopImapAccountList(context); MailService.reconcileAccountsWithAccountManager(context, providerAccounts, accountManagerAccounts, false, context.getContentResolver()); } }; /** * Reconcile POP/IMAP accounts. */ public static void reconcilePopImapAccountsSync(Context context) { sReconcilePopImapAccountsSyncExecutor.run(context); } /** * Handles a variety of cleanup actions that must be performed when an account has been deleted. * This includes triggering an account backup, ensuring that security policies are properly * reset, if necessary, notifying the UI of the change, and resetting scheduled syncs and * notifications. * @param context the caller's context */ public static void accountDeleted(Context context) { AccountBackupRestore.backupAccounts(context); SecurityPolicy.getInstance(context).reducePolicies(); Email.setNotifyUiAccountsChanged(true); MailService.actionReschedule(context); } /** * See Utility.reconcileAccounts for details * @param context The context in which to operate * @param emailProviderAccounts the exchange provider accounts to work from * @param accountManagerAccounts The account manager accounts to work from * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. * @param resolver the content resolver for making provider updates (injected for testability) */ /* package */ public static void reconcileAccountsWithAccountManager(Context context, List emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, boolean blockExternalChanges, ContentResolver resolver) { boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, emailProviderAccounts, accountManagerAccounts, resolver); // If we changed the list of accounts, refresh the backup & security settings if (!blockExternalChanges && accountsDeleted) { accountDeleted(context); } } public static void setupAccountManagerAccount(Context context, EmailContent.Account account, boolean email, boolean calendar, boolean contacts, AccountManagerCallback callback) { Bundle options = new Bundle(); HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); // Set up username/password options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress); options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword); options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts); options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar); options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email); String accountType = hostAuthRecv.mProtocol.equals("eas") ? AccountManagerTypes.TYPE_EXCHANGE : AccountManagerTypes.TYPE_POP_IMAP; AccountManager.get(context).addAccount(accountType, null, null, options, null, callback, null); } }