| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.exchange.service; |
| |
| import android.app.Service; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.provider.CalendarContract; |
| import android.provider.ContactsContract; |
| import android.text.TextUtils; |
| |
| import com.android.emailcommon.TempDirectory; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.HostAuth; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.service.EmailServiceProxy; |
| import com.android.emailcommon.service.EmailServiceStatus; |
| import com.android.emailcommon.service.EmailServiceVersion; |
| import com.android.emailcommon.service.HostAuthCompat; |
| import com.android.emailcommon.service.IEmailService; |
| import com.android.emailcommon.service.IEmailServiceCallback; |
| import com.android.emailcommon.service.SearchParams; |
| import com.android.emailcommon.service.ServiceProxy; |
| import com.android.exchange.Eas; |
| import com.android.exchange.eas.EasAutoDiscover; |
| import com.android.exchange.eas.EasFolderSync; |
| import com.android.exchange.eas.EasFullSyncOperation; |
| import com.android.exchange.eas.EasLoadAttachment; |
| import com.android.exchange.eas.EasOperation; |
| import com.android.exchange.eas.EasSearch; |
| import com.android.exchange.eas.EasSearchGal; |
| import com.android.exchange.eas.EasSendMeetingResponse; |
| import com.android.exchange.eas.EasSyncCalendar; |
| import com.android.exchange.eas.EasSyncContacts; |
| import com.android.exchange.provider.GalResult; |
| import com.android.mail.utils.LogUtils; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * Service to handle all communication with the EAS server. Note that this is completely decoupled |
| * from the sync adapters; sync adapters should make blocking calls on this service to actually |
| * perform any operations. |
| */ |
| public class EasService extends Service { |
| |
| private static final String TAG = Eas.LOG_TAG; |
| |
| public static final String EXTRA_START_PING = "START_PING"; |
| public static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT"; |
| |
| /** |
| * The content authorities that can be synced for EAS accounts. Initialization must wait until |
| * after we have a chance to call {@link EmailContent#init} (and, for future content types, |
| * possibly other initializations) because that's how we can know what the email authority is. |
| */ |
| private static String[] AUTHORITIES_TO_SYNC; |
| |
| /** Bookkeeping for ping tasks & sync threads management. */ |
| private final PingSyncSynchronizer mSynchronizer; |
| |
| private static boolean sProtocolLogging; |
| private static boolean sFileLogging; |
| |
| /** |
| * Implementation of the IEmailService interface. |
| * For the most part these calls should consist of creating the correct {@link EasOperation} |
| * class and calling {@link #doOperation} with it. |
| */ |
| private final IEmailService.Stub mBinder = new IEmailService.Stub() { |
| @Override |
| public void loadAttachment(final IEmailServiceCallback callback, final long accountId, |
| final long attachmentId, final boolean background) { |
| LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); |
| final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId, |
| attachmentId, callback); |
| doOperation(operation, "IEmailService.loadAttachment"); |
| } |
| |
| @Override |
| public void updateFolderList(final long accountId) { |
| final EasFolderSync operation = new EasFolderSync(EasService.this, accountId); |
| doOperation(operation, "IEmailService.updateFolderList"); |
| } |
| |
| public void sendMail(final long accountId) { |
| // TODO: We should get rid of sendMail, and this is done in sync. |
| LogUtils.wtf(TAG, "unexpected call to EasService.sendMail"); |
| } |
| |
| public int sync(final long accountId, Bundle syncExtras) { |
| EasFullSyncOperation op = new EasFullSyncOperation(EasService.this, accountId, syncExtras); |
| return convertToEmailServiceStatus(doOperation(op, "IEmailService.sync")); |
| } |
| |
| @Override |
| public void pushModify(final long accountId) { |
| LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId); |
| final Account account = Account.restoreAccountWithId(EasService.this, accountId); |
| if (pingNeededForAccount(account)) { |
| mSynchronizer.pushModify(account); |
| } else { |
| mSynchronizer.pushStop(accountId); |
| } |
| } |
| |
| @Override |
| public Bundle validate(final HostAuthCompat hostAuthCom) { |
| final HostAuth hostAuth = hostAuthCom.toHostAuth(); |
| final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth); |
| doOperation(operation, "IEmailService.validate"); |
| return operation.getValidationResult(); |
| } |
| |
| @Override |
| public int searchMessages(final long accountId, final SearchParams searchParams, |
| final long destMailboxId) { |
| final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams, |
| destMailboxId); |
| doOperation(operation, "IEmailService.searchMessages"); |
| return operation.getTotalResults(); |
| } |
| |
| @Override |
| public void sendMeetingResponse(final long messageId, final int response) { |
| EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(EasService.this, |
| messageId); |
| if (msg == null) { |
| LogUtils.e(TAG, "Could not load message %d in sendMeetingResponse", messageId); |
| return; |
| } |
| |
| final EasSendMeetingResponse operation = new EasSendMeetingResponse(EasService.this, |
| msg.mAccountKey, msg, response); |
| doOperation(operation, "IEmailService.sendMeetingResponse"); |
| } |
| |
| @Override |
| public Bundle autoDiscover(final String username, final String password) { |
| final String domain = EasAutoDiscover.getDomain(username); |
| for (int attempt = 0; attempt <= EasAutoDiscover.ATTEMPT_MAX; attempt++) { |
| LogUtils.d(TAG, "autodiscover attempt %d", attempt); |
| final String uri = EasAutoDiscover.genUri(domain, attempt); |
| Bundle result = autoDiscoverInternal(uri, attempt, username, password, true); |
| int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE); |
| if (resultCode != EasAutoDiscover.RESULT_BAD_RESPONSE) { |
| return result; |
| } else { |
| LogUtils.d(TAG, "got BAD_RESPONSE"); |
| } |
| } |
| return null; |
| } |
| |
| private Bundle autoDiscoverInternal(final String uri, final int attempt, |
| final String username, final String password, |
| final boolean canRetry) { |
| final EasAutoDiscover op = new EasAutoDiscover(EasService.this, uri, attempt, |
| username, password); |
| final int result = op.performOperation(); |
| if (result == EasAutoDiscover.RESULT_REDIRECT) { |
| // Try again recursively with the new uri. TODO we should limit the number of redirects. |
| final String redirectUri = op.getRedirectUri(); |
| return autoDiscoverInternal(redirectUri, attempt, username, password, canRetry); |
| } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) { |
| if (canRetry && username.contains("@")) { |
| // Try again using the bare user name |
| final int atSignIndex = username.indexOf('@'); |
| final String bareUsername = username.substring(0, atSignIndex); |
| LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex); |
| // Try again recursively, but this time don't allow retries for username. |
| return autoDiscoverInternal(uri, attempt, bareUsername, password, false); |
| } else { |
| // Either we're already on our second try or the username didn't have an "@" |
| // to begin with. Either way, failure. |
| final Bundle bundle = new Bundle(1); |
| bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, |
| EasAutoDiscover.RESULT_OTHER_FAILURE); |
| return bundle; |
| } |
| } else if (result != EasAutoDiscover.RESULT_OK) { |
| // Return failure, we'll try again with an alternate address |
| final Bundle bundle = new Bundle(1); |
| bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, |
| EasAutoDiscover.RESULT_BAD_RESPONSE); |
| return bundle; |
| } |
| // Success. |
| return op.getResultBundle(); |
| } |
| |
| @Override |
| public void setLogging(final int flags) { |
| // TODO: This isn't persisted. If Exchange goes down and restarts, debugging will |
| // be turned off. |
| sProtocolLogging = ((flags & EmailServiceProxy.DEBUG_EXCHANGE_BIT) != 0); |
| sFileLogging = ((flags & EmailServiceProxy.DEBUG_FILE_BIT) != 0); |
| LogUtils.d(TAG, "IEmailService.setLogging %d", flags); |
| } |
| |
| @Override |
| public void deleteExternalAccountPIMData(final String emailAddress) { |
| LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); |
| if (emailAddress != null) { |
| // TODO: stop pings |
| final Context context = EasService.this; |
| EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress); |
| EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress); |
| } |
| } |
| |
| public int getApiVersion() { |
| return EmailServiceVersion.CURRENT; |
| } |
| }; |
| |
| /** |
| * Content selection string for getting all accounts that are configured for push. |
| * TODO: Add protocol check so that we don't get e.g. IMAP accounts here. |
| * (Not currently necessary but eventually will be.) |
| */ |
| private static final String PUSH_ACCOUNTS_SELECTION = |
| EmailContent.AccountColumns.SYNC_INTERVAL + |
| "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); |
| |
| /** {@link AsyncTask} to restart pings for all accounts that need it. */ |
| private class RestartPingsTask extends AsyncTask<Void, Void, Void> { |
| private boolean mHasRestartedPing = false; |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI, |
| Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); |
| if (c != null) { |
| try { |
| while (c.moveToNext()) { |
| final Account account = new Account(); |
| LogUtils.d(TAG, "RestartPingsTask starting ping for %s", account); |
| account.restore(c); |
| if (EasService.this.pingNeededForAccount(account)) { |
| mHasRestartedPing = true; |
| EasService.this.mSynchronizer.pushModify(account); |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void result) { |
| if (!mHasRestartedPing) { |
| LogUtils.d(TAG, "RestartPingsTask did not start any pings."); |
| EasService.this.mSynchronizer.stopServiceIfIdle(); |
| } |
| } |
| } |
| |
| public EasService() { |
| super(); |
| mSynchronizer = new PingSyncSynchronizer(this); |
| sProtocolLogging = false; |
| sFileLogging = false; |
| } |
| |
| @Override |
| public void onCreate() { |
| LogUtils.d(TAG, "EasService.onCreate"); |
| super.onCreate(); |
| TempDirectory.setTempDirectory(this); |
| EmailContent.init(this); |
| AUTHORITIES_TO_SYNC = new String[] { |
| EmailContent.AUTHORITY, |
| CalendarContract.AUTHORITY, |
| ContactsContract.AUTHORITY |
| }; |
| |
| // Restart push for all accounts that need it. Because this requires DB loads, we do it in |
| // an AsyncTask, and we startService to ensure that we stick around long enough for the |
| // task to complete. The task will stop the service if necessary after it's done. |
| startService(new Intent(this, EasService.class)); |
| new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| @Override |
| public void onDestroy() { |
| mSynchronizer.stopAllPings(); |
| } |
| |
| @Override |
| public IBinder onBind(final Intent intent) { |
| return mBinder; |
| } |
| |
| @Override |
| public int onStartCommand(final Intent intent, final int flags, final int startId) { |
| if (intent != null && |
| TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) { |
| if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) { |
| // We've been asked to forcibly shutdown. This happens if email accounts are |
| // deleted, otherwise we can get errors if services are still running for |
| // accounts that are now gone. |
| // TODO: This is kind of a hack, it would be nicer if we could handle it correctly |
| // if accounts disappear out from under us. |
| LogUtils.d(TAG, "Forced shutdown, killing process"); |
| System.exit(-1); |
| } else if (intent.getBooleanExtra(EXTRA_START_PING, false)) { |
| LogUtils.d(LogUtils.TAG, "Restarting ping"); |
| final Account account = intent.getParcelableExtra(EXTRA_PING_ACCOUNT); |
| mSynchronizer.pushModify(account); |
| } |
| } |
| return START_STICKY; |
| } |
| |
| public int doOperation(final EasOperation operation, final String loggingName) { |
| LogUtils.d(TAG, "%s: %d", loggingName, operation.getAccountId()); |
| mSynchronizer.syncStart(operation.getAccountId()); |
| int result = EasOperation.RESULT_MIN_OK_RESULT; |
| // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA |
| // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls. |
| // If we add a wakelock (or anything else for that matter) here, must remember to undo |
| // it in the finally block below. |
| // On the other hand, even for SAs, it doesn't hurt to get a wakelock here. |
| try { |
| result = operation.performOperation(); |
| LogUtils.d(TAG, "Operation result %d", result); |
| return result; |
| } finally { |
| mSynchronizer.syncEnd(result < EasOperation.RESULT_MIN_OK_RESULT, |
| operation.getAccount()); |
| } |
| } |
| |
| /** |
| * Determine whether this account is configured with folders that are ready for push |
| * notifications. |
| * @param account The {@link Account} that we're interested in. |
| * @return Whether this account needs to ping. |
| */ |
| public boolean pingNeededForAccount(final Account account) { |
| // Check account existence. |
| if (account == null || account.mId == Account.NO_ACCOUNT) { |
| LogUtils.d(TAG, "Do not ping: Account not found or not valid"); |
| return false; |
| } |
| |
| // Check if account is configured for a push sync interval. |
| if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) { |
| LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId); |
| return false; |
| } |
| |
| // Check security hold status of the account. |
| if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { |
| LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId); |
| return false; |
| } |
| |
| // Check if the account has performed at least one sync so far (accounts must perform |
| // the initial sync before push is possible). |
| if (EmailContent.isInitialSyncKey(account.mSyncKey)) { |
| LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId); |
| return false; |
| } |
| |
| // Check that there's at least one mailbox that is both configured for push notifications, |
| // and whose content type is enabled for sync in the account manager. |
| final android.accounts.Account amAccount = new android.accounts.Account( |
| account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| |
| final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC); |
| // If we have at least one sync-enabled content type, check for syncing mailboxes. |
| if (!authsToSync.isEmpty()) { |
| final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId); |
| if (c != null) { |
| try { |
| while (c.moveToNext()) { |
| final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); |
| if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { |
| return true; |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| } |
| LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId); |
| return false; |
| } |
| |
| static public GalResult searchGal(final Context context, final long accountId, |
| final String filter, final int limit) { |
| final EasSearchGal operation = new EasSearchGal(context, accountId, filter, limit); |
| // We don't use doOperation() here for two reasons: |
| // 1. This is a static function, doOperation is not, and we don't have an instance of |
| // EasService. |
| // 2. All doOperation() does besides this is stop the ping and then restart it. This is |
| // required during syncs, but not for GalSearches. |
| final int result = operation.performOperation(); |
| if (result == EasSearchGal.RESULT_OK) { |
| return operation.getResult(); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Converts from an EasOperation status to a status code defined in EmailServiceStatus. |
| * This is used to communicate the status of a sync operation to the caller. |
| * @param easStatus result returned from an EasOperation |
| * @return EmailServiceStatus |
| */ |
| private int convertToEmailServiceStatus(int easStatus) { |
| if (easStatus >= EasOperation.RESULT_MIN_OK_RESULT) { |
| return EmailServiceStatus.SUCCESS; |
| } |
| switch (easStatus) { |
| case EasOperation.RESULT_ABORT: |
| case EasOperation.RESULT_RESTART: |
| // This should only happen if a ping is interruped for some reason. We would not |
| // expect see that here, since this should only be called for a sync. |
| LogUtils.e(TAG, "Abort or Restart easStatus"); |
| return EmailServiceStatus.SUCCESS; |
| |
| case EasOperation.RESULT_TOO_MANY_REDIRECTS: |
| return EmailServiceStatus.INTERNAL_ERROR; |
| |
| case EasOperation.RESULT_NETWORK_PROBLEM: |
| // This is due to an IO error, we need the caller to know about this so that it |
| // can let the syncManager know. |
| return EmailServiceStatus.IO_ERROR; |
| |
| case EasOperation.RESULT_FORBIDDEN: |
| case EasOperation.RESULT_AUTHENTICATION_ERROR: |
| return EmailServiceStatus.LOGIN_FAILED; |
| |
| case EasOperation.RESULT_PROVISIONING_ERROR: |
| return EmailServiceStatus.PROVISIONING_ERROR; |
| |
| case EasOperation.RESULT_CLIENT_CERTIFICATE_REQUIRED: |
| return EmailServiceStatus.CLIENT_CERTIFICATE_ERROR; |
| |
| case EasOperation.RESULT_PROTOCOL_VERSION_UNSUPPORTED: |
| return EmailServiceStatus.PROTOCOL_ERROR; |
| |
| case EasOperation.RESULT_INITIALIZATION_FAILURE: |
| case EasOperation.RESULT_HARD_DATA_FAILURE: |
| case EasOperation.RESULT_OTHER_FAILURE: |
| return EmailServiceStatus.INTERNAL_ERROR; |
| |
| case EasOperation.RESULT_NON_FATAL_ERROR: |
| // We do not expect to see this error here: This should be consumed in |
| // EasFullSyncOperation. The only case this occurs in is when we try to send |
| // a message in the outbox, and there's some problem with the message locally |
| // that prevents it from being sent. We return a |
| LogUtils.e(TAG, "Other non-fatal error easStatus %d", easStatus); |
| return EmailServiceStatus.SUCCESS; |
| } |
| LogUtils.e(TAG, "Unexpected easStatus %d", easStatus); |
| return EmailServiceStatus.INTERNAL_ERROR; |
| } |
| |
| |
| /** |
| * Determine which content types are set to sync for an account. |
| * @param account The account whose sync settings we're looking for. |
| * @param authorities All possible authorities we could care about. |
| * @return The authorities for the content types we want to sync for account. |
| */ |
| public static Set<String> getAuthoritiesToSync(final android.accounts.Account account, |
| final String[] authorities) { |
| final HashSet<String> authsToSync = new HashSet(); |
| for (final String authority : authorities) { |
| if (ContentResolver.getSyncAutomatically(account, authority)) { |
| authsToSync.add(authority); |
| } |
| } |
| return authsToSync; |
| } |
| |
| @VisibleForTesting |
| public static void setProtocolLogging(final boolean val) { |
| sProtocolLogging = val; |
| } |
| |
| @VisibleForTesting |
| public static void setFileLogging(final boolean val) { |
| sFileLogging = val; |
| } |
| |
| public static boolean getProtocolLogging() { |
| return sProtocolLogging; |
| } |
| |
| public static boolean getFileLogging() { |
| return sFileLogging; |
| } |
| |
| } |