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.Intent;
     22 import android.database.Cursor;
     23 import android.os.AsyncTask;
     24 import android.os.Bundle;
     25 import android.os.IBinder;
     26 import android.provider.CalendarContract;
     27 import android.provider.ContactsContract;
     28 import android.text.TextUtils;
     29 
     30 import com.android.emailcommon.provider.Account;
     31 import com.android.emailcommon.provider.EmailContent;
     32 import com.android.emailcommon.provider.HostAuth;
     33 import com.android.emailcommon.provider.Mailbox;
     34 import com.android.emailcommon.service.IEmailService;
     35 import com.android.emailcommon.service.IEmailServiceCallback;
     36 import com.android.emailcommon.service.SearchParams;
     37 import com.android.emailcommon.service.ServiceProxy;
     38 import com.android.exchange.Eas;
     39 import com.android.exchange.eas.EasFolderSync;
     40 import com.android.exchange.eas.EasLoadAttachment;
     41 import com.android.exchange.eas.EasOperation;
     42 import com.android.exchange.eas.EasSearch;
     43 import com.android.mail.utils.LogUtils;
     44 
     45 import java.util.HashSet;
     46 import java.util.Set;
     47 
     48 /**
     49  * Service to handle all communication with the EAS server. Note that this is completely decoupled
     50  * from the sync adapters; sync adapters should make blocking calls on this service to actually
     51  * perform any operations.
     52  */
     53 public class EasService extends Service {
     54 
     55     private static final String TAG = Eas.LOG_TAG;
     56 
     57     /**
     58      * The content authorities that can be synced for EAS accounts. Initialization must wait until
     59      * after we have a chance to call {@link EmailContent#init} (and, for future content types,
     60      * possibly other initializations) because that's how we can know what the email authority is.
     61      */
     62     private static String[] AUTHORITIES_TO_SYNC;
     63 
     64     /** Bookkeeping for ping tasks & sync threads management. */
     65     private final PingSyncSynchronizer mSynchronizer;
     66 
     67     /**
     68      * Implementation of the IEmailService interface.
     69      * For the most part these calls should consist of creating the correct {@link EasOperation}
     70      * class and calling {@link #doOperation} with it.
     71      */
     72     private final IEmailService.Stub mBinder = new IEmailService.Stub() {
     73         @Override
     74         public void sendMail(final long accountId) {
     75             LogUtils.d(TAG, "IEmailService.sendMail: %d", accountId);
     76         }
     77 
     78         @Override
     79         public void loadAttachment(final IEmailServiceCallback callback, final long accountId,
     80                 final long attachmentId, final boolean background) {
     81             LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId);
     82             final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId,
     83                     attachmentId, callback);
     84             doOperation(operation, "IEmailService.loadAttachment");
     85         }
     86 
     87         @Override
     88         public void updateFolderList(final long accountId) {
     89             final EasFolderSync operation = new EasFolderSync(EasService.this, accountId);
     90             doOperation(operation, "IEmailService.updateFolderList");
     91         }
     92 
     93         @Override
     94         public void sync(final long accountId, final boolean updateFolderList,
     95                 final int mailboxType, final long[] folders) {}
     96 
     97         @Override
     98         public void pushModify(final long accountId) {
     99             LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId);
    100             final Account account = Account.restoreAccountWithId(EasService.this, accountId);
    101             if (pingNeededForAccount(account)) {
    102                 mSynchronizer.pushModify(account);
    103             } else {
    104                 mSynchronizer.pushStop(accountId);
    105             }
    106         }
    107 
    108         @Override
    109         public Bundle validate(final HostAuth hostAuth) {
    110             final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth);
    111             doOperation(operation, "IEmailService.validate");
    112             return operation.getValidationResult();
    113         }
    114 
    115         @Override
    116         public int searchMessages(final long accountId, final SearchParams searchParams,
    117                 final long destMailboxId) {
    118             final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams,
    119                     destMailboxId);
    120             doOperation(operation, "IEmailService.searchMessages");
    121             return operation.getTotalResults();
    122         }
    123 
    124         @Override
    125         public void sendMeetingResponse(final long messageId, final int response) {
    126             LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
    127         }
    128 
    129         @Override
    130         public Bundle autoDiscover(final String username, final String password) {
    131             LogUtils.d(TAG, "IEmailService.autoDiscover");
    132             return null;
    133         }
    134 
    135         @Override
    136         public void setLogging(final int flags) {
    137             LogUtils.d(TAG, "IEmailService.setLogging");
    138         }
    139 
    140         @Override
    141         public void deleteAccountPIMData(final String emailAddress) {
    142             LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
    143         }
    144     };
    145 
    146     /**
    147      * Content selection string for getting all accounts that are configured for push.
    148      * TODO: Add protocol check so that we don't get e.g. IMAP accounts here.
    149      * (Not currently necessary but eventually will be.)
    150      */
    151     private static final String PUSH_ACCOUNTS_SELECTION =
    152             EmailContent.AccountColumns.SYNC_INTERVAL +
    153                     "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH);
    154 
    155     /** {@link AsyncTask} to restart pings for all accounts that need it. */
    156     private class RestartPingsTask extends AsyncTask<Void, Void, Void> {
    157         private boolean mHasRestartedPing = false;
    158 
    159         @Override
    160         protected Void doInBackground(Void... params) {
    161             final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI,
    162                     Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null);
    163             if (c != null) {
    164                 try {
    165                     while (c.moveToNext()) {
    166                         final Account account = new Account();
    167                         account.restore(c);
    168                         if (EasService.this.pingNeededForAccount(account)) {
    169                             mHasRestartedPing = true;
    170                             EasService.this.mSynchronizer.pushModify(account);
    171                         }
    172                     }
    173                 } finally {
    174                     c.close();
    175                 }
    176             }
    177             return null;
    178         }
    179 
    180         @Override
    181         protected void onPostExecute(Void result) {
    182             if (!mHasRestartedPing) {
    183                 LogUtils.d(TAG, "RestartPingsTask did not start any pings.");
    184                 EasService.this.mSynchronizer.stopServiceIfIdle();
    185             }
    186         }
    187     }
    188 
    189     public EasService() {
    190         super();
    191         mSynchronizer = new PingSyncSynchronizer(this);
    192     }
    193 
    194     @Override
    195     public void onCreate() {
    196         super.onCreate();
    197         EmailContent.init(this);
    198         AUTHORITIES_TO_SYNC = new String[] {
    199                 EmailContent.AUTHORITY,
    200                 CalendarContract.AUTHORITY,
    201                 ContactsContract.AUTHORITY
    202         };
    203 
    204         // Restart push for all accounts that need it. Because this requires DB loads, we do it in
    205         // an AsyncTask, and we startService to ensure that we stick around long enough for the
    206         // task to complete. The task will stop the service if necessary after it's done.
    207         startService(new Intent(this, EasService.class));
    208         new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    209     }
    210 
    211     @Override
    212     public void onDestroy() {
    213         mSynchronizer.stopAllPings();
    214     }
    215 
    216     @Override
    217     public IBinder onBind(final Intent intent) {
    218         return mBinder;
    219     }
    220 
    221     @Override
    222     public int onStartCommand(final Intent intent, final int flags, final int startId) {
    223         if (intent != null &&
    224                 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) {
    225             if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) {
    226                 // We've been asked to forcibly shutdown. This happens if email accounts are
    227                 // deleted, otherwise we can get errors if services are still running for
    228                 // accounts that are now gone.
    229                 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly
    230                 // if accounts disappear out from under us.
    231                 LogUtils.d(TAG, "Forced shutdown, killing process");
    232                 System.exit(-1);
    233             }
    234         }
    235         return START_STICKY;
    236     }
    237 
    238     public int doOperation(final EasOperation operation, final String loggingName) {
    239         final long accountId = operation.getAccountId();
    240         final Account account = operation.getAccount();
    241         LogUtils.d(TAG, "%s: %d", loggingName, accountId);
    242         mSynchronizer.syncStart(accountId);
    243         // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA
    244         // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls.
    245         // If we add a wakelock (or anything else for that matter) here, must remember to undo
    246         // it in the finally block below.
    247         // On the other hand, even for SAs, it doesn't hurt to get a wakelock here.
    248         try {
    249             return operation.performOperation();
    250         } finally {
    251             mSynchronizer.syncEnd(account);
    252         }
    253     }
    254 
    255     /**
    256      * Determine whether this account is configured with folders that are ready for push
    257      * notifications.
    258      * @param account The {@link Account} that we're interested in.
    259      * @return Whether this account needs to ping.
    260      */
    261     public boolean pingNeededForAccount(final Account account) {
    262         // Check account existence.
    263         if (account == null || account.mId == Account.NO_ACCOUNT) {
    264             LogUtils.d(TAG, "Do not ping: Account not found or not valid");
    265             return false;
    266         }
    267 
    268         // Check if account is configured for a push sync interval.
    269         if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) {
    270             LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId);
    271             return false;
    272         }
    273 
    274         // Check security hold status of the account.
    275         if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    276             LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId);
    277             return false;
    278         }
    279 
    280         // Check if the account has performed at least one sync so far (accounts must perform
    281         // the initial sync before push is possible).
    282         if (EmailContent.isInitialSyncKey(account.mSyncKey)) {
    283             LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId);
    284             return false;
    285         }
    286 
    287         // Check that there's at least one mailbox that is both configured for push notifications,
    288         // and whose content type is enabled for sync in the account manager.
    289         final android.accounts.Account amAccount = new android.accounts.Account(
    290                         account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    291 
    292         final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC);
    293         // If we have at least one sync-enabled content type, check for syncing mailboxes.
    294         if (!authsToSync.isEmpty()) {
    295             final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId);
    296             if (c != null) {
    297                 try {
    298                     while (c.moveToNext()) {
    299                         final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
    300                         if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) {
    301                             return true;
    302                         }
    303                     }
    304                 } finally {
    305                     c.close();
    306                 }
    307             }
    308         }
    309         LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId);
    310         return false;
    311     }
    312 
    313     /**
    314      * Determine which content types are set to sync for an account.
    315      * @param account The account whose sync settings we're looking for.
    316      * @param authorities All possible authorities we could care about.
    317      * @return The authorities for the content types we want to sync for account.
    318      */
    319     private static Set<String> getAuthoritiesToSync(final android.accounts.Account account,
    320             final String[] authorities) {
    321         final HashSet<String> authsToSync = new HashSet();
    322         for (final String authority : authorities) {
    323             if (ContentResolver.getSyncAutomatically(account, authority)) {
    324                 authsToSync.add(authority);
    325             }
    326         }
    327         return authsToSync;
    328     }
    329 }
    330