blob: 65d9b00530a9819341b003d7c0e94ffac2311443 [file] [log] [blame]
/*
* Copyright (C) 2010 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.exchange.service;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.service.ServiceProxy;
import com.android.emailcommon.utility.IntentUtilities;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.Eas;
import com.android.exchange.R.drawable;
import com.android.exchange.R.string;
import com.android.exchange.adapter.PingParser;
import com.android.exchange.adapter.Search;
import com.android.exchange.eas.EasFolderSync;
import com.android.exchange.eas.EasLoadAttachment;
import com.android.exchange.eas.EasMoveItems;
import com.android.exchange.eas.EasOperation;
import com.android.exchange.eas.EasPing;
import com.android.exchange.eas.EasSync;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
import java.util.HashMap;
import java.util.HashSet;
/**
* Service for communicating with Exchange servers. There are three main parts of this class:
* TODO: Flesh out these comments.
* 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
* 2) Bookkeeping for running Ping requests, which handles push notifications.
* 3) An {@link IEmailService} Stub to handle RPC from the UI.
*/
public class EmailSyncAdapterService extends AbstractSyncAdapterService {
private static final String TAG = Eas.LOG_TAG;
/**
* Temporary while converting to EasService. Do not check in set to true.
* When true, delegates various operations to {@link EasService}, for use while developing the
* new service.
* The two following fields are used to support what happens when this is true.
*/
private static final boolean DELEGATE_TO_EAS_SERVICE = false;
private IEmailService mEasService;
private ServiceConnection mConnection;
private static final String EXTRA_START_PING = "START_PING";
private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS;
/**
* The amount of time between periodic syncs intended to ensure that push hasn't died.
*/
private static final long KICK_SYNC_INTERVAL =
DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
/** Controls whether we do a periodic "kick" to restart the ping. */
private static final boolean SCHEDULE_KICK = true;
/** Projection used for getting email address for an account. */
private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
private static final Object sSyncAdapterLock = new Object();
private static AbstractThreadedSyncAdapter sSyncAdapter = null;
/**
* Bookkeeping for handling synchronization between pings and syncs.
* "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
* the term for the Exchange command, but this code should be generic enough to be easily
* extended to IMAP.
* "Sync" refers to an actual sync command to either fetch mail state, account state, or send
* mail (send is implemented as "sync the outbox").
* TODO: Outbox sync probably need not stop a ping in progress.
* Basic rules of how these interact (note that all rules are per account):
* - Only one ping or sync may run at a time.
* - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
* a sync is in progress.
* - On the other hand, ping requests may come in while handling a ping.
* - "Ping request" is shorthand for "a request to change our ping parameters", which includes
* a request to stop receiving push notifications.
* - If neither a ping nor a sync is running, then a request for either will run it.
* - If a sync is running, new ping requests block until the sync completes.
* - If a ping is running, a new sync request stops the ping and creates a pending ping
* (which blocks until the sync completes).
* - If a ping is running, a new ping request stops the ping and either starts a new one or
* does nothing, as appopriate (since a ping request can be to stop pushing).
* - As an optimization, while a ping request is waiting to run, subsequent ping requests are
* ignored (the pending ping will pick up the latest ping parameters at the time it runs).
*/
public class SyncHandlerSynchronizer {
/**
* Map of account id -> ping handler.
* For a given account id, there are three possible states:
* 1) If no ping or sync is currently running, there is no entry in the map for the account.
* 2) If a ping is running, there is an entry with the appropriate ping handler.
* 3) If there is a sync running, there is an entry with null as the value.
* We cannot have more than one ping or sync running at a time.
*/
private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>();
/**
* Wait until neither a sync nor a ping is running on this account, and then return.
* If there's a ping running, actively stop it. (For syncs, we have to just wait.)
* @param accountId The account we want to wait for.
*/
private synchronized void waitUntilNoActivity(final long accountId) {
while (mPingHandlers.containsKey(accountId)) {
final PingTask pingHandler = mPingHandlers.get(accountId);
if (pingHandler != null) {
pingHandler.stop();
}
try {
wait();
} catch (final InterruptedException e) {
// TODO: When would this happen, and how should I handle it?
}
}
}
/**
* Use this to see if we're currently syncing, as opposed to pinging or doing nothing.
* @param accountId The account to check.
* @return Whether that account is currently running a sync.
*/
private synchronized boolean isRunningSync(final long accountId) {
return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null);
}
/**
* If there are no running pings, stop the service.
*/
private void stopServiceIfNoPings() {
for (final PingTask pingHandler : mPingHandlers.values()) {
if (pingHandler != null) {
return;
}
}
EmailSyncAdapterService.this.stopSelf();
}
/**
* Called prior to starting a sync to update our bookkeeping. We don't actually run the sync
* here; the caller must do that.
* @param accountId The account on which we are running a sync.
*/
public synchronized void startSync(final long accountId) {
waitUntilNoActivity(accountId);
mPingHandlers.put(accountId, null);
}
/**
* Starts or restarts a ping for an account, if the current account state indicates that it
* wants to push.
* @param account The account whose ping is being modified.
*/
public synchronized void modifyPing(final boolean lastSyncHadError,
final Account account) {
// If a sync is currently running, it will start a ping when it's done, so there's no
// need to do anything right now.
if (isRunningSync(account.mId)) {
return;
}
// Don't ping if we're on security hold.
if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
return;
}
// Don't ping for accounts that haven't performed initial sync.
if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
return;
}
// Determine if this account needs pushes. All of the following must be true:
// - The account's sync interval must indicate that it wants push.
// - At least one content type must be sync-enabled in the account manager.
// - At least one mailbox of a sync-enabled type must have automatic sync enabled.
final EmailSyncAdapterService service = EmailSyncAdapterService.this;
final android.accounts.Account amAccount = new android.accounts.Account(
account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
boolean pushNeeded = false;
if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
final HashSet<String> authsToSync = getAuthsToSync(amAccount);
// If we have at least one sync-enabled content type, check for syncing mailboxes.
if (!authsToSync.isEmpty()) {
final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(),
account.mId);
if (c != null) {
try {
while (c.moveToNext()) {
final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
pushNeeded = true;
break;
}
}
} finally {
c.close();
}
}
}
}
// Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync.
final PingTask pingSyncHandler = mPingHandlers.get(account.mId);
final Bundle extras = new Bundle(1);
extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
if (pushNeeded) {
// First start or restart the ping as appropriate.
if (pingSyncHandler != null) {
pingSyncHandler.restart();
} else {
// Start a new ping.
// Note: unlike startSync, we CANNOT allow the caller to do the actual work.
// If we return before the ping starts, there's a race condition where another
// ping or sync might start first. It only works for startSync because sync is
// higher priority than ping (i.e. a ping can't start while a sync is pending)
// and only one sync can run at a time.
if (lastSyncHadError) {
// Schedule an alarm to set up the ping in 5 minutes
final Intent intent = new Intent(service, EmailSyncAdapterService.class);
intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
intent.putExtra(EXTRA_START_PING, true);
intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
final PendingIntent pi = PendingIntent.getService(
EmailSyncAdapterService.this, 0, intent,
PendingIntent.FLAG_ONE_SHOT);
final AlarmManager am = (AlarmManager)getSystemService(
Context.ALARM_SERVICE);
final long atTime = SystemClock.elapsedRealtime() +
SYNC_ERROR_BACKOFF_MILLIS;
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
} else {
final PingTask pingHandler = new PingTask(service, account, amAccount,
this);
mPingHandlers.put(account.mId, pingHandler);
pingHandler.start();
// Whenever we have a running ping, make sure this service stays running.
service.startService(new Intent(service, EmailSyncAdapterService.class));
}
}
if (SCHEDULE_KICK) {
ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
KICK_SYNC_INTERVAL);
}
} else {
if (pingSyncHandler != null) {
pingSyncHandler.stop();
}
if (SCHEDULE_KICK) {
ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras);
}
}
}
/**
* Updates the synchronization bookkeeping when a sync is done.
* @param account The account whose sync just finished.
*/
public synchronized void syncComplete(final boolean lastSyncHadError,
final Account account) {
LogUtils.d(TAG, "syncComplete, err: " + lastSyncHadError);
mPingHandlers.remove(account.mId);
// Syncs can interrupt pings, so we should check if we need to start one now.
// If the last sync had a fatal error, we will not immediately recreate the ping.
// Instead, we'll set an alarm that will restart them in a few minutes. This prevents
// a battery draining spin if there is some kind of protocol error or other
// non-transient failure. (Actually, immediately pinging even for a transient error
// isn't great)
modifyPing(lastSyncHadError, account);
stopServiceIfNoPings();
notifyAll();
}
/**
* Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only
* sync if necessary.
* @param amAccount The {@link android.accounts.Account} for this account.
* @param accountId The account whose ping just finished.
* @param pingStatus The status value from {@link PingParser} for the last ping performed.
* This cannot be one of the values that results in another ping, so this
* function only needs to handle the terminal statuses.
*/
public synchronized void pingComplete(final android.accounts.Account amAccount,
final long accountId, final int pingStatus) {
mPingHandlers.remove(accountId);
// TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
// TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
// TODO: Should this just re-request ping if status < 0? This would do the wrong thing
// for e.g. auth errors, though.
if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE ||
pingStatus == EasOperation.RESULT_OTHER_FAILURE) {
// Request a new ping through the SyncManager. This will do the right thing if the
// exception was due to loss of network connectivity, etc. (i.e. it will wait for
// network to restore and then request it).
EasPing.requestPing(amAccount);
} else {
stopServiceIfNoPings();
}
// TODO: It might be the case that only STATUS_CHANGES_FOUND and
// STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through.
notifyAll();
}
}
private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
/**
* The binder for IEmailService.
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
private String getEmailAddressForAccount(final long accountId) {
final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
new String[] {Long.toString(accountId)}, null, 0);
if (emailAddress == null) {
LogUtils.e(TAG, "Could not find email address for account %d", accountId);
}
return emailAddress;
}
@Override
public Bundle validate(final HostAuth hostAuth) {
LogUtils.d(TAG, "IEmailService.validate");
if (mEasService != null) {
try {
return mEasService.validate(hostAuth);
} catch (final RemoteException re) {
LogUtils.e(TAG, re, "While asking EasService to handle validate");
}
}
return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate();
}
@Override
public Bundle autoDiscover(final String username, final String password) {
LogUtils.d(TAG, "IEmailService.autoDiscover");
return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
.doAutodiscover();
}
@Override
public void updateFolderList(final long accountId) {
LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
if (mEasService != null) {
try {
mEasService.updateFolderList(accountId);
return;
} catch (final RemoteException re) {
LogUtils.e(TAG, re, "While asking EasService to updateFolderList");
}
}
final String emailAddress = getEmailAddressForAccount(accountId);
if (emailAddress != null) {
final Bundle extras = new Bundle(1);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(new android.accounts.Account(
emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
EmailContent.AUTHORITY, extras);
}
}
@Override
public void setLogging(final int flags) {
// TODO: fix this?
// Protocol logging
Eas.setUserDebug(flags);
// Sync logging
//setUserDebug(flags);
}
@Override
public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
final long attachmentId, final boolean background) {
LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
// TODO: Prevent this from happening in parallel with a sync?
final EasLoadAttachment operation = new EasLoadAttachment(EmailSyncAdapterService.this,
accountId, attachmentId, callback);
operation.performOperation(null);
}
@Override
public void sendMeetingResponse(final long messageId, final int response) {
LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
response);
}
/**
* Delete PIM (calendar, contacts) data for the specified account
*
* @param emailAddress the email address for the account whose data should be deleted
*/
@Override
public void deleteAccountPIMData(final String emailAddress) {
LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
if (emailAddress != null) {
final Context context = EmailSyncAdapterService.this;
EasContactsSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
EasCalendarSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
}
// TODO: Run account reconciler?
}
@Override
public int searchMessages(final long accountId, final SearchParams searchParams,
final long destMailboxId) {
LogUtils.d(TAG, "IEmailService.searchMessages");
return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams,
destMailboxId);
// TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
}
@Override
public void sendMail(final long accountId) {}
@Override
public void pushModify(final long accountId) {}
};
public EmailSyncAdapterService() {
super();
}
/**
* {@link AsyncTask} for restarting pings for all accounts that need it.
*/
private static final String PUSH_ACCOUNTS_SELECTION =
AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
private final ContentResolver mContentResolver;
private final SyncHandlerSynchronizer mSyncHandlerMap;
private boolean mAnyAccounts;
public RestartPingsTask(final ContentResolver contentResolver,
final SyncHandlerSynchronizer syncHandlerMap) {
mContentResolver = contentResolver;
mSyncHandlerMap = syncHandlerMap;
}
@Override
protected Void doInBackground(Void... params) {
final Cursor c = mContentResolver.query(Account.CONTENT_URI,
Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
if (c != null) {
try {
mAnyAccounts = (c.getCount() != 0);
while (c.moveToNext()) {
final Account account = new Account();
account.restore(c);
mSyncHandlerMap.modifyPing(false, account);
}
} finally {
c.close();
}
} else {
mAnyAccounts = false;
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!mAnyAccounts) {
LogUtils.d(TAG, "stopping for no accounts");
EmailSyncAdapterService.this.stopSelf();
}
}
}
@Override
public void onCreate() {
LogUtils.v(TAG, "onCreate()");
super.onCreate();
startService(new Intent(this, EmailSyncAdapterService.class));
// Restart push for all accounts that need it.
new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR);
if (DELEGATE_TO_EAS_SERVICE) {
// TODO: This block is temporary to support the transition to EasService.
mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mEasService = IEmailService.Stub.asInterface(binder);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mEasService = null;
}
};
bindService(new Intent(this, EasService.class), mConnection, Context.BIND_AUTO_CREATE);
}
}
@Override
public void onDestroy() {
LogUtils.v(TAG, "onDestroy()");
super.onDestroy();
for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) {
if (task != null) {
task.stop();
}
}
if (DELEGATE_TO_EAS_SERVICE) {
// TODO: This block is temporary to support the transition to EasService.
unbindService(mConnection);
}
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) {
return mBinder;
}
return super.onBind(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null &&
TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
// We've been asked to forcibly shutdown. This happens if email accounts are
// deleted, otherwise we can get errors if services are still running for
// accounts that are now gone.
// TODO: This is kind of a hack, it would be nicer if we could handle it correctly
// if accounts disappear out from under us.
LogUtils.d(TAG, "Forced shutdown, killing process");
System.exit(-1);
} else if (intent.getBooleanExtra(EXTRA_START_PING, false)) {
LogUtils.d(TAG, "Restarting ping from alarm");
// We've been woken up by an alarm to restart our ping. This happens if a sync
// fails, rather that instantly starting the ping, we'll hold off for a few minutes.
final android.accounts.Account account =
intent.getParcelableExtra(EXTRA_PING_ACCOUNT);
EasPing.requestPing(account);
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
protected AbstractThreadedSyncAdapter getSyncAdapter() {
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapterImpl(this);
}
return sSyncAdapter;
}
}
// TODO: Handle cancelSync() appropriately.
private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */);
}
@Override
public void onPerformSync(final android.accounts.Account acct, final Bundle extras,
final String authority, final ContentProviderClient provider,
final SyncResult syncResult) {
if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
LogUtils.d(TAG, "onPerformSync: %s, %s", acct.toString(), extras.toString());
} else {
LogUtils.i(TAG, "onPerformSync: %s", extras.toString());
}
TempDirectory.setTempDirectory(EmailSyncAdapterService.this);
// TODO: Perform any connectivity checks, bail early if we don't have proper network
// for this sync operation.
final Context context = getContext();
final ContentResolver cr = context.getContentResolver();
// Get the EmailContent Account
final Account account;
final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null);
try {
if (!accountCursor.moveToFirst()) {
// Could not load account.
// TODO: improve error handling.
LogUtils.w(TAG, "onPerformSync: could not load account");
return;
}
account = new Account();
account.restore(accountCursor);
} finally {
accountCursor.close();
}
// Figure out what we want to sync, based on the extras and our account sync status.
final boolean isInitialSync = EmailContent.isInitialSyncKey(account.mSyncKey);
final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(extras);
final int mailboxType = extras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE,
Mailbox.TYPE_NONE);
final boolean hasCallbackMethod =
extras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
if (hasCallbackMethod && mailboxIds != null) {
for (long mailboxId : mailboxIds) {
EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
EmailServiceStatus.IN_PROGRESS, 0, UIProvider.LastSyncResult.SUCCESS);
}
}
// Push only means this sync request should only refresh the ping (either because
// settings changed, or we need to restart it for some reason).
final boolean pushOnly = Mailbox.isPushOnlyExtras(extras);
// Account only means just do a FolderSync.
final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras);
// A "full sync" means that we didn't request a more specific type of sync.
final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
mailboxType == Mailbox.TYPE_NONE);
// A FolderSync is necessary for full sync, initial sync, and account only sync.
final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
// If we're just twiddling the push, we do the lightweight thing and bail early.
if (pushOnly && !isFolderSync) {
mSyncHandlerMap.modifyPing(false, account);
LogUtils.d(TAG, "onPerformSync: mailbox push only");
return;
}
// Do the bookkeeping for starting a sync, including stopping a ping if necessary.
mSyncHandlerMap.startSync(account.mId);
// Perform a FolderSync if necessary.
// TODO: We permit FolderSync even during security hold, because it's necessary to
// resolve some holds. Ideally we would only do it for the holds that require it.
if (isFolderSync) {
final EasFolderSync folderSync = new EasFolderSync(context, account);
folderSync.doFolderSync(syncResult);
}
boolean lastSyncHadError = false;
if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) == 0) {
// Perform email upsync for this account. Moves first, then state changes.
if (!isInitialSync) {
EasMoveItems move = new EasMoveItems(context, account);
move.upsyncMovedMessages(syncResult);
// TODO: EasSync should eventually handle both up and down; for now, it's used
// purely for upsync.
EasSync upsync = new EasSync(context, account);
upsync.upsync(syncResult);
}
// TODO: Should we refresh account here? It may have changed while waiting for any
// pings to stop. It may not matter since the things that may have been twiddled
// might not affect syncing.
if (mailboxIds != null) {
long numIoExceptions = 0;
long numAuthExceptions = 0;
// Sync the mailbox that was explicitly requested.
for (final long mailboxId : mailboxIds) {
final boolean success = syncMailbox(context, cr, acct, account, mailboxId,
extras, syncResult, null, true);
if (!success) {
lastSyncHadError = true;
}
if (hasCallbackMethod) {
final int result;
if (syncResult.hasError()) {
if (syncResult.stats.numIoExceptions > numIoExceptions) {
result = UIProvider.LastSyncResult.CONNECTION_ERROR;
numIoExceptions = syncResult.stats.numIoExceptions;
} else if (syncResult.stats.numAuthExceptions> numAuthExceptions) {
result = UIProvider.LastSyncResult.AUTH_ERROR;
numAuthExceptions= syncResult.stats.numAuthExceptions;
} else {
result = UIProvider.LastSyncResult.INTERNAL_ERROR;
}
} else {
result = UIProvider.LastSyncResult.SUCCESS;
}
EmailServiceStatus.syncMailboxStatus(
cr, extras, mailboxId,EmailServiceStatus.SUCCESS, 0, result);
}
}
} else if (!accountOnly && !pushOnly) {
// We have to sync multiple folders.
final Cursor c;
if (isFullSync) {
// Full account sync includes all mailboxes that participate in system sync.
c = Mailbox.getMailboxIdsForSync(cr, account.mId);
} else {
// Type-filtered sync should only get the mailboxes of a specific type.
c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType);
}
if (c != null) {
try {
final HashSet<String> authsToSync = getAuthsToSync(acct);
while (c.moveToNext()) {
boolean success = syncMailbox(context, cr, acct, account,
c.getLong(0), extras, syncResult, authsToSync, false);
if (!success) {
lastSyncHadError = true;
}
}
} finally {
c.close();
}
}
}
}
// Clean up the bookkeeping, including restarting ping if necessary.
mSyncHandlerMap.syncComplete(lastSyncHadError, account);
// TODO: It may make sense to have common error handling here. Two possible mechanisms:
// 1) performSync return value can signal some useful info.
// 2) syncResult can contain useful info.
LogUtils.d(TAG, "onPerformSync: finished");
}
/**
* Update the mailbox's sync status with the provider and, if we're finished with the sync,
* write the last sync time as well.
* @param context Our {@link Context}.
* @param mailbox The mailbox whose sync status to update.
* @param cv A {@link ContentValues} object to use for updating the provider.
* @param syncStatus The status for the current sync.
*/
private void updateMailbox(final Context context, final Mailbox mailbox,
final ContentValues cv, final int syncStatus) {
cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
}
mailbox.update(context, cv);
}
private boolean syncMailbox(final Context context, final ContentResolver cr,
final android.accounts.Account acct, final Account account, final long mailboxId,
final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync,
final boolean isMailboxSync) {
final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) {
return false;
}
if (mailbox.mAccountKey != account.mId) {
LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(),
extras.toString());
return false;
}
if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
// We are asking for an account sync, but this mailbox type is not configured for
// sync. Do NOT treat this as a sync error for ping backoff purposes.
return true;
}
if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
// TODO: Because we don't have bidirectional sync working, trying to downsync
// the drafts folder is confusing. b/11158759
// For now, just disable all syncing of DRAFTS type folders.
// Automatic syncing should always be disabled, but we also stop it here to ensure
// that we won't sync even if the user attempts to force a sync from the UI.
// Do NOT treat as a sync error for ping backoff purposes.
LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
return true;
}
final boolean success;
// Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
// treated as background syncs.
// TODO: Push will be treated as "user" syncs, and probably should be background.
final ContentValues cv = new ContentValues(2);
updateMailbox(context, mailbox, cv, isMailboxSync ?
EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
final EasOutboxSyncHandler outboxSyncHandler =
new EasOutboxSyncHandler(context, account, mailbox);
outboxSyncHandler.performSync();
success = true;
} else if(mailbox.isSyncable()) {
final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr,
acct, account, mailbox, extras, syncResult);
if (syncHandler != null) {
success = syncHandler.performSync(syncResult);
} else {
success = false;
}
} else {
success = false;
}
updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE);
if (syncResult.stats.numAuthExceptions > 0) {
showAuthNotification(account.mId, account.mEmailAddress);
}
return success;
}
}
private void showAuthNotification(long accountId, String accountName) {
final PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
createAccountSettingsIntent(accountId, accountName),
0);
final Notification notification = new Builder(this)
.setContentTitle(this.getString(string.auth_error_notification_title))
.setContentText(this.getString(
string.auth_error_notification_text, accountName))
.setSmallIcon(drawable.stat_notify_auth)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
final NotificationManager nm = (NotificationManager)
this.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify("AuthError", 0, notification);
}
/**
* Create and return an intent to display (and edit) settings for a specific account, or -1
* for any/all accounts. If an account name string is provided, a warning dialog will be
* displayed as well.
*/
public static Intent createAccountSettingsIntent(long accountId, String accountName) {
final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder(
IntentUtilities.PATH_SETTINGS);
IntentUtilities.setAccountId(builder, accountId);
IntentUtilities.setAccountName(builder, accountName);
return new Intent(Intent.ACTION_EDIT, builder.build());
}
/**
* Determine which content types are set to sync for an account.
* @param account The account whose sync settings we're looking for.
* @return The authorities for the content types we want to sync for account.
*/
private static HashSet<String> getAuthsToSync(final android.accounts.Account account) {
final HashSet<String> authsToSync = new HashSet();
if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) {
authsToSync.add(EmailContent.AUTHORITY);
}
if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) {
authsToSync.add(CalendarContract.AUTHORITY);
}
if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
authsToSync.add(ContactsContract.AUTHORITY);
}
return authsToSync;
}
}