Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2014 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.Service;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.database.Cursor;
     24 import android.os.AsyncTask;
     25 import android.os.Bundle;
     26 import android.os.IBinder;
     27 import android.provider.CalendarContract;
     28 import android.provider.ContactsContract;
     29 import android.text.TextUtils;
     30 
     31 import com.android.emailcommon.TempDirectory;
     32 import com.android.emailcommon.provider.Account;
     33 import com.android.emailcommon.provider.EmailContent;
     34 import com.android.emailcommon.provider.HostAuth;
     35 import com.android.emailcommon.provider.Mailbox;
     36 import com.android.emailcommon.service.EmailServiceProxy;
     37 import com.android.emailcommon.service.EmailServiceStatus;
     38 import com.android.emailcommon.service.EmailServiceVersion;
     39 import com.android.emailcommon.service.HostAuthCompat;
     40 import com.android.emailcommon.service.IEmailService;
     41 import com.android.emailcommon.service.IEmailServiceCallback;
     42 import com.android.emailcommon.service.SearchParams;
     43 import com.android.emailcommon.service.ServiceProxy;
     44 import com.android.exchange.Eas;
     45 import com.android.exchange.eas.EasAutoDiscover;
     46 import com.android.exchange.eas.EasFolderSync;
     47 import com.android.exchange.eas.EasFullSyncOperation;
     48 import com.android.exchange.eas.EasLoadAttachment;
     49 import com.android.exchange.eas.EasOperation;
     50 import com.android.exchange.eas.EasSearch;
     51 import com.android.exchange.eas.EasSearchGal;
     52 import com.android.exchange.eas.EasSendMeetingResponse;
     53 import com.android.exchange.eas.EasSyncCalendar;
     54 import com.android.exchange.eas.EasSyncContacts;
     55 import com.android.exchange.provider.GalResult;
     56 import com.android.mail.utils.LogUtils;
     57 
     58 import java.util.HashSet;
     59 import java.util.Set;
     60 
     61 /**
     62  * Service to handle all communication with the EAS server. Note that this is completely decoupled
     63  * from the sync adapters; sync adapters should make blocking calls on this service to actually
     64  * perform any operations.
     65  */
     66 public class EasService extends Service {
     67 
     68     private static final String TAG = Eas.LOG_TAG;
     69 
     70     /**
     71      * The content authorities that can be synced for EAS accounts. Initialization must wait until
     72      * after we have a chance to call {@link EmailContent#init} (and, for future content types,
     73      * possibly other initializations) because that's how we can know what the email authority is.
     74      */
     75     private static String[] AUTHORITIES_TO_SYNC;
     76 
     77     /** Bookkeeping for ping tasks & sync threads management. */
     78     private final PingSyncSynchronizer mSynchronizer;
     79 
     80     /**
     81      * Implementation of the IEmailService interface.
     82      * For the most part these calls should consist of creating the correct {@link EasOperation}
     83      * class and calling {@link #doOperation} with it.
     84      */
     85     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
     86         @Override
     87         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
     88                 final long attachmentId, final boolean background) {
     89             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
     90             final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId,
     91                     attachmentId, callback);
     92             doOperation(operation, "IEmailService.loadAttachment");
     93         }
     94 
     95         @Override
     96         public void updateFolderList(final long accountId) {
     97             final EasFolderSync operation = new EasFolderSync(EasService.this, accountId);
     98             doOperation(operation, "IEmailService.updateFolderList");
     99         }
    100 
    101         public void sendMail(final long accountId) {
    102             // TODO: We should get rid of sendMail, and this is done in sync.
    103             LogUtils.wtf(TAG, "unexpected call to EasService.sendMail");
    104         }
    105 
    106         public int sync(final long accountId, Bundle syncExtras) {
    107             EasFullSyncOperation op = new EasFullSyncOperation(EasService.this, accountId, syncExtras);
    108             return convertToEmailServiceStatus(doOperation(op, "IEmailService.sync"));
    109         }
    110 
    111         @Override
    112         public void pushModify(final long accountId) {
    113             LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId);
    114             final Account account = Account.restoreAccountWithId(EasService.this, accountId);
    115             if (pingNeededForAccount(account)) {
    116                 mSynchronizer.pushModify(account);
    117             } else {
    118                 mSynchronizer.pushStop(accountId);
    119             }
    120         }
    121 
    122         @Override
    123         public Bundle validate(final HostAuthCompat hostAuthCom) {
    124             final HostAuth hostAuth = hostAuthCom.toHostAuth();
    125             final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth);
    126             doOperation(operation, "IEmailService.validate");
    127             return operation.getValidationResult();
    128         }
    129 
    130         @Override
    131         public int searchMessages(final long accountId, final SearchParams searchParams,
    132                 final long destMailboxId) {
    133             final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams,
    134                     destMailboxId);
    135             doOperation(operation, "IEmailService.searchMessages");
    136             return operation.getTotalResults();
    137         }
    138 
    139         @Override
    140         public void sendMeetingResponse(final long messageId, final int response) {
    141             EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(EasService.this,
    142                     messageId);
    143             if (msg == null) {
    144                 LogUtils.e(TAG, "Could not load message %d in sendMeetingResponse", messageId);
    145                 return;
    146             }
    147 
    148             final EasSendMeetingResponse operation = new EasSendMeetingResponse(EasService.this,
    149                     msg.mAccountKey, msg, response);
    150             doOperation(operation, "IEmailService.sendMeetingResponse");
    151         }
    152 
    153         @Override
    154         public Bundle autoDiscover(final String username, final String password) {
    155             final String domain = EasAutoDiscover.getDomain(username);
    156             for (int attempt = 0; attempt <= EasAutoDiscover.ATTEMPT_MAX; attempt++) {
    157                 LogUtils.d(TAG, "autodiscover attempt %d", attempt);
    158                 final String uri = EasAutoDiscover.genUri(domain, attempt);
    159                 Bundle result = autoDiscoverInternal(uri, attempt, username, password, true);
    160                 int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
    161                 if (resultCode != EasAutoDiscover.RESULT_BAD_RESPONSE) {
    162                     return result;
    163                 } else {
    164                     LogUtils.d(TAG, "got BAD_RESPONSE");
    165                 }
    166             }
    167             return null;
    168         }
    169 
    170         private Bundle autoDiscoverInternal(final String uri, final int attempt,
    171                                             final String username, final String password,
    172                                             final boolean canRetry) {
    173             final EasAutoDiscover op = new EasAutoDiscover(EasService.this, uri, attempt,
    174                     username, password);
    175             final int result = op.performOperation();
    176             if (result == EasAutoDiscover.RESULT_REDIRECT) {
    177                 // Try again recursively with the new uri. TODO we should limit the number of redirects.
    178                 final String redirectUri = op.getRedirectUri();
    179                 return autoDiscoverInternal(redirectUri, attempt, username, password, canRetry);
    180             } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) {
    181                 if (canRetry && username.contains("@")) {
    182                     // Try again using the bare user name
    183                     final int atSignIndex = username.indexOf('@');
    184                     final String bareUsername = username.substring(0, atSignIndex);
    185                     LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex);
    186                     // Try again recursively, but this time don't allow retries for username.
    187                     return autoDiscoverInternal(uri, attempt, bareUsername, password, false);
    188                 } else {
    189                     // Either we're already on our second try or the username didn't have an "@"
    190                     // to begin with. Either way, failure.
    191                     final Bundle bundle = new Bundle(1);
    192                     bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    193                             EasAutoDiscover.RESULT_OTHER_FAILURE);
    194                     return bundle;
    195                 }
    196             } else if (result != EasAutoDiscover.RESULT_OK) {
    197                 // Return failure, we'll try again with an alternate address
    198                 final Bundle bundle = new Bundle(1);
    199                 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    200                         EasAutoDiscover.RESULT_BAD_RESPONSE);
    201                 return bundle;
    202             }
    203             // Success.
    204             return op.getResultBundle();
    205         }
    206 
    207         @Override
    208         public void setLogging(final int flags) {
    209             LogUtils.d(TAG, "IEmailService.setLogging");
    210         }
    211 
    212         @Override
    213         public void deleteExternalAccountPIMData(final String emailAddress) {
    214             LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
    215             if (emailAddress != null) {
    216                 // TODO: stop pings
    217                 final Context context = EasService.this;
    218                 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress);
    219                 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress);
    220             }
    221         }
    222 
    223         public int getApiVersion() {
    224             return EmailServiceVersion.CURRENT;
    225         }
    226     };
    227 
    228     /**
    229      * Content selection string for getting all accounts that are configured for push.
    230      * TODO: Add protocol check so that we don't get e.g. IMAP accounts here.
    231      * (Not currently necessary but eventually will be.)
    232      */
    233     private static final String PUSH_ACCOUNTS_SELECTION =
    234             EmailContent.AccountColumns.SYNC_INTERVAL +
    235                     "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
    236 
    237     /** {@link AsyncTask} to restart pings for all accounts that need it. */
    238     private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
    239         private boolean mHasRestartedPing = false;
    240 
    241         @Override
    242         protected Void doInBackground(Void... params) {
    243             final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI,
    244                     Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
    245             if (c != null) {
    246                 try {
    247                     while (c.moveToNext()) {
    248                         final Account account = new Account();
    249                         LogUtils.d(TAG, "RestartPingsTask starting ping for %s", account);
    250                         account.restore(c);
    251                         if (EasService.this.pingNeededForAccount(account)) {
    252                             mHasRestartedPing = true;
    253                             EasService.this.mSynchronizer.pushModify(account);
    254                         }
    255                     }
    256                 } finally {
    257                     c.close();
    258                 }
    259             }
    260             return null;
    261         }
    262 
    263         @Override
    264         protected void onPostExecute(Void result) {
    265             if (!mHasRestartedPing) {
    266                 LogUtils.d(TAG, "RestartPingsTask did not start any pings.");
    267                 EasService.this.mSynchronizer.stopServiceIfIdle();
    268             }
    269         }
    270     }
    271 
    272     public EasService() {
    273         super();
    274         mSynchronizer = new PingSyncSynchronizer(this);
    275     }
    276 
    277     @Override
    278     public void onCreate() {
    279         LogUtils.d(TAG, "EasService.onCreate");
    280         super.onCreate();
    281         TempDirectory.setTempDirectory(this);
    282         EmailContent.init(this);
    283         AUTHORITIES_TO_SYNC = new String[] {
    284                 EmailContent.AUTHORITY,
    285                 CalendarContract.AUTHORITY,
    286                 ContactsContract.AUTHORITY
    287         };
    288 
    289         // Restart push for all accounts that need it. Because this requires DB loads, we do it in
    290         // an AsyncTask, and we startService to ensure that we stick around long enough for the
    291         // task to complete. The task will stop the service if necessary after it's done.
    292         startService(new Intent(this, EasService.class));
    293         new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    294     }
    295 
    296     @Override
    297     public void onDestroy() {
    298         mSynchronizer.stopAllPings();
    299     }
    300 
    301     @Override
    302     public IBinder onBind(final Intent intent) {
    303         return mBinder;
    304     }
    305 
    306     @Override
    307     public int onStartCommand(final Intent intent, final int flags, final int startId) {
    308         if (intent != null &&
    309                 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
    310             if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
    311                 // We've been asked to forcibly shutdown. This happens if email accounts are
    312                 // deleted, otherwise we can get errors if services are still running for
    313                 // accounts that are now gone.
    314                 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
    315                 // if accounts disappear out from under us.
    316                 LogUtils.d(TAG, "Forced shutdown, killing process");
    317                 System.exit(-1);
    318             }
    319         }
    320         return START_STICKY;
    321     }
    322 
    323     public int doOperation(final EasOperation operation, final String loggingName) {
    324         LogUtils.d(TAG, "%s: %d", loggingName, operation.getAccountId());
    325         mSynchronizer.syncStart(operation.getAccountId());
    326         int result = EasOperation.RESULT_MIN_OK_RESULT;
    327         // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA
    328         // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls.
    329         // If we add a wakelock (or anything else for that matter) here, must remember to undo
    330         // it in the finally block below.
    331         // On the other hand, even for SAs, it doesn't hurt to get a wakelock here.
    332         try {
    333             result = operation.performOperation();
    334             LogUtils.d(TAG, "Operation result %d", result);
    335             return result;
    336         } finally {
    337             mSynchronizer.syncEnd(result >= EasOperation.RESULT_MIN_OK_RESULT,
    338                     operation.getAccount());
    339         }
    340     }
    341 
    342     /**
    343      * Determine whether this account is configured with folders that are ready for push
    344      * notifications.
    345      * @param account The {@link Account} that we're interested in.
    346      * @return Whether this account needs to ping.
    347      */
    348     public boolean pingNeededForAccount(final Account account) {
    349         // Check account existence.
    350         if (account == null || account.mId == Account.NO_ACCOUNT) {
    351             LogUtils.d(TAG, "Do not ping: Account not found or not valid");
    352             return false;
    353         }
    354 
    355         // Check if account is configured for a push sync interval.
    356         if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) {
    357             LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId);
    358             return false;
    359         }
    360 
    361         // Check security hold status of the account.
    362         if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    363             LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId);
    364             return false;
    365         }
    366 
    367         // Check if the account has performed at least one sync so far (accounts must perform
    368         // the initial sync before push is possible).
    369         if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
    370             LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId);
    371             return false;
    372         }
    373 
    374         // Check that there's at least one mailbox that is both configured for push notifications,
    375         // and whose content type is enabled for sync in the account manager.
    376         final android.accounts.Account amAccount = new android.accounts.Account(
    377                         account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    378 
    379         final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC);
    380         // If we have at least one sync-enabled content type, check for syncing mailboxes.
    381         if (!authsToSync.isEmpty()) {
    382             final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId);
    383             if (c != null) {
    384                 try {
    385                     while (c.moveToNext()) {
    386                         final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
    387                         if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
    388                             return true;
    389                         }
    390                     }
    391                 } finally {
    392                     c.close();
    393                 }
    394             }
    395         }
    396         LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId);
    397         return false;
    398     }
    399 
    400     static public GalResult searchGal(final Context context, final long accountId,
    401                                       final String filter, final int limit) {
    402         final EasSearchGal operation = new EasSearchGal(context, accountId, filter, limit);
    403         // We don't use doOperation() here for two reasons:
    404         // 1. This is a static function, doOperation is not, and we don't have an instance of
    405         // EasService.
    406         // 2. All doOperation() does besides this is stop the ping and then restart it. This is
    407         // required during syncs, but not for GalSearches.
    408         final int result = operation.performOperation();
    409         if (result == EasSearchGal.RESULT_OK) {
    410             return operation.getResult();
    411         } else {
    412             return null;
    413         }
    414     }
    415 
    416     /**
    417      * Converts from an EasOperation status to a status code defined in EmailServiceStatus.
    418      * This is used to communicate the status of a sync operation to the caller.
    419      * @param easStatus result returned from an EasOperation
    420      * @return EmailServiceStatus
    421      */
    422     private int convertToEmailServiceStatus(int easStatus) {
    423         if (easStatus >= EasOperation.RESULT_MIN_OK_RESULT) {
    424             return EmailServiceStatus.SUCCESS;
    425         }
    426         switch (easStatus) {
    427             case EasOperation.RESULT_ABORT:
    428             case EasOperation.RESULT_RESTART:
    429                 // This should only happen if a ping is interruped for some reason. We would not
    430                 // expect see that here, since this should only be called for a sync.
    431                 LogUtils.e(TAG, "Abort or Restart easStatus");
    432                 return EmailServiceStatus.SUCCESS;
    433 
    434             case EasOperation.RESULT_TOO_MANY_REDIRECTS:
    435                 return EmailServiceStatus.INTERNAL_ERROR;
    436 
    437             case EasOperation.RESULT_NETWORK_PROBLEM:
    438                 // This is due to an IO error, we need the caller to know about this so that it
    439                 // can let the syncManager know.
    440                 return EmailServiceStatus.IO_ERROR;
    441 
    442             case EasOperation.RESULT_FORBIDDEN:
    443             case EasOperation.RESULT_AUTHENTICATION_ERROR:
    444                 return EmailServiceStatus.LOGIN_FAILED;
    445 
    446             case EasOperation.RESULT_PROVISIONING_ERROR:
    447                 return EmailServiceStatus.PROVISIONING_ERROR;
    448 
    449             case EasOperation.RESULT_CLIENT_CERTIFICATE_REQUIRED:
    450                 return EmailServiceStatus.CLIENT_CERTIFICATE_ERROR;
    451 
    452             case EasOperation.RESULT_PROTOCOL_VERSION_UNSUPPORTED:
    453                 return EmailServiceStatus.PROTOCOL_ERROR;
    454 
    455             case EasOperation.RESULT_INITIALIZATION_FAILURE:
    456             case EasOperation.RESULT_HARD_DATA_FAILURE:
    457             case EasOperation.RESULT_OTHER_FAILURE:
    458                 return EmailServiceStatus.INTERNAL_ERROR;
    459 
    460             case EasOperation.RESULT_NON_FATAL_ERROR:
    461                 // We do not expect to see this error here: This should be consumed in
    462                 // EasFullSyncOperation. The only case this occurs in is when we try to send
    463                 // a message in the outbox, and there's some problem with the message locally
    464                 // that prevents it from being sent. We return a
    465                 LogUtils.e(TAG, "Other non-fatal error easStatus %d", easStatus);
    466                 return EmailServiceStatus.SUCCESS;
    467         }
    468         LogUtils.e(TAG, "Unexpected easStatus %d", easStatus);
    469         return EmailServiceStatus.INTERNAL_ERROR;
    470     }
    471 
    472 
    473     /**
    474      * Determine which content types are set to sync for an account.
    475      * @param account The account whose sync settings we're looking for.
    476      * @param authorities All possible authorities we could care about.
    477      * @return The authorities for the content types we want to sync for account.
    478      */
    479     public static Set<String> getAuthoritiesToSync(final android.accounts.Account account,
    480                                                     final String[] authorities) {
    481         final HashSet<String> authsToSync = new HashSet();
    482         for (final String authority : authorities) {
    483             if (ContentResolver.getSyncAutomatically(account, authority)) {
    484                 authsToSync.add(authority);
    485             }
    486         }
    487         return authsToSync;
    488     }
    489 }
    490