Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2010 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.exchange.service;
     18 
     19 import android.app.AlarmManager;
     20 import android.app.Notification;
     21 import android.app.Notification.Builder;
     22 import android.app.NotificationManager;
     23 import android.app.PendingIntent;
     24 import android.content.AbstractThreadedSyncAdapter;
     25 import android.content.ComponentName;
     26 import android.content.ContentProviderClient;
     27 import android.content.ContentResolver;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.ServiceConnection;
     32 import android.content.SyncResult;
     33 import android.database.Cursor;
     34 import android.net.Uri;
     35 import android.os.AsyncTask;
     36 import android.os.Bundle;
     37 import android.os.IBinder;
     38 import android.os.RemoteException;
     39 import android.os.SystemClock;
     40 import android.provider.CalendarContract;
     41 import android.provider.ContactsContract;
     42 import android.text.TextUtils;
     43 import android.text.format.DateUtils;
     44 import android.util.Log;
     45 
     46 import com.android.emailcommon.TempDirectory;
     47 import com.android.emailcommon.provider.Account;
     48 import com.android.emailcommon.provider.EmailContent;
     49 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     50 import com.android.emailcommon.provider.EmailContent.Message;
     51 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     52 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     53 import com.android.emailcommon.provider.HostAuth;
     54 import com.android.emailcommon.provider.Mailbox;
     55 import com.android.emailcommon.service.EmailServiceStatus;
     56 import com.android.emailcommon.service.IEmailService;
     57 import com.android.emailcommon.service.IEmailServiceCallback;
     58 import com.android.emailcommon.service.SearchParams;
     59 import com.android.emailcommon.service.ServiceProxy;
     60 import com.android.emailcommon.utility.IntentUtilities;
     61 import com.android.emailcommon.utility.Utility;
     62 import com.android.exchange.Eas;
     63 import com.android.exchange.R.drawable;
     64 import com.android.exchange.R.string;
     65 import com.android.exchange.adapter.PingParser;
     66 import com.android.exchange.eas.EasSyncContacts;
     67 import com.android.exchange.eas.EasSyncCalendar;
     68 import com.android.exchange.eas.EasFolderSync;
     69 import com.android.exchange.eas.EasLoadAttachment;
     70 import com.android.exchange.eas.EasMoveItems;
     71 import com.android.exchange.eas.EasOperation;
     72 import com.android.exchange.eas.EasOutboxSync;
     73 import com.android.exchange.eas.EasPing;
     74 import com.android.exchange.eas.EasSearch;
     75 import com.android.exchange.eas.EasSync;
     76 import com.android.exchange.eas.EasSyncBase;
     77 import com.android.mail.providers.UIProvider;
     78 import com.android.mail.utils.LogUtils;
     79 
     80 import java.util.HashMap;
     81 import java.util.HashSet;
     82 
     83 /**
     84  * Service for communicating with Exchange servers. There are three main parts of this class:
     85  * TODO: Flesh out these comments.
     86  * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
     87  * 2) Bookkeeping for running Ping requests, which handles push notifications.
     88  * 3) An {@link IEmailService} Stub to handle RPC from the UI.
     89  */
     90 public class EmailSyncAdapterService extends AbstractSyncAdapterService {
     91 
     92     private static final String TAG = Eas.LOG_TAG;
     93 
     94     /**
     95      * Temporary while converting to EasService. Do not check in set to true.
     96      * When true, delegates various operations to {@link EasService}, for use while developing the
     97      * new service.
     98      * The two following fields are used to support what happens when this is true.
     99      */
    100     private static final boolean DELEGATE_TO_EAS_SERVICE = false;
    101     private IEmailService mEasService;
    102     private ServiceConnection mConnection;
    103 
    104     private static final String EXTRA_START_PING = "START_PING";
    105     private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
    106     private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS;
    107 
    108     /**
    109      * The amount of time between periodic syncs intended to ensure that push hasn't died.
    110      */
    111     private static final long KICK_SYNC_INTERVAL =
    112             DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
    113 
    114     /** Controls whether we do a periodic "kick" to restart the ping. */
    115     private static final boolean SCHEDULE_KICK = true;
    116 
    117     /** Projection used for getting email address for an account. */
    118     private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
    119 
    120     private static final Object sSyncAdapterLock = new Object();
    121     private static AbstractThreadedSyncAdapter sSyncAdapter = null;
    122 
    123     // Value for a message's server id when sending fails.
    124     public static final int SEND_FAILED = 1;
    125     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
    126             MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
    127             SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
    128 
    129     /**
    130      * Bookkeeping for handling synchronization between pings and syncs.
    131      * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
    132      * the term for the Exchange command, but this code should be generic enough to be easily
    133      * extended to IMAP.
    134      * "Sync" refers to an actual sync command to either fetch mail state, account state, or send
    135      * mail (send is implemented as "sync the outbox").
    136      * TODO: Outbox sync probably need not stop a ping in progress.
    137      * Basic rules of how these interact (note that all rules are per account):
    138      * - Only one ping or sync may run at a time.
    139      * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
    140      *   a sync is in progress.
    141      * - On the other hand, ping requests may come in while handling a ping.
    142      * - "Ping request" is shorthand for "a request to change our ping parameters", which includes
    143      *   a request to stop receiving push notifications.
    144      * - If neither a ping nor a sync is running, then a request for either will run it.
    145      * - If a sync is running, new ping requests block until the sync completes.
    146      * - If a ping is running, a new sync request stops the ping and creates a pending ping
    147      *   (which blocks until the sync completes).
    148      * - If a ping is running, a new ping request stops the ping and either starts a new one or
    149      *   does nothing, as appopriate (since a ping request can be to stop pushing).
    150      * - As an optimization, while a ping request is waiting to run, subsequent ping requests are
    151      *   ignored (the pending ping will pick up the latest ping parameters at the time it runs).
    152      */
    153     public class SyncHandlerSynchronizer {
    154         /**
    155          * Map of account id -> ping handler.
    156          * For a given account id, there are three possible states:
    157          * 1) If no ping or sync is currently running, there is no entry in the map for the account.
    158          * 2) If a ping is running, there is an entry with the appropriate ping handler.
    159          * 3) If there is a sync running, there is an entry with null as the value.
    160          * We cannot have more than one ping or sync running at a time.
    161          */
    162         private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>();
    163 
    164         /**
    165          * Wait until neither a sync nor a ping is running on this account, and then return.
    166          * If there's a ping running, actively stop it. (For syncs, we have to just wait.)
    167          * @param accountId The account we want to wait for.
    168          */
    169         private synchronized void waitUntilNoActivity(final long accountId) {
    170             while (mPingHandlers.containsKey(accountId)) {
    171                 final PingTask pingHandler = mPingHandlers.get(accountId);
    172                 if (pingHandler != null) {
    173                     pingHandler.stop();
    174                 }
    175                 try {
    176                     wait();
    177                 } catch (final InterruptedException e) {
    178                     // TODO: When would this happen, and how should I handle it?
    179                 }
    180             }
    181         }
    182 
    183         /**
    184          * Use this to see if we're currently syncing, as opposed to pinging or doing nothing.
    185          * @param accountId The account to check.
    186          * @return Whether that account is currently running a sync.
    187          */
    188         private synchronized boolean isRunningSync(final long accountId) {
    189             return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null);
    190         }
    191 
    192         /**
    193          * If there are no running pings, stop the service.
    194          */
    195         private void stopServiceIfNoPings() {
    196             for (final PingTask pingHandler : mPingHandlers.values()) {
    197                 if (pingHandler != null) {
    198                     return;
    199                 }
    200             }
    201             EmailSyncAdapterService.this.stopSelf();
    202         }
    203 
    204         /**
    205          * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync
    206          * here; the caller must do that.
    207          * @param accountId The account on which we are running a sync.
    208          */
    209         public synchronized void startSync(final long accountId) {
    210             waitUntilNoActivity(accountId);
    211             mPingHandlers.put(accountId, null);
    212         }
    213 
    214         /**
    215          * Starts or restarts a ping for an account, if the current account state indicates that it
    216          * wants to push.
    217          * @param account The account whose ping is being modified.
    218          */
    219         public synchronized void modifyPing(final boolean lastSyncHadError,
    220                 final Account account) {
    221             // If a sync is currently running, it will start a ping when it's done, so there's no
    222             // need to do anything right now.
    223             if (isRunningSync(account.mId)) {
    224                 return;
    225             }
    226 
    227             // Don't ping if we're on security hold.
    228             if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    229                 return;
    230             }
    231 
    232             // Don't ping for accounts that haven't performed initial sync.
    233             if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
    234                 return;
    235             }
    236 
    237             // Determine if this account needs pushes. All of the following must be true:
    238             // - The account's sync interval must indicate that it wants push.
    239             // - At least one content type must be sync-enabled in the account manager.
    240             // - At least one mailbox of a sync-enabled type must have automatic sync enabled.
    241             final EmailSyncAdapterService service = EmailSyncAdapterService.this;
    242             final android.accounts.Account amAccount = new android.accounts.Account(
    243                             account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    244             boolean pushNeeded = false;
    245             if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
    246                 final HashSet<String> authsToSync = getAuthsToSync(amAccount);
    247                 // If we have at least one sync-enabled content type, check for syncing mailboxes.
    248                 if (!authsToSync.isEmpty()) {
    249                     final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(),
    250                             account.mId);
    251                     if (c != null) {
    252                         try {
    253                             while (c.moveToNext()) {
    254                                 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
    255                                 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
    256                                     pushNeeded = true;
    257                                     break;
    258                                 }
    259                             }
    260                         } finally {
    261                             c.close();
    262                         }
    263                     }
    264                 }
    265             }
    266 
    267             // Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync.
    268             final PingTask pingSyncHandler = mPingHandlers.get(account.mId);
    269             final Bundle extras = new Bundle(1);
    270             extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
    271             if (pushNeeded) {
    272                 // First start or restart the ping as appropriate.
    273                 if (pingSyncHandler != null) {
    274                     pingSyncHandler.restart();
    275                 } else {
    276                     if (lastSyncHadError) {
    277                         // Schedule an alarm to set up the ping in 5 minutes
    278                         scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
    279                     } else {
    280                         // Start a new ping.
    281                         // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
    282                         // If we return before the ping starts, there's a race condition where
    283                         // another ping or sync might start first. It only works for startSync
    284                         // because sync is higher priority than ping (i.e. a ping can't start while
    285                         // a sync is pending) and only one sync can run at a time.
    286                         final PingTask pingHandler = new PingTask(service, account, amAccount,
    287                                 this);
    288                         mPingHandlers.put(account.mId, pingHandler);
    289                         pingHandler.start();
    290                         // Whenever we have a running ping, make sure this service stays running.
    291                         service.startService(new Intent(service, EmailSyncAdapterService.class));
    292                     }
    293                 }
    294                 if (SCHEDULE_KICK) {
    295                     ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
    296                                KICK_SYNC_INTERVAL);
    297                 }
    298             } else {
    299                 if (pingSyncHandler != null) {
    300                     pingSyncHandler.stop();
    301                 }
    302                 if (SCHEDULE_KICK) {
    303                     ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras);
    304                 }
    305             }
    306         }
    307 
    308         /**
    309          * Updates the synchronization bookkeeping when a sync is done.
    310          * @param account The account whose sync just finished.
    311          */
    312         public synchronized void syncComplete(final boolean lastSyncHadError,
    313                 final Account account) {
    314             LogUtils.d(TAG, "syncComplete, err: " + lastSyncHadError);
    315             mPingHandlers.remove(account.mId);
    316             // Syncs can interrupt pings, so we should check if we need to start one now.
    317             // If the last sync had a fatal error, we will not immediately recreate the ping.
    318             // Instead, we'll set an alarm that will restart them in a few minutes. This prevents
    319             // a battery draining spin if there is some kind of protocol error or other
    320             // non-transient failure. (Actually, immediately pinging even for a transient error
    321             // isn't great)
    322             modifyPing(lastSyncHadError, account);
    323             stopServiceIfNoPings();
    324             notifyAll();
    325         }
    326 
    327         /**
    328          * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only
    329          * sync if necessary.
    330          * @param amAccount The {@link android.accounts.Account} for this account.
    331          * @param accountId The account whose ping just finished.
    332          * @param pingStatus The status value from {@link PingParser} for the last ping performed.
    333          *                   This cannot be one of the values that results in another ping, so this
    334          *                   function only needs to handle the terminal statuses.
    335          */
    336         public synchronized void pingComplete(final android.accounts.Account amAccount,
    337                 final long accountId, final int pingStatus) {
    338             mPingHandlers.remove(accountId);
    339 
    340             // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
    341             // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
    342 
    343             if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE ||
    344                     pingStatus == EasOperation.RESULT_OTHER_FAILURE) {
    345                 // TODO: Sticky problem here: we necessarily aren't in a sync, so it's impossible to
    346                 // signal the error to the SyncManager and take advantage of backoff there. Worse,
    347                 // the current mechanism for how we do this will just encourage spammy requests
    348                 // since the actual ping-only sync request ALWAYS succeeds.
    349                 // So for now, let's delay a bit before asking the SyncManager to perform the sync.
    350                 // Longer term, this should be incorporated into some form of backoff, either
    351                 // by integrating with the SyncManager more fully or by implementing a Ping-specific
    352                 // backoff mechanism (e.g. integrate this with the logic for ping duration).
    353                 LogUtils.e(TAG, "Ping for account %d completed with error %d, delaying next ping",
    354                         accountId, pingStatus);
    355                 scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS);
    356             } else {
    357                 stopServiceIfNoPings();
    358             }
    359 
    360             // TODO: It might be the case that only STATUS_CHANGES_FOUND and
    361             // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through.
    362             notifyAll();
    363         }
    364 
    365     }
    366     private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
    367 
    368     /**
    369      * The binder for IEmailService.
    370      */
    371     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
    372 
    373         private String getEmailAddressForAccount(final long accountId) {
    374             final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
    375                     Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
    376                     new String[] {Long.toString(accountId)}, null, 0);
    377             if (emailAddress == null) {
    378                 LogUtils.e(TAG, "Could not find email address for account %d", accountId);
    379             }
    380             return emailAddress;
    381         }
    382 
    383         @Override
    384         public Bundle validate(final HostAuth hostAuth) {
    385             LogUtils.d(TAG, "IEmailService.validate");
    386             if (mEasService != null) {
    387                 try {
    388                     return mEasService.validate(hostAuth);
    389                 } catch (final RemoteException re) {
    390                     LogUtils.e(TAG, re, "While asking EasService to handle validate");
    391                 }
    392             }
    393             return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate();
    394         }
    395 
    396         @Override
    397         public Bundle autoDiscover(final String username, final String password) {
    398             LogUtils.d(TAG, "IEmailService.autoDiscover");
    399             return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
    400                     .doAutodiscover();
    401         }
    402 
    403         @Override
    404         public void updateFolderList(final long accountId) {
    405             LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
    406             if (mEasService != null) {
    407                 try {
    408                     mEasService.updateFolderList(accountId);
    409                     return;
    410                 } catch (final RemoteException re) {
    411                     LogUtils.e(TAG, re, "While asking EasService to updateFolderList");
    412                 }
    413             }
    414             final String emailAddress = getEmailAddressForAccount(accountId);
    415             if (emailAddress != null) {
    416                 final Bundle extras = new Bundle(1);
    417                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
    418                 ContentResolver.requestSync(new android.accounts.Account(
    419                         emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
    420                         EmailContent.AUTHORITY, extras);
    421             }
    422         }
    423 
    424         @Override
    425         public void setLogging(final int flags) {
    426             // TODO: fix this?
    427             // Protocol logging
    428             Eas.setUserDebug(flags);
    429             // Sync logging
    430             //setUserDebug(flags);
    431         }
    432 
    433         @Override
    434         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
    435                 final long attachmentId, final boolean background) {
    436             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
    437             // TODO: Prevent this from happening in parallel with a sync?
    438             final EasLoadAttachment operation = new EasLoadAttachment(EmailSyncAdapterService.this,
    439                     accountId, attachmentId, callback);
    440             operation.performOperation();
    441         }
    442 
    443         @Override
    444         public void sendMeetingResponse(final long messageId, final int response) {
    445             LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
    446             EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
    447                     response);
    448         }
    449 
    450         /**
    451          * Delete PIM (calendar, contacts) data for the specified account
    452          *
    453          * @param emailAddress the email address for the account whose data should be deleted
    454          */
    455         @Override
    456         public void deleteAccountPIMData(final String emailAddress) {
    457             LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
    458             if (emailAddress != null) {
    459                 final Context context = EmailSyncAdapterService.this;
    460                 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress);
    461                 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress);
    462             }
    463             // TODO: Run account reconciler?
    464         }
    465 
    466         @Override
    467         public int searchMessages(final long accountId, final SearchParams searchParams,
    468                 final long destMailboxId) {
    469             LogUtils.d(TAG, "IEmailService.searchMessages");
    470             final EasSearch operation = new EasSearch(EmailSyncAdapterService.this, accountId,
    471                     searchParams, destMailboxId);
    472             operation.performOperation();
    473             return operation.getTotalResults();
    474             // TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
    475         }
    476 
    477         @Override
    478         public void sendMail(final long accountId) {}
    479 
    480         @Override
    481         public void pushModify(final long accountId) {
    482             LogUtils.d(TAG, "IEmailService.pushModify");
    483             if (mEasService != null) {
    484                 try {
    485                     mEasService.pushModify(accountId);
    486                     return;
    487                 } catch (final RemoteException re) {
    488                     LogUtils.e(TAG, re, "While asking EasService to handle pushModify");
    489                 }
    490             }
    491             final Account account = Account.restoreAccountWithId(EmailSyncAdapterService.this,
    492                     accountId);
    493             if (account != null) {
    494                 mSyncHandlerMap.modifyPing(false, account);
    495             }
    496         }
    497 
    498         @Override
    499         public void sync(final long accountId, final boolean updateFolderList,
    500                 final int mailboxType, final long[] folders) {}
    501     };
    502 
    503     public EmailSyncAdapterService() {
    504         super();
    505     }
    506 
    507     /**
    508      * {@link AsyncTask} for restarting pings for all accounts that need it.
    509      */
    510     private static final String PUSH_ACCOUNTS_SELECTION =
    511             AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
    512     private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
    513 
    514         private final ContentResolver mContentResolver;
    515         private final SyncHandlerSynchronizer mSyncHandlerMap;
    516         private boolean mAnyAccounts;
    517 
    518         public RestartPingsTask(final ContentResolver contentResolver,
    519                 final SyncHandlerSynchronizer syncHandlerMap) {
    520             mContentResolver = contentResolver;
    521             mSyncHandlerMap = syncHandlerMap;
    522         }
    523 
    524         @Override
    525         protected Void doInBackground(Void... params) {
    526             final Cursor c = mContentResolver.query(Account.CONTENT_URI,
    527                     Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
    528             if (c != null) {
    529                 try {
    530                     mAnyAccounts = (c.getCount() != 0);
    531                     while (c.moveToNext()) {
    532                         final Account account = new Account();
    533                         account.restore(c);
    534                         mSyncHandlerMap.modifyPing(false, account);
    535                     }
    536                 } finally {
    537                     c.close();
    538                 }
    539             } else {
    540                 mAnyAccounts = false;
    541             }
    542             return null;
    543         }
    544 
    545         @Override
    546         protected void onPostExecute(Void result) {
    547             if (!mAnyAccounts) {
    548                 LogUtils.d(TAG, "stopping for no accounts");
    549                 EmailSyncAdapterService.this.stopSelf();
    550             }
    551         }
    552     }
    553 
    554     @Override
    555     public void onCreate() {
    556         LogUtils.v(TAG, "onCreate()");
    557         super.onCreate();
    558         startService(new Intent(this, EmailSyncAdapterService.class));
    559         // Restart push for all accounts that need it.
    560         new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
    561                 AsyncTask.THREAD_POOL_EXECUTOR);
    562         if (DELEGATE_TO_EAS_SERVICE) {
    563             // TODO: This block is temporary to support the transition to EasService.
    564             mConnection = new ServiceConnection() {
    565                 @Override
    566                 public void onServiceConnected(ComponentName name,  IBinder binder) {
    567                     mEasService = IEmailService.Stub.asInterface(binder);
    568                 }
    569 
    570                 @Override
    571                 public void onServiceDisconnected(ComponentName name) {
    572                     mEasService = null;
    573                 }
    574             };
    575             bindService(new Intent(this, EasService.class), mConnection, Context.BIND_AUTO_CREATE);
    576         }
    577     }
    578 
    579     @Override
    580     public void onDestroy() {
    581         LogUtils.v(TAG, "onDestroy()");
    582         super.onDestroy();
    583         for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) {
    584             if (task != null) {
    585                 task.stop();
    586             }
    587         }
    588         if (DELEGATE_TO_EAS_SERVICE) {
    589             // TODO: This block is temporary to support the transition to EasService.
    590             unbindService(mConnection);
    591         }
    592     }
    593 
    594     @Override
    595     public IBinder onBind(Intent intent) {
    596         if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) {
    597             return mBinder;
    598         }
    599         return super.onBind(intent);
    600     }
    601 
    602     @Override
    603     public int onStartCommand(Intent intent, int flags, int startId) {
    604         if (intent != null &&
    605                 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
    606             if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
    607                 // We've been asked to forcibly shutdown. This happens if email accounts are
    608                 // deleted, otherwise we can get errors if services are still running for
    609                 // accounts that are now gone.
    610                 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
    611                 // if accounts disappear out from under us.
    612                 LogUtils.d(TAG, "Forced shutdown, killing process");
    613                 System.exit(-1);
    614             } else if (intent.getBooleanExtra(EXTRA_START_PING, false)) {
    615                 LogUtils.d(TAG, "Restarting ping from alarm");
    616                 // We've been woken up by an alarm to restart our ping. This happens if a sync
    617                 // fails, rather that instantly starting the ping, we'll hold off for a few minutes.
    618                 final android.accounts.Account account =
    619                         intent.getParcelableExtra(EXTRA_PING_ACCOUNT);
    620                 EasPing.requestPing(account);
    621             }
    622         }
    623         return super.onStartCommand(intent, flags, startId);
    624     }
    625 
    626     @Override
    627     protected AbstractThreadedSyncAdapter getSyncAdapter() {
    628         synchronized (sSyncAdapterLock) {
    629             if (sSyncAdapter == null) {
    630                 sSyncAdapter = new SyncAdapterImpl(this);
    631             }
    632             return sSyncAdapter;
    633         }
    634     }
    635 
    636     // TODO: Handle cancelSync() appropriately.
    637     private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
    638         public SyncAdapterImpl(Context context) {
    639             super(context, true /* autoInitialize */);
    640         }
    641 
    642         @Override
    643         public void onPerformSync(final android.accounts.Account acct, final Bundle extras,
    644                 final String authority, final ContentProviderClient provider,
    645                 final SyncResult syncResult) {
    646             if (LogUtils.isLoggable(TAG, Log.DEBUG)) {
    647                 LogUtils.d(TAG, "onPerformSync: %s, %s", acct.toString(), extras.toString());
    648             } else {
    649                 LogUtils.i(TAG, "onPerformSync: %s", extras.toString());
    650             }
    651             TempDirectory.setTempDirectory(EmailSyncAdapterService.this);
    652 
    653             // TODO: Perform any connectivity checks, bail early if we don't have proper network
    654             // for this sync operation.
    655 
    656             final Context context = getContext();
    657             final ContentResolver cr = context.getContentResolver();
    658 
    659             // Get the EmailContent Account
    660             final Account account;
    661             final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
    662                     AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null);
    663             try {
    664                 if (!accountCursor.moveToFirst()) {
    665                     // Could not load account.
    666                     // TODO: improve error handling.
    667                     LogUtils.w(TAG, "onPerformSync: could not load account");
    668                     return;
    669                 }
    670                 account = new Account();
    671                 account.restore(accountCursor);
    672             } finally {
    673                 accountCursor.close();
    674             }
    675 
    676             // Figure out what we want to sync, based on the extras and our account sync status.
    677             final boolean isInitialSync = EmailContent.isInitialSyncKey(account.mSyncKey);
    678             final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(extras);
    679             final int mailboxType = extras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE,
    680                     Mailbox.TYPE_NONE);
    681 
    682             // Push only means this sync request should only refresh the ping (either because
    683             // settings changed, or we need to restart it for some reason).
    684             final boolean pushOnly = Mailbox.isPushOnlyExtras(extras);
    685             // Account only means just do a FolderSync.
    686             final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras);
    687 
    688             // A "full sync" means that we didn't request a more specific type of sync.
    689             final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
    690                     mailboxType == Mailbox.TYPE_NONE);
    691 
    692             // A FolderSync is necessary for full sync, initial sync, and account only sync.
    693             final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
    694 
    695             // If we're just twiddling the push, we do the lightweight thing and bail early.
    696             if (pushOnly && !isFolderSync) {
    697                 LogUtils.d(TAG, "onPerformSync: mailbox push only");
    698                 if (mEasService != null) {
    699                     try {
    700                         mEasService.pushModify(account.mId);
    701                         return;
    702                     } catch (final RemoteException re) {
    703                         LogUtils.e(TAG, re, "While trying to pushModify within onPerformSync");
    704                     }
    705                 }
    706                 mSyncHandlerMap.modifyPing(false, account);
    707                 return;
    708             }
    709 
    710             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
    711             mSyncHandlerMap.startSync(account.mId);
    712             int operationResult = 0;
    713             try {
    714                 // Perform a FolderSync if necessary.
    715                 // TODO: We permit FolderSync even during security hold, because it's necessary to
    716                 // resolve some holds. Ideally we would only do it for the holds that require it.
    717                 if (isFolderSync) {
    718                     final EasFolderSync folderSync = new EasFolderSync(context, account);
    719                     operationResult = folderSync.doFolderSync();
    720                     if (operationResult < 0) {
    721                         return;
    722                     }
    723                 }
    724 
    725                 // Do not permit further syncs if we're on security hold.
    726                 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    727                     return;
    728                 }
    729 
    730                 // Perform email upsync for this account. Moves first, then state changes.
    731                 if (!isInitialSync) {
    732                     EasMoveItems move = new EasMoveItems(context, account);
    733                     operationResult = move.upsyncMovedMessages();
    734                     if (operationResult < 0) {
    735                         return;
    736                     }
    737 
    738                     // TODO: EasSync should eventually handle both up and down; for now, it's used
    739                     // purely for upsync.
    740                     EasSync upsync = new EasSync(context, account);
    741                     operationResult = upsync.upsync();
    742                     if (operationResult < 0) {
    743                         return;
    744                     }
    745                 }
    746 
    747                 if (mailboxIds != null) {
    748                     final boolean hasCallbackMethod =
    749                             extras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
    750                     // Sync the mailbox that was explicitly requested.
    751                     for (final long mailboxId : mailboxIds) {
    752                         if (hasCallbackMethod) {
    753                             EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
    754                                     EmailServiceStatus.IN_PROGRESS, 0,
    755                                     UIProvider.LastSyncResult.SUCCESS);
    756                         }
    757                         operationResult = syncMailbox(context, cr, acct, account, mailboxId,
    758                                 extras, syncResult, null, true);
    759                         if (hasCallbackMethod) {
    760                             EmailServiceStatus.syncMailboxStatus(cr, extras,
    761                                     mailboxId,EmailServiceStatus.SUCCESS, 0,
    762                                     EasOperation.translateSyncResultToUiResult(operationResult));
    763                         }
    764 
    765                         if (operationResult < 0) {
    766                             break;
    767                         }
    768                     }
    769                 } else if (!accountOnly && !pushOnly) {
    770                     // We have to sync multiple folders.
    771                     final Cursor c;
    772                     if (isFullSync) {
    773                         // Full account sync includes all mailboxes that participate in system sync.
    774                         c = Mailbox.getMailboxIdsForSync(cr, account.mId);
    775                     } else {
    776                         // Type-filtered sync should only get the mailboxes of a specific type.
    777                         c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType);
    778                     }
    779                     if (c != null) {
    780                         try {
    781                             final HashSet<String> authsToSync = getAuthsToSync(acct);
    782                             while (c.moveToNext()) {
    783                                 operationResult = syncMailbox(context, cr, acct, account,
    784                                         c.getLong(0), extras, syncResult, authsToSync, false);
    785                                 if (operationResult < 0) {
    786                                     break;
    787                                 }
    788                             }
    789                         } finally {
    790                             c.close();
    791                         }
    792                     }
    793                 }
    794             } finally {
    795                 // Clean up the bookkeeping, including restarting ping if necessary.
    796                 mSyncHandlerMap.syncComplete(syncResult.hasError(), account);
    797 
    798                 if (operationResult < 0) {
    799                     EasFolderSync.writeResultToSyncResult(operationResult, syncResult);
    800                     // If any operations had an auth error, notify the user.
    801                     // Note that provisioning errors should have already triggered the policy
    802                     // notification, so suppress those from showing the auth notification.
    803                     if (syncResult.stats.numAuthExceptions > 0 &&
    804                             operationResult != EasOperation.RESULT_PROVISIONING_ERROR) {
    805                         showAuthNotification(account.mId, account.mEmailAddress);
    806                     }
    807                 }
    808 
    809                 LogUtils.d(TAG, "onPerformSync: finished");
    810             }
    811         }
    812 
    813         /**
    814          * Update the mailbox's sync status with the provider and, if we're finished with the sync,
    815          * write the last sync time as well.
    816          * @param context Our {@link Context}.
    817          * @param mailbox The mailbox whose sync status to update.
    818          * @param cv A {@link ContentValues} object to use for updating the provider.
    819          * @param syncStatus The status for the current sync.
    820          */
    821         private void updateMailbox(final Context context, final Mailbox mailbox,
    822                 final ContentValues cv, final int syncStatus) {
    823             cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
    824             if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
    825                 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
    826             }
    827             mailbox.update(context, cv);
    828         }
    829 
    830         private int syncMailbox(final Context context, final ContentResolver cr,
    831                 final android.accounts.Account acct, final Account account, final long mailboxId,
    832                 final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync,
    833                 final boolean isMailboxSync) {
    834             final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
    835             if (mailbox == null) {
    836                 return EasSyncBase.RESULT_HARD_DATA_FAILURE;
    837             }
    838 
    839             if (mailbox.mAccountKey != account.mId) {
    840                 LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(),
    841                         extras.toString());
    842                 return EasSyncBase.RESULT_HARD_DATA_FAILURE;
    843             }
    844             if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
    845                 // We are asking for an account sync, but this mailbox type is not configured for
    846                 // sync. Do NOT treat this as a sync error for ping backoff purposes.
    847                 return EasSyncBase.RESULT_DONE;
    848             }
    849 
    850             if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
    851                 // TODO: Because we don't have bidirectional sync working, trying to downsync
    852                 // the drafts folder is confusing. b/11158759
    853                 // For now, just disable all syncing of DRAFTS type folders.
    854                 // Automatic syncing should always be disabled, but we also stop it here to ensure
    855                 // that we won't sync even if the user attempts to force a sync from the UI.
    856                 // Do NOT treat as a sync error for ping backoff purposes.
    857                 LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
    858                 return EasSyncBase.RESULT_DONE;
    859             }
    860 
    861             // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
    862             // treated as background syncs.
    863             // TODO: Push will be treated as "user" syncs, and probably should be background.
    864             if (mailbox.mType == Mailbox.TYPE_OUTBOX || mailbox.isSyncable()) {
    865                 final ContentValues cv = new ContentValues(2);
    866                 updateMailbox(context, mailbox, cv, isMailboxSync ?
    867                         EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
    868                 try {
    869                     if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
    870                         return syncOutbox(context, cr, account, mailbox);
    871                     }
    872                     final EasSyncBase operation = new EasSyncBase(context, account, mailbox);
    873                     return operation.performOperation();
    874                 } finally {
    875                     updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE);
    876                 }
    877             }
    878 
    879             return EasSyncBase.RESULT_DONE;
    880         }
    881     }
    882 
    883     private int syncOutbox(Context context, ContentResolver cr, Account account, Mailbox mailbox) {
    884         // Get a cursor to Outbox messages
    885         final Cursor c = cr.query(Message.CONTENT_URI,
    886                 Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
    887                 new String[] {Long.toString(mailbox.mId)}, null);
    888         try {
    889             // Loop through the messages, sending each one
    890             while (c.moveToNext()) {
    891                 final Message message = new Message();
    892                 message.restore(c);
    893                 if (Utility.hasUnloadedAttachments(context, message.mId)) {
    894                     // We'll just have to wait on this...
    895                     continue;
    896                 }
    897 
    898                 // TODO: Fix -- how do we want to signal to UI that we started syncing?
    899                 // Note the entire callback mechanism here needs improving.
    900                 //sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0);
    901 
    902                 EasOperation op = new EasOutboxSync(context, account, message, true);
    903                 int result = op.performOperation();
    904                 if (result == EasOutboxSync.RESULT_ITEM_NOT_FOUND) {
    905                     // This can happen if we are using smartReply, and the message we are referring
    906                     // to has disappeared from the server. Try again with smartReply disabled.
    907                     op = new EasOutboxSync(context, account, message, false);
    908                     result = op.performOperation();
    909                 }
    910                 // If we got some connection error or other fatal error, terminate the sync.
    911                 if (result != EasOutboxSync.RESULT_OK &&
    912                     result != EasOutboxSync.RESULT_NON_FATAL_ERROR &&
    913                     result > EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) {
    914                     LogUtils.w(TAG, "Aborting outbox sync for error %d", result);
    915                     return result;
    916                 }
    917             }
    918         } finally {
    919             // TODO: Some sort of sendMessageStatus() is needed here.
    920             c.close();
    921         }
    922         return EasOutboxSync.RESULT_OK;
    923     }
    924 
    925     private void showAuthNotification(long accountId, String accountName) {
    926         final PendingIntent pendingIntent = PendingIntent.getActivity(
    927                 this,
    928                 0,
    929                 createAccountSettingsIntent(accountId, accountName),
    930                 0);
    931 
    932         final Notification notification = new Builder(this)
    933                 .setContentTitle(this.getString(string.auth_error_notification_title))
    934                 .setContentText(this.getString(
    935                         string.auth_error_notification_text, accountName))
    936                 .setSmallIcon(drawable.stat_notify_auth)
    937                 .setContentIntent(pendingIntent)
    938                 .setAutoCancel(true)
    939                 .build();
    940 
    941         final NotificationManager nm = (NotificationManager)
    942                 this.getSystemService(Context.NOTIFICATION_SERVICE);
    943         nm.notify("AuthError", 0, notification);
    944     }
    945 
    946     /**
    947      * Create and return an intent to display (and edit) settings for a specific account, or -1
    948      * for any/all accounts.  If an account name string is provided, a warning dialog will be
    949      * displayed as well.
    950      */
    951     public static Intent createAccountSettingsIntent(long accountId, String accountName) {
    952         final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder(
    953                 IntentUtilities.PATH_SETTINGS);
    954         IntentUtilities.setAccountId(builder, accountId);
    955         IntentUtilities.setAccountName(builder, accountName);
    956         return new Intent(Intent.ACTION_EDIT, builder.build());
    957     }
    958 
    959     /**
    960      * Determine which content types are set to sync for an account.
    961      * @param account The account whose sync settings we're looking for.
    962      * @return The authorities for the content types we want to sync for account.
    963      */
    964     private static HashSet<String> getAuthsToSync(final android.accounts.Account account) {
    965         final HashSet<String> authsToSync = new HashSet();
    966         if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) {
    967             authsToSync.add(EmailContent.AUTHORITY);
    968         }
    969         if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) {
    970             authsToSync.add(CalendarContract.AUTHORITY);
    971         }
    972         if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
    973             authsToSync.add(ContactsContract.AUTHORITY);
    974         }
    975         return authsToSync;
    976     }
    977 
    978     /**
    979      * Schedule to have a ping start some time in the future. This is used when we encounter an
    980      * error, and properly should be a more full featured back-off, but for the short run, just
    981      * waiting a few minutes at least avoids burning battery.
    982      * @param amAccount The account that needs to be pinged.
    983      * @param delay The time in milliseconds to wait before requesting the ping-only sync. Note that
    984      *              it may take longer than this before the ping actually happens, since there's two
    985      *              layers of waiting ({@link AlarmManager} can choose to wait longer, as can the
    986      *              SyncManager).
    987      */
    988     private void scheduleDelayedPing(final android.accounts.Account amAccount, final long delay) {
    989         final Intent intent = new Intent(this, EmailSyncAdapterService.class);
    990         intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
    991         intent.putExtra(EXTRA_START_PING, true);
    992         intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
    993         final PendingIntent pi = PendingIntent.getService(this, 0, intent,
    994                 PendingIntent.FLAG_ONE_SHOT);
    995         final AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    996         final long atTime = SystemClock.elapsedRealtime() + delay;
    997         am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
    998     }
    999 }
   1000