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