• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
doInBackground(Void... params)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
onPostExecute(Void result)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 
EasService()272     public EasService() {
273         super();
274         mSynchronizer = new PingSyncSynchronizer(this);
275     }
276 
277     @Override
onCreate()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
onDestroy()297     public void onDestroy() {
298         mSynchronizer.stopAllPings();
299     }
300 
301     @Override
onBind(final Intent intent)302     public IBinder onBind(final Intent intent) {
303         return mBinder;
304     }
305 
306     @Override
onStartCommand(final Intent intent, final int flags, final int startId)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 
doOperation(final EasOperation operation, final String loggingName)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      */
pingNeededForAccount(final Account account)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 
searchGal(final Context context, final long accountId, final String filter, final int limit)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      */
convertToEmailServiceStatus(int easStatus)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      */
getAuthoritiesToSync(final android.accounts.Account account, final String[] authorities)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