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.ContentProviderClient;
     26 import android.content.ContentResolver;
     27 import android.content.ContentValues;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.SyncResult;
     31 import android.database.Cursor;
     32 import android.net.Uri;
     33 import android.os.AsyncTask;
     34 import android.os.Bundle;
     35 import android.os.IBinder;
     36 import android.os.SystemClock;
     37 import android.provider.CalendarContract;
     38 import android.provider.ContactsContract;
     39 import android.text.TextUtils;
     40 import android.text.format.DateUtils;
     41 import android.util.Log;
     42 
     43 import com.android.emailcommon.Api;
     44 import com.android.emailcommon.TempDirectory;
     45 import com.android.emailcommon.provider.Account;
     46 import com.android.emailcommon.provider.EmailContent;
     47 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     48 import com.android.emailcommon.provider.HostAuth;
     49 import com.android.emailcommon.provider.Mailbox;
     50 import com.android.emailcommon.service.EmailServiceStatus;
     51 import com.android.emailcommon.service.IEmailService;
     52 import com.android.emailcommon.service.IEmailServiceCallback;
     53 import com.android.emailcommon.service.SearchParams;
     54 import com.android.emailcommon.service.ServiceProxy;
     55 import com.android.emailcommon.utility.IntentUtilities;
     56 import com.android.emailcommon.utility.Utility;
     57 import com.android.exchange.Eas;
     58 import com.android.exchange.R.drawable;
     59 import com.android.exchange.R.string;
     60 import com.android.exchange.adapter.PingParser;
     61 import com.android.exchange.adapter.Search;
     62 import com.android.exchange.eas.EasFolderSync;
     63 import com.android.exchange.eas.EasMoveItems;
     64 import com.android.exchange.eas.EasOperation;
     65 import com.android.exchange.eas.EasPing;
     66 import com.android.exchange.eas.EasSync;
     67 import com.android.mail.providers.UIProvider;
     68 import com.android.mail.providers.UIProvider.AccountCapabilities;
     69 import com.android.mail.utils.LogUtils;
     70 
     71 import java.util.HashMap;
     72 import java.util.HashSet;
     73 
     74 /**
     75  * Service for communicating with Exchange servers. There are three main parts of this class:
     76  * TODO: Flesh out these comments.
     77  * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs.
     78  * 2) Bookkeeping for running Ping requests, which handles push notifications.
     79  * 3) An {@link IEmailService} Stub to handle RPC from the UI.
     80  */
     81 public class EmailSyncAdapterService extends AbstractSyncAdapterService {
     82 
     83     private static final String TAG = Eas.LOG_TAG;
     84 
     85     private static final String EXTRA_START_PING = "START_PING";
     86     private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT";
     87     private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS;
     88 
     89     /**
     90      * The amount of time between periodic syncs intended to ensure that push hasn't died.
     91      */
     92     private static final long KICK_SYNC_INTERVAL =
     93             DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS;
     94 
     95     /** Controls whether we do a periodic "kick" to restart the ping. */
     96     private static final boolean SCHEDULE_KICK = true;
     97 
     98     /**
     99      * If sync extras do not include a mailbox id, then we want to perform a full sync.
    100      */
    101     private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX;
    102 
    103     /** Projection used for getting email address for an account. */
    104     private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };
    105 
    106     private static final Object sSyncAdapterLock = new Object();
    107     private static AbstractThreadedSyncAdapter sSyncAdapter = null;
    108 
    109     /**
    110      * Bookkeeping for handling synchronization between pings and syncs.
    111      * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is
    112      * the term for the Exchange command, but this code should be generic enough to be easily
    113      * extended to IMAP.
    114      * "Sync" refers to an actual sync command to either fetch mail state, account state, or send
    115      * mail (send is implemented as "sync the outbox").
    116      * TODO: Outbox sync probably need not stop a ping in progress.
    117      * Basic rules of how these interact (note that all rules are per account):
    118      * - Only one ping or sync may run at a time.
    119      * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while
    120      *   a sync is in progress.
    121      * - On the other hand, ping requests may come in while handling a ping.
    122      * - "Ping request" is shorthand for "a request to change our ping parameters", which includes
    123      *   a request to stop receiving push notifications.
    124      * - If neither a ping nor a sync is running, then a request for either will run it.
    125      * - If a sync is running, new ping requests block until the sync completes.
    126      * - If a ping is running, a new sync request stops the ping and creates a pending ping
    127      *   (which blocks until the sync completes).
    128      * - If a ping is running, a new ping request stops the ping and either starts a new one or
    129      *   does nothing, as appopriate (since a ping request can be to stop pushing).
    130      * - As an optimization, while a ping request is waiting to run, subsequent ping requests are
    131      *   ignored (the pending ping will pick up the latest ping parameters at the time it runs).
    132      */
    133     public class SyncHandlerSynchronizer {
    134         /**
    135          * Map of account id -> ping handler.
    136          * For a given account id, there are three possible states:
    137          * 1) If no ping or sync is currently running, there is no entry in the map for the account.
    138          * 2) If a ping is running, there is an entry with the appropriate ping handler.
    139          * 3) If there is a sync running, there is an entry with null as the value.
    140          * We cannot have more than one ping or sync running at a time.
    141          */
    142         private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>();
    143 
    144         /**
    145          * Wait until neither a sync nor a ping is running on this account, and then return.
    146          * If there's a ping running, actively stop it. (For syncs, we have to just wait.)
    147          * @param accountId The account we want to wait for.
    148          */
    149         private synchronized void waitUntilNoActivity(final long accountId) {
    150             while (mPingHandlers.containsKey(accountId)) {
    151                 final PingTask pingHandler = mPingHandlers.get(accountId);
    152                 if (pingHandler != null) {
    153                     pingHandler.stop();
    154                 }
    155                 try {
    156                     wait();
    157                 } catch (final InterruptedException e) {
    158                     // TODO: When would this happen, and how should I handle it?
    159                 }
    160             }
    161         }
    162 
    163         /**
    164          * Use this to see if we're currently syncing, as opposed to pinging or doing nothing.
    165          * @param accountId The account to check.
    166          * @return Whether that account is currently running a sync.
    167          */
    168         private synchronized boolean isRunningSync(final long accountId) {
    169             return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null);
    170         }
    171 
    172         /**
    173          * If there are no running pings, stop the service.
    174          */
    175         private void stopServiceIfNoPings() {
    176             for (final PingTask pingHandler : mPingHandlers.values()) {
    177                 if (pingHandler != null) {
    178                     return;
    179                 }
    180             }
    181             EmailSyncAdapterService.this.stopSelf();
    182         }
    183 
    184         /**
    185          * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync
    186          * here; the caller must do that.
    187          * @param accountId The account on which we are running a sync.
    188          */
    189         public synchronized void startSync(final long accountId) {
    190             waitUntilNoActivity(accountId);
    191             mPingHandlers.put(accountId, null);
    192         }
    193 
    194         /**
    195          * Starts or restarts a ping for an account, if the current account state indicates that it
    196          * wants to push.
    197          * @param account The account whose ping is being modified.
    198          */
    199         public synchronized void modifyPing(final boolean lastSyncHadError,
    200                 final Account account) {
    201             // If a sync is currently running, it will start a ping when it's done, so there's no
    202             // need to do anything right now.
    203             if (isRunningSync(account.mId)) {
    204                 return;
    205             }
    206 
    207             // Don't ping if we're on security hold.
    208             if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    209                 return;
    210             }
    211 
    212             // Don't ping for accounts that haven't performed initial sync.
    213             if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
    214                 return;
    215             }
    216 
    217             // Determine if this account needs pushes. All of the following must be true:
    218             // - The account's sync interval must indicate that it wants push.
    219             // - At least one content type must be sync-enabled in the account manager.
    220             // - At least one mailbox of a sync-enabled type must have automatic sync enabled.
    221             final EmailSyncAdapterService service = EmailSyncAdapterService.this;
    222             final android.accounts.Account amAccount = new android.accounts.Account(
    223                             account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    224             boolean pushNeeded = false;
    225             if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
    226                 final HashSet<String> authsToSync = getAuthsToSync(amAccount);
    227                 // If we have at least one sync-enabled content type, check for syncing mailboxes.
    228                 if (!authsToSync.isEmpty()) {
    229                     final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(),
    230                             account.mId);
    231                     if (c != null) {
    232                         try {
    233                             while (c.moveToNext()) {
    234                                 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
    235                                 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
    236                                     pushNeeded = true;
    237                                     break;
    238                                 }
    239                             }
    240                         } finally {
    241                             c.close();
    242                         }
    243                     }
    244                 }
    245             }
    246 
    247             // Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync.
    248             final PingTask pingSyncHandler = mPingHandlers.get(account.mId);
    249             final Bundle extras = new Bundle(1);
    250             extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true);
    251             if (pushNeeded) {
    252                 // First start or restart the ping as appropriate.
    253                 if (pingSyncHandler != null) {
    254                     pingSyncHandler.restart();
    255                 } else {
    256                     // Start a new ping.
    257                     // Note: unlike startSync, we CANNOT allow the caller to do the actual work.
    258                     // If we return before the ping starts, there's a race condition where another
    259                     // ping or sync might start first. It only works for startSync because sync is
    260                     // higher priority than ping (i.e. a ping can't start while a sync is pending)
    261                     // and only one sync can run at a time.
    262                     if (lastSyncHadError) {
    263                         // Schedule an alarm to set up the ping in 5 minutes
    264                         final Intent intent = new Intent(service, EmailSyncAdapterService.class);
    265                         intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
    266                         intent.putExtra(EXTRA_START_PING, true);
    267                         intent.putExtra(EXTRA_PING_ACCOUNT, amAccount);
    268                         final PendingIntent pi = PendingIntent.getService(
    269                                 EmailSyncAdapterService.this, 0, intent,
    270                                 PendingIntent.FLAG_ONE_SHOT);
    271                         final AlarmManager am = (AlarmManager)getSystemService(
    272                                 Context.ALARM_SERVICE);
    273                         final long atTime = SystemClock.elapsedRealtime() +
    274                                 SYNC_ERROR_BACKOFF_MILLIS;
    275                         am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi);
    276                     } else {
    277                         final PingTask pingHandler = new PingTask(service, account, amAccount,
    278                                 this);
    279                         mPingHandlers.put(account.mId, pingHandler);
    280                         pingHandler.start();
    281                         // Whenever we have a running ping, make sure this service stays running.
    282                         service.startService(new Intent(service, EmailSyncAdapterService.class));
    283                     }
    284                 }
    285                 if (SCHEDULE_KICK) {
    286                     ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras,
    287                                KICK_SYNC_INTERVAL);
    288                 }
    289             } else {
    290                 if (pingSyncHandler != null) {
    291                     pingSyncHandler.stop();
    292                 }
    293                 if (SCHEDULE_KICK) {
    294                     ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras);
    295                 }
    296             }
    297         }
    298 
    299         /**
    300          * Updates the synchronization bookkeeping when a sync is done.
    301          * @param account The account whose sync just finished.
    302          */
    303         public synchronized void syncComplete(final boolean lastSyncHadError,
    304                 final Account account) {
    305             LogUtils.d(TAG, "syncComplete, err: " + lastSyncHadError);
    306             mPingHandlers.remove(account.mId);
    307             // Syncs can interrupt pings, so we should check if we need to start one now.
    308             // If the last sync had a fatal error, we will not immediately recreate the ping.
    309             // Instead, we'll set an alarm that will restart them in a few minutes. This prevents
    310             // a battery draining spin if there is some kind of protocol error or other
    311             // non-transient failure. (Actually, immediately pinging even for a transient error
    312             // isn't great)
    313             modifyPing(lastSyncHadError, account);
    314             stopServiceIfNoPings();
    315             notifyAll();
    316         }
    317 
    318         /**
    319          * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only
    320          * sync if necessary.
    321          * @param amAccount The {@link android.accounts.Account} for this account.
    322          * @param accountId The account whose ping just finished.
    323          * @param pingStatus The status value from {@link PingParser} for the last ping performed.
    324          *                   This cannot be one of the values that results in another ping, so this
    325          *                   function only needs to handle the terminal statuses.
    326          */
    327         public synchronized void pingComplete(final android.accounts.Account amAccount,
    328                 final long accountId, final int pingStatus) {
    329             mPingHandlers.remove(accountId);
    330 
    331             // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI.
    332             // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI.
    333 
    334             // TODO: Should this just re-request ping if status < 0? This would do the wrong thing
    335             // for e.g. auth errors, though.
    336             if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE ||
    337                     pingStatus == EasOperation.RESULT_OTHER_FAILURE) {
    338                 // Request a new ping through the SyncManager. This will do the right thing if the
    339                 // exception was due to loss of network connectivity, etc. (i.e. it will wait for
    340                 // network to restore and then request it).
    341                 EasPing.requestPing(amAccount);
    342             } else {
    343                 stopServiceIfNoPings();
    344             }
    345 
    346             // TODO: It might be the case that only STATUS_CHANGES_FOUND and
    347             // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through.
    348             notifyAll();
    349         }
    350 
    351     }
    352     private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer();
    353 
    354     /**
    355      * The binder for IEmailService.
    356      */
    357     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
    358 
    359         private String getEmailAddressForAccount(final long accountId) {
    360             final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this,
    361                     Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION,
    362                     new String[] {Long.toString(accountId)}, null, 0);
    363             if (emailAddress == null) {
    364                 LogUtils.e(TAG, "Could not find email address for account %d", accountId);
    365             }
    366             return emailAddress;
    367         }
    368 
    369         @Override
    370         public Bundle validate(final HostAuth hostAuth) {
    371             LogUtils.d(TAG, "IEmailService.validate");
    372             return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).validate();
    373         }
    374 
    375         @Override
    376         public Bundle autoDiscover(final String username, final String password) {
    377             LogUtils.d(TAG, "IEmailService.autoDiscover");
    378             return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
    379                     .doAutodiscover();
    380         }
    381 
    382         @Override
    383         public void updateFolderList(final long accountId) {
    384             LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
    385             final String emailAddress = getEmailAddressForAccount(accountId);
    386             if (emailAddress != null) {
    387                 final Bundle extras = new Bundle(1);
    388                 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
    389                 ContentResolver.requestSync(new android.accounts.Account(
    390                         emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
    391                         EmailContent.AUTHORITY, extras);
    392             }
    393         }
    394 
    395         @Override
    396         public void setLogging(final int flags) {
    397             // TODO: fix this?
    398             // Protocol logging
    399             Eas.setUserDebug(flags);
    400             // Sync logging
    401             //setUserDebug(flags);
    402         }
    403 
    404         @Override
    405         public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId,
    406                 final boolean background) {
    407             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
    408             // TODO: Prevent this from happening in parallel with a sync?
    409             EasAttachmentLoader.loadAttachment(EmailSyncAdapterService.this, attachmentId,
    410                     callback);
    411         }
    412 
    413         @Override
    414         public void sendMeetingResponse(final long messageId, final int response) {
    415             LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
    416             EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
    417                     response);
    418         }
    419 
    420         /**
    421          * Delete PIM (calendar, contacts) data for the specified account
    422          *
    423          * @param emailAddress the email address for the account whose data should be deleted
    424          */
    425         @Override
    426         public void deleteAccountPIMData(final String emailAddress) {
    427             LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
    428             if (emailAddress != null) {
    429                 final Context context = EmailSyncAdapterService.this;
    430                 EasContactsSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
    431                 EasCalendarSyncHandler.wipeAccountFromContentProvider(context, emailAddress);
    432             }
    433             // TODO: Run account reconciler?
    434         }
    435 
    436         @Override
    437         public int searchMessages(final long accountId, final SearchParams searchParams,
    438                 final long destMailboxId) {
    439             LogUtils.d(TAG, "IEmailService.searchMessages");
    440             return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams,
    441                     destMailboxId);
    442             // TODO: may need an explicit callback to replace the one to IEmailServiceCallback.
    443         }
    444 
    445         @Override
    446         public void sendMail(final long accountId) {}
    447 
    448         @Override
    449         public int getCapabilities(final Account acct) {
    450             String easVersion = acct.mProtocolVersion;
    451             Double easVersionDouble = 2.5D;
    452             if (easVersion != null) {
    453                 try {
    454                     easVersionDouble = Double.parseDouble(easVersion);
    455                 } catch (NumberFormatException e) {
    456                     // Stick with 2.5
    457                 }
    458             }
    459             if (easVersionDouble >= 12.0D) {
    460                 return AccountCapabilities.SYNCABLE_FOLDERS |
    461                         AccountCapabilities.SERVER_SEARCH |
    462                         AccountCapabilities.FOLDER_SERVER_SEARCH |
    463                         AccountCapabilities.SMART_REPLY |
    464                         AccountCapabilities.UNDO |
    465                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
    466             } else {
    467                 return AccountCapabilities.SYNCABLE_FOLDERS |
    468                         AccountCapabilities.SMART_REPLY |
    469                         AccountCapabilities.UNDO |
    470                         AccountCapabilities.DISCARD_CONVERSATION_DRAFTS;
    471             }
    472         }
    473 
    474         @Override
    475         public void serviceUpdated(final String emailAddress) {
    476             // Not required for EAS
    477         }
    478 
    479         // All IEmailService messages below are UNCALLED in Email.
    480         // TODO: Remove.
    481         @Deprecated
    482         @Override
    483         public int getApiLevel() {
    484             return Api.LEVEL;
    485         }
    486 
    487         @Deprecated
    488         @Override
    489         public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {}
    490 
    491         @Deprecated
    492         @Override
    493         public void stopSync(long mailboxId) {}
    494 
    495         @Deprecated
    496         @Override
    497         public void loadMore(long messageId) {}
    498 
    499         @Deprecated
    500         @Override
    501         public boolean createFolder(long accountId, String name) {
    502             return false;
    503         }
    504 
    505         @Deprecated
    506         @Override
    507         public boolean deleteFolder(long accountId, String name) {
    508             return false;
    509         }
    510 
    511         @Deprecated
    512         @Override
    513         public boolean renameFolder(long accountId, String oldName, String newName) {
    514             return false;
    515         }
    516 
    517         @Deprecated
    518         @Override
    519         public void hostChanged(long accountId) {}
    520     };
    521 
    522     public EmailSyncAdapterService() {
    523         super();
    524     }
    525 
    526     /**
    527      * {@link AsyncTask} for restarting pings for all accounts that need it.
    528      */
    529     private static final String PUSH_ACCOUNTS_SELECTION =
    530             AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
    531     private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
    532 
    533         private final ContentResolver mContentResolver;
    534         private final SyncHandlerSynchronizer mSyncHandlerMap;
    535         private boolean mAnyAccounts;
    536 
    537         public RestartPingsTask(final ContentResolver contentResolver,
    538                 final SyncHandlerSynchronizer syncHandlerMap) {
    539             mContentResolver = contentResolver;
    540             mSyncHandlerMap = syncHandlerMap;
    541         }
    542 
    543         @Override
    544         protected Void doInBackground(Void... params) {
    545             final Cursor c = mContentResolver.query(Account.CONTENT_URI,
    546                     Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
    547             if (c != null) {
    548                 try {
    549                     mAnyAccounts = (c.getCount() != 0);
    550                     while (c.moveToNext()) {
    551                         final Account account = new Account();
    552                         account.restore(c);
    553                         mSyncHandlerMap.modifyPing(false, account);
    554                     }
    555                 } finally {
    556                     c.close();
    557                 }
    558             } else {
    559                 mAnyAccounts = false;
    560             }
    561             return null;
    562         }
    563 
    564         @Override
    565         protected void onPostExecute(Void result) {
    566             if (!mAnyAccounts) {
    567                 LogUtils.d(TAG, "stopping for no accounts");
    568                 EmailSyncAdapterService.this.stopSelf();
    569             }
    570         }
    571     }
    572 
    573     @Override
    574     public void onCreate() {
    575         LogUtils.v(TAG, "onCreate()");
    576         super.onCreate();
    577         startService(new Intent(this, EmailSyncAdapterService.class));
    578         // Restart push for all accounts that need it.
    579         new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor(
    580                 AsyncTask.THREAD_POOL_EXECUTOR);
    581     }
    582 
    583     @Override
    584     public void onDestroy() {
    585         LogUtils.v(TAG, "onDestroy()");
    586         super.onDestroy();
    587         for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) {
    588             if (task != null) {
    589                 task.stop();
    590             }
    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             final boolean hasCallbackMethod =
    682                     extras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
    683             if (hasCallbackMethod && mailboxIds != null) {
    684                 for (long mailboxId : mailboxIds) {
    685                     EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId,
    686                             EmailServiceStatus.IN_PROGRESS, 0, UIProvider.LastSyncResult.SUCCESS);
    687                 }
    688             }
    689 
    690             // Push only means this sync request should only refresh the ping (either because
    691             // settings changed, or we need to restart it for some reason).
    692             final boolean pushOnly = Mailbox.isPushOnlyExtras(extras);
    693             // Account only means just do a FolderSync.
    694             final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras);
    695 
    696             // A "full sync" means that we didn't request a more specific type of sync.
    697             final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
    698                     mailboxType == Mailbox.TYPE_NONE);
    699 
    700             // A FolderSync is necessary for full sync, initial sync, and account only sync.
    701             final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
    702 
    703             // If we're just twiddling the push, we do the lightweight thing and bail early.
    704             if (pushOnly && !isFolderSync) {
    705                 mSyncHandlerMap.modifyPing(false, account);
    706                 LogUtils.d(TAG, "onPerformSync: mailbox push only");
    707                 return;
    708             }
    709 
    710             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
    711             mSyncHandlerMap.startSync(account.mId);
    712 
    713             // Perform a FolderSync if necessary.
    714             // TODO: We permit FolderSync even during security hold, because it's necessary to
    715             // resolve some holds. Ideally we would only do it for the holds that require it.
    716             if (isFolderSync) {
    717                 final EasFolderSync folderSync = new EasFolderSync(context, account);
    718                 folderSync.doFolderSync(syncResult);
    719             }
    720 
    721             boolean lastSyncHadError = false;
    722 
    723             if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) == 0) {
    724                 // Perform email upsync for this account. Moves first, then state changes.
    725                 if (!isInitialSync) {
    726                     EasMoveItems move = new EasMoveItems(context, account);
    727                     move.upsyncMovedMessages(syncResult);
    728                     // TODO: EasSync should eventually handle both up and down; for now, it's used
    729                     // purely for upsync.
    730                     EasSync upsync = new EasSync(context, account);
    731                     upsync.upsync(syncResult);
    732                 }
    733 
    734                 // TODO: Should we refresh account here? It may have changed while waiting for any
    735                 // pings to stop. It may not matter since the things that may have been twiddled
    736                 // might not affect syncing.
    737 
    738                 if (mailboxIds != null) {
    739                     long numIoExceptions = 0;
    740                     long numAuthExceptions = 0;
    741                     // Sync the mailbox that was explicitly requested.
    742                     for (final long mailboxId : mailboxIds) {
    743                         final boolean success = syncMailbox(context, cr, acct, account, mailboxId,
    744                                 extras, syncResult, null, true);
    745                         if (!success) {
    746                             lastSyncHadError = true;
    747                         }
    748                         if (hasCallbackMethod) {
    749                             final int result;
    750                             if (syncResult.hasError()) {
    751                                 if (syncResult.stats.numIoExceptions > numIoExceptions) {
    752                                     result = UIProvider.LastSyncResult.CONNECTION_ERROR;
    753                                     numIoExceptions = syncResult.stats.numIoExceptions;
    754                                 } else if (syncResult.stats.numAuthExceptions> numAuthExceptions) {
    755                                     result = UIProvider.LastSyncResult.AUTH_ERROR;
    756                                     numAuthExceptions= syncResult.stats.numAuthExceptions;
    757                                 }  else {
    758                                     result = UIProvider.LastSyncResult.INTERNAL_ERROR;
    759                                 }
    760                             } else {
    761                                 result = UIProvider.LastSyncResult.SUCCESS;
    762                             }
    763                             EmailServiceStatus.syncMailboxStatus(
    764                                     cr, extras, mailboxId,EmailServiceStatus.SUCCESS, 0, result);
    765                         }
    766                     }
    767                 } else if (!accountOnly && !pushOnly) {
    768                     // We have to sync multiple folders.
    769                     final Cursor c;
    770                     if (isFullSync) {
    771                         // Full account sync includes all mailboxes that participate in system sync.
    772                         c = Mailbox.getMailboxIdsForSync(cr, account.mId);
    773                     } else {
    774                         // Type-filtered sync should only get the mailboxes of a specific type.
    775                         c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType);
    776                     }
    777                     if (c != null) {
    778                         try {
    779                             final HashSet<String> authsToSync = getAuthsToSync(acct);
    780                             while (c.moveToNext()) {
    781                                 boolean success = syncMailbox(context, cr, acct, account,
    782                                         c.getLong(0), extras, syncResult, authsToSync, false);
    783                                 if (!success) {
    784                                     lastSyncHadError = true;
    785                                 }
    786                             }
    787                         } finally {
    788                             c.close();
    789                         }
    790                     }
    791                 }
    792             }
    793 
    794             // Clean up the bookkeeping, including restarting ping if necessary.
    795             mSyncHandlerMap.syncComplete(lastSyncHadError, account);
    796 
    797             // TODO: It may make sense to have common error handling here. Two possible mechanisms:
    798             // 1) performSync return value can signal some useful info.
    799             // 2) syncResult can contain useful info.
    800             LogUtils.d(TAG, "onPerformSync: finished");
    801         }
    802 
    803         /**
    804          * Update the mailbox's sync status with the provider and, if we're finished with the sync,
    805          * write the last sync time as well.
    806          * @param context Our {@link Context}.
    807          * @param mailbox The mailbox whose sync status to update.
    808          * @param cv A {@link ContentValues} object to use for updating the provider.
    809          * @param syncStatus The status for the current sync.
    810          */
    811         private void updateMailbox(final Context context, final Mailbox mailbox,
    812                 final ContentValues cv, final int syncStatus) {
    813             cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
    814             if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
    815                 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
    816             }
    817             mailbox.update(context, cv);
    818         }
    819 
    820         private boolean syncMailbox(final Context context, final ContentResolver cr,
    821                 final android.accounts.Account acct, final Account account, final long mailboxId,
    822                 final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync,
    823                 final boolean isMailboxSync) {
    824             final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
    825             if (mailbox == null) {
    826                 return false;
    827             }
    828 
    829             if (mailbox.mAccountKey != account.mId) {
    830                 LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(),
    831                         extras.toString());
    832                 return false;
    833             }
    834             if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
    835                 // We are asking for an account sync, but this mailbox type is not configured for
    836                 // sync. Do NOT treat this as a sync error for ping backoff purposes.
    837                 return true;
    838             }
    839 
    840             if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
    841                 // TODO: Because we don't have bidirectional sync working, trying to downsync
    842                 // the drafts folder is confusing. b/11158759
    843                 // For now, just disable all syncing of DRAFTS type folders.
    844                 // Automatic syncing should always be disabled, but we also stop it here to ensure
    845                 // that we won't sync even if the user attempts to force a sync from the UI.
    846                 // Do NOT treat as a sync error for ping backoff purposes.
    847                 LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
    848                 return true;
    849             }
    850             final boolean success;
    851             // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
    852             // treated as background syncs.
    853             // TODO: Push will be treated as "user" syncs, and probably should be background.
    854             final ContentValues cv = new ContentValues(2);
    855             updateMailbox(context, mailbox, cv, isMailboxSync ?
    856                     EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
    857             if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
    858                 final EasOutboxSyncHandler outboxSyncHandler =
    859                         new EasOutboxSyncHandler(context, account, mailbox);
    860                 outboxSyncHandler.performSync();
    861                 success = true;
    862             } else if(mailbox.isSyncable()) {
    863                 final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr,
    864                         acct, account, mailbox, extras, syncResult);
    865                 if (syncHandler != null) {
    866                     success = syncHandler.performSync(syncResult);
    867                 } else {
    868                     success = false;
    869                 }
    870             } else {
    871                 success = false;
    872             }
    873             updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE);
    874 
    875             if (syncResult.stats.numAuthExceptions > 0) {
    876                 showAuthNotification(account.mId, account.mEmailAddress);
    877             }
    878             return success;
    879         }
    880     }
    881     private void showAuthNotification(long accountId, String accountName) {
    882         final PendingIntent pendingIntent = PendingIntent.getActivity(
    883                 this,
    884                 0,
    885                 createAccountSettingsIntent(accountId, accountName),
    886                 0);
    887 
    888         final Notification notification = new Builder(this)
    889                 .setContentTitle(this.getString(string.auth_error_notification_title))
    890                 .setContentText(this.getString(
    891                         string.auth_error_notification_text, accountName))
    892                 .setSmallIcon(drawable.stat_notify_auth)
    893                 .setContentIntent(pendingIntent)
    894                 .setAutoCancel(true)
    895                 .build();
    896 
    897         final NotificationManager nm = (NotificationManager)
    898                 this.getSystemService(Context.NOTIFICATION_SERVICE);
    899         nm.notify("AuthError", 0, notification);
    900     }
    901 
    902     /**
    903      * Create and return an intent to display (and edit) settings for a specific account, or -1
    904      * for any/all accounts.  If an account name string is provided, a warning dialog will be
    905      * displayed as well.
    906      */
    907     public static Intent createAccountSettingsIntent(long accountId, String accountName) {
    908         final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder(
    909                 IntentUtilities.PATH_SETTINGS);
    910         IntentUtilities.setAccountId(builder, accountId);
    911         IntentUtilities.setAccountName(builder, accountName);
    912         return new Intent(Intent.ACTION_EDIT, builder.build());
    913     }
    914 
    915     /**
    916      * Determine which content types are set to sync for an account.
    917      * @param account The account whose sync settings we're looking for.
    918      * @return The authorities for the content types we want to sync for account.
    919      */
    920     private static HashSet<String> getAuthsToSync(final android.accounts.Account account) {
    921         final HashSet<String> authsToSync = new HashSet();
    922         if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) {
    923             authsToSync.add(EmailContent.AUTHORITY);
    924         }
    925         if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) {
    926             authsToSync.add(CalendarContract.AUTHORITY);
    927         }
    928         if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
    929             authsToSync.add(ContactsContract.AUTHORITY);
    930         }
    931         return authsToSync;
    932     }
    933 }
    934