| /* |
| * Copyright (C) 2013 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.eas; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.SyncResult; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| |
| 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.utility.Utility; |
| import com.android.exchange.CommandStatusException; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasResponse; |
| import com.android.exchange.adapter.Serializer; |
| import com.android.exchange.adapter.Tags; |
| import com.android.exchange.service.EasServerConnection; |
| import com.android.mail.providers.UIProvider; |
| import com.android.mail.utils.LogUtils; |
| |
| import org.apache.http.HttpEntity; |
| import org.apache.http.client.methods.HttpUriRequest; |
| import org.apache.http.entity.ByteArrayEntity; |
| |
| import java.io.IOException; |
| import java.security.cert.CertificateException; |
| import java.util.ArrayList; |
| |
| /** |
| * Base class for all Exchange operations that use a POST to talk to the server. |
| * |
| * The core of this class is {@link #performOperation}, which provides the skeleton of making |
| * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one. |
| * This class abstracts the connection handling from its subclasses and callers. |
| * |
| * {@link #performOperation} calls various abstract functions to create the request and parse the |
| * response. For the most part subclasses can implement just these bits of functionality and rely |
| * on {@link #performOperation} to do all the boilerplate etc. |
| * |
| * There are also a set of functions that a subclass may override if it's substantially |
| * different from the "normal" operation (e.g. autodiscover deviates from the standard URI since |
| * it's not account-specific so it needs to override {@link #getRequestUri()}), but the default |
| * implementations of these functions should suffice for most operations. |
| * |
| * Some subclasses may need to override {@link #performOperation} to add validation and results |
| * processing around a call to super.performOperation. Subclasses should avoid doing too much more |
| * than wrapping some handling around the chained call; if you find that's happening, it's likely |
| * a sign that the base class needs to be enhanced. |
| * |
| * One notable reason this wrapping happens is for operations that need to return a result directly |
| * to their callers (as opposed to simply writing the results to the provider, as is common with |
| * sync operations). This happens for example in |
| * {@link com.android.emailcommon.service.IEmailService} message handlers. In such cases, due to |
| * how {@link com.android.exchange.service.EasService} uses this class, the subclass needs to |
| * store the result as a member variable and then provide an accessor to read the result. Since |
| * different operations have different results (or none at all), there is no function in the base |
| * class for this. |
| * |
| * Note that it is not practical to avoid the race between when an operation loads its account data |
| * and when it uses it, as that would require some form of locking in the provider. There are three |
| * interesting situations where this might happen, and that this class must handle: |
| * |
| * 1) Deleted from provider: Any subsequent provider access should return an error. Operations |
| * must detect this and terminate with an error. |
| * 2) Account sync settings change: Generally only affects Ping. We interrupt the operation and |
| * load the new settings before proceeding. |
| * 3) Sync suspended due to hold: A special case of the previous, and affects all operations, but |
| * fortunately doesn't need special handling here. Correct provider functionality must generate |
| * write failures, so the handling for #1 should cover this case as well. |
| * |
| * This class attempts to defer loading of account data as long as possible -- ideally we load |
| * immediately before the network request -- but does not proactively check for changes after that. |
| * This approach is a a practical balance between minimizing the race without adding too much |
| * complexity beyond what's required. |
| */ |
| public abstract class EasOperation { |
| public static final String LOG_TAG = Eas.LOG_TAG; |
| |
| /** The maximum number of server redirects we allow before returning failure. */ |
| private static final int MAX_REDIRECTS = 3; |
| |
| /** Message MIME type for EAS version 14 and later. */ |
| private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml"; |
| |
| /** |
| * EasOperation error codes below. All subclasses should try to create error codes |
| * that do not overlap these codes or the codes of other subclasses. The error |
| * code values for each subclass should start in a different 100 range (i.e. -100, |
| * -200, etc...). |
| */ |
| |
| /** Error code indicating the operation was cancelled via {@link #abort}. */ |
| public static final int RESULT_ABORT = -1; |
| /** Error code indicating the operation was cancelled via {@link #restart}. */ |
| public static final int RESULT_RESTART = -2; |
| /** Error code indicating the Exchange servers redirected too many times. */ |
| public static final int RESULT_TOO_MANY_REDIRECTS = -3; |
| /** Error code indicating the request failed due to a network problem. */ |
| public static final int RESULT_REQUEST_FAILURE = -4; |
| /** Error code indicating a 403 (forbidden) error. */ |
| public static final int RESULT_FORBIDDEN = -5; |
| /** Error code indicating an unresolved provisioning error. */ |
| public static final int RESULT_PROVISIONING_ERROR = -6; |
| /** Error code indicating an authentication problem. */ |
| public static final int RESULT_AUTHENTICATION_ERROR = -7; |
| /** Error code indicating the client is missing a certificate. */ |
| public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8; |
| /** Error code indicating we don't have a protocol version in common with the server. */ |
| public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9; |
| /** Error code indicating a hard error when initializing the operation. */ |
| public static final int RESULT_INITIALIZATION_FAILURE = -10; |
| /** Error code indicating a hard data layer error. */ |
| public static final int RESULT_HARD_DATA_FAILURE = -11; |
| /** Error code indicating that this operation failed, but we should not abort the sync */ |
| /** TODO: This is currently only used in EasOutboxSync, no other place handles it correctly */ |
| public static final int RESULT_NON_FATAL_ERROR = -12; |
| /** Error code indicating some other failure. */ |
| public static final int RESULT_OTHER_FAILURE = -99; |
| /** Constant to delimit where op specific error codes begin. */ |
| public static final int RESULT_OP_SPECIFIC_ERROR_RESULT = -100; |
| |
| protected final Context mContext; |
| |
| /** The provider id for the account this operation is on. */ |
| private final long mAccountId; |
| |
| /** The cached {@link Account} state; can be null if it hasn't been loaded yet. */ |
| protected Account mAccount; |
| |
| /** The connection to use for this operation. This is created when {@link #mAccount} is set. */ |
| private EasServerConnection mConnection; |
| |
| public class MessageInvalidException extends Exception { |
| public MessageInvalidException(final String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * Constructor which defers loading of account and connection info. |
| * @param context |
| * @param accountId |
| */ |
| protected EasOperation(final Context context, final long accountId) { |
| mContext = context; |
| mAccountId = accountId; |
| } |
| |
| protected EasOperation(final Context context, final Account account, |
| final EasServerConnection connection) { |
| this(context, account.mId); |
| mAccount = account; |
| mConnection = connection; |
| } |
| |
| protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) { |
| this(context, account, new EasServerConnection(context, account, hostAuth)); |
| } |
| |
| protected EasOperation(final Context context, final Account account) { |
| this(context, account, account.getOrCreateHostAuthRecv(context)); |
| } |
| |
| /** |
| * This constructor is for use by operations that are created by other operations, e.g. |
| * {@link EasProvision}. It reuses the account and connection of its parent. |
| * @param parentOperation The {@link EasOperation} that is creating us. |
| */ |
| protected EasOperation(final EasOperation parentOperation) { |
| mContext = parentOperation.mContext; |
| mAccountId = parentOperation.mAccountId; |
| mAccount = parentOperation.mAccount; |
| mConnection = parentOperation.mConnection; |
| } |
| |
| /** |
| * Some operations happen before the account exists (e.g. account validation). |
| * These operations cannot use {@link #init}, so instead we make a dummy account and |
| * supply a temporary {@link HostAuth}. |
| * @param hostAuth |
| */ |
| protected final void setDummyAccount(final HostAuth hostAuth) { |
| mAccount = new Account(); |
| mAccount.mEmailAddress = hostAuth.mLogin; |
| mConnection = new EasServerConnection(mContext, mAccount, hostAuth); |
| } |
| |
| /** |
| * Loads (or reloads) the {@link Account} for this operation, and sets up our connection to the |
| * server. This can be overridden to add additional functionality, but child implementations |
| * should always call super(). |
| * @param allowReload If false, do not perform a load if we already have an {@link Account} |
| * (i.e. just keep the existing one); otherwise allow replacement of the |
| * account. Note that this can result in a valid Account being replaced with |
| * null if the account no longer exists. |
| * @return Whether we now have a valid {@link Account} object. |
| */ |
| public boolean init(final boolean allowReload) { |
| if (mAccount == null || allowReload) { |
| mAccount = Account.restoreAccountWithId(mContext, getAccountId()); |
| if (mAccount != null) { |
| mConnection = new EasServerConnection(mContext, mAccount, |
| mAccount.getOrCreateHostAuthRecv(mContext)); |
| } |
| } |
| return (mAccount != null); |
| } |
| |
| public final long getAccountId() { |
| return mAccountId; |
| } |
| |
| public final Account getAccount() { |
| return mAccount; |
| } |
| |
| /** |
| * Request that this operation terminate. Intended for use by the sync service to interrupt |
| * running operations, primarily Ping. |
| */ |
| public final void abort() { |
| mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT); |
| } |
| |
| /** |
| * Request that this operation restart. Intended for use by the sync service to interrupt |
| * running operations, primarily Ping. |
| */ |
| public final void restart() { |
| mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART); |
| } |
| |
| /** |
| * The skeleton of performing an operation. This function handles all the common code and |
| * error handling, calling into virtual functions that are implemented or overridden by the |
| * subclass to do the operation-specific logic. |
| * |
| * The result codes work as follows: |
| * - Negative values indicate common error codes and are defined above (the various RESULT_* |
| * constants). |
| * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously |
| * specific to the subclass, and may indicate success or error conditions. |
| * |
| * The common error codes primarily indicate conditions that occur when performing the POST |
| * itself, such as network errors and handling of the HTTP response. However, some errors that |
| * can be indicated in the HTTP response code can also be indicated in the payload of the |
| * response as well, so {@link #handleResponse} should in those cases return the appropriate |
| * negative result code, which will be handled the same as if it had been indicated in the HTTP |
| * response code. |
| * |
| * @return A result code for the outcome of this operation, as described above. |
| */ |
| public int performOperation() { |
| // Make sure the account is loaded if it hasn't already been. |
| if (!init(false)) { |
| LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s", |
| getAccountId(), getCommand()); |
| return RESULT_INITIALIZATION_FAILURE; |
| } |
| |
| // We handle server redirects by looping, but we need to protect against too much looping. |
| int redirectCount = 0; |
| |
| do { |
| // Perform the HTTP request and handle exceptions. |
| final EasResponse response; |
| try { |
| try { |
| response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout()); |
| } finally { |
| onRequestMade(); |
| } |
| } catch (final IOException e) { |
| // If we were stopped, return the appropriate result code. |
| switch (mConnection.getStoppedReason()) { |
| case EasServerConnection.STOPPED_REASON_ABORT: |
| return RESULT_ABORT; |
| case EasServerConnection.STOPPED_REASON_RESTART: |
| return RESULT_RESTART; |
| default: |
| break; |
| } |
| // If we're here, then we had a IOException that's not from a stop request. |
| String message = e.getMessage(); |
| if (message == null) { |
| message = "(no message)"; |
| } |
| LogUtils.i(LOG_TAG, "IOException while sending request: %s", message); |
| return RESULT_REQUEST_FAILURE; |
| } catch (final CertificateException e) { |
| LogUtils.i(LOG_TAG, "CertificateException while sending request: %s", |
| e.getMessage()); |
| return RESULT_CLIENT_CERTIFICATE_REQUIRED; |
| } catch (final MessageInvalidException e) { |
| LogUtils.d(LOG_TAG, "Exception sending request %s", e.getMessage()); |
| return RESULT_NON_FATAL_ERROR; |
| } catch (final IllegalStateException e) { |
| // Subclasses use ISE to signal a hard error when building the request. |
| // TODO: Switch away from ISEs. |
| LogUtils.e(LOG_TAG, e, "Exception while sending request"); |
| return RESULT_HARD_DATA_FAILURE; |
| } |
| |
| // The POST completed, so process the response. |
| try { |
| final int result; |
| // First off, the success case. |
| if (response.isSuccess()) { |
| int responseResult; |
| try { |
| responseResult = handleResponse(response); |
| } catch (final IOException e) { |
| LogUtils.e(LOG_TAG, e, "Exception while handling response"); |
| return RESULT_REQUEST_FAILURE; |
| } catch (final CommandStatusException e) { |
| // For some operations (notably Sync & FolderSync), errors are signaled in |
| // the payload of the response. These will have a HTTP 200 response, and the |
| // error condition is only detected during response parsing. |
| // The various parsers handle this by throwing a CommandStatusException. |
| // TODO: Consider having the parsers return the errors instead of throwing. |
| final int status = e.mStatus; |
| LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status); |
| if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) { |
| responseResult = RESULT_PROVISIONING_ERROR; |
| } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) { |
| responseResult = RESULT_FORBIDDEN; |
| } else { |
| responseResult = RESULT_OTHER_FAILURE; |
| } |
| } |
| result = responseResult; |
| } else { |
| result = handleHttpError(response.getStatus()); |
| } |
| |
| // Non-negative results indicate success. Return immediately and bypass the error |
| // handling. |
| if (result >= 0) { |
| return result; |
| } |
| |
| // If this operation has distinct handling for 403 errors, do that. |
| if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) { |
| LogUtils.e(LOG_TAG, "Forbidden response"); |
| return RESULT_FORBIDDEN; |
| } |
| |
| // Handle provisioning errors. |
| if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) { |
| if (handleProvisionError()) { |
| // The provisioning error has been taken care of, so we should re-do this |
| // request. |
| LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying", |
| getCommand()); |
| continue; |
| } |
| return RESULT_PROVISIONING_ERROR; |
| } |
| |
| // Handle authentication errors. |
| if (response.isAuthError()) { |
| LogUtils.e(LOG_TAG, "Authentication error"); |
| if (response.isMissingCertificate()) { |
| return RESULT_CLIENT_CERTIFICATE_REQUIRED; |
| } |
| return RESULT_AUTHENTICATION_ERROR; |
| } |
| |
| // Handle redirects. |
| if (response.isRedirectError()) { |
| ++redirectCount; |
| mConnection.redirectHostAuth(response.getRedirectAddress()); |
| // Note that unlike other errors, we do NOT return here; we just keep looping. |
| } else { |
| // All other errors. |
| LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d", |
| getCommand(), response.getStatus(), result); |
| // TODO: This probably should return result. |
| return RESULT_OTHER_FAILURE; |
| } |
| } finally { |
| response.close(); |
| } |
| } while (redirectCount < MAX_REDIRECTS); |
| |
| // Non-redirects return immediately after handling, so the only way to reach here is if we |
| // looped too many times. |
| LogUtils.e(LOG_TAG, "Too many redirects"); |
| return RESULT_TOO_MANY_REDIRECTS; |
| } |
| |
| protected void onRequestMade() { |
| // This can be overridden to do any cleanup that must happen after the request has |
| // been sent. It will always be called, regardless of the status of the request. |
| } |
| |
| protected int handleHttpError(final int httpStatus) { |
| // This function can be overriden if the child class needs to change the result code |
| // based on the http response status. |
| return RESULT_OTHER_FAILURE; |
| } |
| |
| /** |
| * Reset the protocol version to use for this connection. If it's changed, and our account is |
| * persisted, also write back the changes to the DB. |
| * @param protocolVersion The new protocol version to use, as a string. |
| */ |
| protected final void setProtocolVersion(final String protocolVersion) { |
| final long accountId = getAccountId(); |
| if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) { |
| final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); |
| final ContentValues cv = new ContentValues(2); |
| if (getProtocolVersion() >= 12.0) { |
| final int oldFlags = Utility.getFirstRowInt(mContext, uri, |
| Account.ACCOUNT_FLAGS_PROJECTION, null, null, null, |
| Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0); |
| final int newFlags = oldFlags |
| | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH; |
| if (oldFlags != newFlags) { |
| cv.put(EmailContent.AccountColumns.FLAGS, newFlags); |
| } |
| } |
| cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion); |
| mContext.getContentResolver().update(uri, cv, null, null); |
| } |
| } |
| |
| /** |
| * Create the request object for this operation. |
| * Most operations use a POST, but some use other request types (e.g. Options). |
| * @return An {@link HttpUriRequest}. |
| * @throws IOException |
| */ |
| private final HttpUriRequest makeRequest() throws IOException, MessageInvalidException { |
| final String requestUri = getRequestUri(); |
| if (requestUri == null) { |
| return mConnection.makeOptions(); |
| } |
| return mConnection.makePost(requestUri, getRequestEntity(), |
| getRequestContentType(), addPolicyKeyHeaderToRequest()); |
| } |
| |
| /** |
| * The following functions MUST be overridden by subclasses; these are things that are unique |
| * to each operation. |
| */ |
| |
| /** |
| * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note |
| * that if you override {@link #getRequestUri}, then this function may be unused for normal |
| * operation, but all subclasses should return something non-null for use with logging. |
| * @return The name of the command for this operation as defined by the EAS protocol, or for |
| * commands that don't need it, a suitable descriptive name for logging. |
| */ |
| protected abstract String getCommand(); |
| |
| /** |
| * Build the {@link HttpEntity} which is used to construct the POST. Typically this function |
| * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}. |
| * If the subclass is not using a POST, then it should override this to return null. |
| * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}. |
| * @throws IOException |
| */ |
| protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException; |
| |
| /** |
| * Parse the response from the Exchange perform whatever actions are dictated by that. |
| * @param response The {@link EasResponse} to our request. |
| * @return A result code. Non-negative values are returned directly to the caller; negative |
| * values |
| * |
| * that is returned to the caller of {@link #performOperation}. |
| * @throws IOException |
| */ |
| protected abstract int handleResponse(final EasResponse response) |
| throws IOException, CommandStatusException; |
| |
| /** |
| * The following functions may be overriden by a subclass, but most operations will not need |
| * to do so. |
| */ |
| |
| /** |
| * Get the URI for the Exchange server and this operation. Most (signed in) operations need |
| * not override this; the notable operation that needs to override it is auto-discover. |
| * @return |
| */ |
| protected String getRequestUri() { |
| return mConnection.makeUriString(getCommand()); |
| } |
| |
| /** |
| * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header. |
| */ |
| protected boolean addPolicyKeyHeaderToRequest() { |
| return true; |
| } |
| |
| /** |
| * @return The content type of this request. |
| */ |
| protected String getRequestContentType() { |
| return EAS_14_MIME_TYPE; |
| } |
| |
| /** |
| * @return The timeout to use for the POST. |
| */ |
| protected long getTimeout() { |
| return 30 * DateUtils.SECOND_IN_MILLIS; |
| } |
| |
| /** |
| * If 403 responses should be handled in a special way, this function should be overridden to |
| * do that. |
| * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error. |
| */ |
| protected boolean handleForbidden() { |
| return false; |
| } |
| |
| /** |
| * Handle a provisioning error. Subclasses may override this to do something different, e.g. |
| * to validate rather than actually do the provisioning. |
| * @return |
| */ |
| protected boolean handleProvisionError() { |
| final EasProvision provisionOperation = new EasProvision(this); |
| return provisionOperation.provision(); |
| } |
| |
| /** |
| * Convenience methods for subclasses to use. |
| */ |
| |
| /** |
| * Convenience method to make an {@link HttpEntity} from {@link Serializer}. |
| */ |
| protected final HttpEntity makeEntity(final Serializer s) { |
| return new ByteArrayEntity(s.toByteArray()); |
| } |
| |
| /** |
| * Check whether we should ask the server what protocol versions it supports and set this |
| * account to use that version. |
| * @return Whether we need a new protocol version from the server. |
| */ |
| protected final boolean shouldGetProtocolVersion() { |
| // TODO: Find conditions under which we should check other than not having one yet. |
| return !mConnection.isProtocolVersionSet(); |
| } |
| |
| /** |
| * @return The protocol version to use. |
| */ |
| protected final double getProtocolVersion() { |
| return mConnection.getProtocolVersion(); |
| } |
| |
| /** |
| * @return Our useragent. |
| */ |
| protected final String getUserAgent() { |
| return mConnection.getUserAgent(); |
| } |
| |
| /** |
| * @return Whether we succeeeded in registering the client cert. |
| */ |
| protected final boolean registerClientCert() { |
| return mConnection.registerClientCert(); |
| } |
| |
| /** |
| * Add the device information to the current request. |
| * @param s The {@link Serializer} for our current request. |
| * @param context The {@link Context} for current device. |
| * @param userAgent The user agent string that our connection use. |
| */ |
| protected static void expandedAddDeviceInformationToSerializer(final Serializer s, |
| final Context context, final String userAgent) throws IOException { |
| final String deviceId; |
| final String phoneNumber; |
| final String operator; |
| final TelephonyManager tm = (TelephonyManager)context.getSystemService( |
| Context.TELEPHONY_SERVICE); |
| if (tm != null) { |
| deviceId = tm.getDeviceId(); |
| phoneNumber = tm.getLine1Number(); |
| // TODO: This is not perfect and needs to be improved, for at least two reasons: |
| // 1) SIM cards can override this name. |
| // 2) We don't resend this info to the server when we change networks. |
| final String operatorName = tm.getNetworkOperatorName(); |
| final String operatorNumber = tm.getNetworkOperator(); |
| if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) { |
| operator = operatorName + " (" + operatorNumber + ")"; |
| } else if (!TextUtils.isEmpty(operatorName)) { |
| operator = operatorName; |
| } else { |
| operator = operatorNumber; |
| } |
| } else { |
| deviceId = null; |
| phoneNumber = null; |
| operator = null; |
| } |
| |
| // TODO: Right now, we won't send this information unless the device is provisioned again. |
| // Potentially, this means that our phone number could be out of date if the user |
| // switches sims. Is there something we can do to force a reprovision? |
| s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET); |
| s.data(Tags.SETTINGS_MODEL, Build.MODEL); |
| if (deviceId != null) { |
| s.data(Tags.SETTINGS_IMEI, tm.getDeviceId()); |
| } |
| // Set the device friendly name, if we have one. |
| // TODO: Longer term, this should be done without a provider call. |
| final Bundle deviceName = context.getContentResolver().call( |
| EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null); |
| if (deviceName != null) { |
| final String friendlyName = deviceName.getString(EmailContent.DEVICE_FRIENDLY_NAME); |
| if (!TextUtils.isEmpty(friendlyName)) { |
| s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName); |
| } |
| } |
| s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE); |
| if (phoneNumber != null) { |
| s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber); |
| } |
| // TODO: Consider setting this, but make sure we know what it's used for. |
| // If the user changes the device's locale and we don't do a reprovision, the server's |
| // idea of the language will be wrong. Since we're not sure what this is used for, |
| // right now we're leaving it out. |
| //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage()); |
| s.data(Tags.SETTINGS_USER_AGENT, userAgent); |
| if (operator != null) { |
| s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator); |
| } |
| s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION |
| } |
| |
| /** |
| * Add the device information to the current request. |
| * @param s The {@link Serializer} that contains the payload for this request. |
| */ |
| protected final void addDeviceInformationToSerializer(final Serializer s) |
| throws IOException { |
| final String userAgent = getUserAgent(); |
| expandedAddDeviceInformationToSerializer(s, mContext, userAgent); |
| } |
| |
| /** |
| * Convenience method for adding a Message to an account's outbox |
| * @param account The {@link Account} from which to send the message. |
| * @param msg the message to send |
| */ |
| protected final void sendMessage(final Account account, final EmailContent.Message msg) { |
| long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); |
| // TODO: Improve system mailbox handling. |
| if (mailboxId == Mailbox.NO_MAILBOX) { |
| LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId); |
| final Mailbox outbox = |
| Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX); |
| outbox.save(mContext); |
| mailboxId = outbox.mId; |
| } |
| msg.mMailboxKey = mailboxId; |
| msg.mAccountKey = account.mId; |
| msg.save(mContext); |
| requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress, |
| Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId); |
| } |
| |
| /** |
| * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox. |
| * @param amAccount The {@link android.accounts.Account} for the account we're pinging. |
| * @param mailboxId The id of the mailbox that needs to sync. |
| */ |
| protected static void requestSyncForMailbox(final android.accounts.Account amAccount, |
| final long mailboxId) { |
| final Bundle extras = Mailbox.createSyncBundle(mailboxId); |
| ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); |
| LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s", |
| amAccount.toString(), extras.toString()); |
| } |
| |
| protected static void requestSyncForMailboxes(final android.accounts.Account amAccount, |
| final ArrayList<Long> mailboxIds) { |
| final Bundle extras = Mailbox.createSyncBundle(mailboxIds); |
| /** |
| * Please note that it is very possible that we are trying to send a request to the |
| * email sync adapter even though email push is turned off (i.e. this account might only |
| * be syncing calendar or contacts). In this situation we need to make sure that |
| * this request is marked as manual as to ensure that the sync manager does not drop it |
| * on the floor. Right now, this function is only called by EasPing, if it is every called |
| * by another caller, then we should reconsider if manual=true is the right thing to do. |
| */ |
| extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); |
| ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); |
| LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailboxes %s, %s", |
| amAccount.toString(), extras.toString()); |
| } |
| |
| /** |
| * RequestNoOpSync |
| * This requests a sync for a particular authority purely so that that account |
| * in settings will recognize that it is trying to sync, and will display the |
| * appropriate UI. In fact, all exchange data syncing actually happens through the |
| * EmailSyncAdapterService. |
| * @param amAccount |
| * @param authority |
| */ |
| protected static void requestNoOpSync(final android.accounts.Account amAccount, |
| final String authority) { |
| final Bundle extras = new Bundle(1); |
| extras.putBoolean(Mailbox.SYNC_EXTRA_NOOP, true); |
| ContentResolver.requestSync(amAccount, authority, extras); |
| LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s", |
| amAccount.toString(), extras.toString()); |
| } |
| |
| /** |
| * Interpret a result code from an {@link EasOperation} and, if it's an error, write it to |
| * the appropriate field in {@link SyncResult}. |
| * @param result |
| * @param syncResult |
| * @return Whether an error code was written to syncResult. |
| */ |
| public static boolean writeResultToSyncResult(final int result, final SyncResult syncResult) { |
| switch (result) { |
| case RESULT_TOO_MANY_REDIRECTS: |
| syncResult.tooManyRetries = true; |
| return true; |
| case RESULT_REQUEST_FAILURE: |
| syncResult.stats.numIoExceptions = 1; |
| return true; |
| case RESULT_FORBIDDEN: |
| case RESULT_PROVISIONING_ERROR: |
| case RESULT_AUTHENTICATION_ERROR: |
| case RESULT_CLIENT_CERTIFICATE_REQUIRED: |
| syncResult.stats.numAuthExceptions = 1; |
| return true; |
| case RESULT_PROTOCOL_VERSION_UNSUPPORTED: |
| // Only used in validate, so there's never a syncResult to write to here. |
| break; |
| case RESULT_INITIALIZATION_FAILURE: |
| case RESULT_HARD_DATA_FAILURE: |
| syncResult.databaseError = true; |
| return true; |
| case RESULT_OTHER_FAILURE: |
| // TODO: Is this correct? |
| syncResult.stats.numIoExceptions = 1; |
| return true; |
| } |
| return false; |
| } |
| |
| public static int translateSyncResultToUiResult(final int result) { |
| switch (result) { |
| case RESULT_TOO_MANY_REDIRECTS: |
| return UIProvider.LastSyncResult.INTERNAL_ERROR; |
| case RESULT_REQUEST_FAILURE: |
| return UIProvider.LastSyncResult.CONNECTION_ERROR; |
| case RESULT_FORBIDDEN: |
| case RESULT_PROVISIONING_ERROR: |
| case RESULT_AUTHENTICATION_ERROR: |
| case RESULT_CLIENT_CERTIFICATE_REQUIRED: |
| return UIProvider.LastSyncResult.AUTH_ERROR; |
| case RESULT_PROTOCOL_VERSION_UNSUPPORTED: |
| // Only used in validate, so there's never a syncResult to write to here. |
| break; |
| case RESULT_INITIALIZATION_FAILURE: |
| case RESULT_HARD_DATA_FAILURE: |
| return UIProvider.LastSyncResult.INTERNAL_ERROR; |
| case RESULT_OTHER_FAILURE: |
| return UIProvider.LastSyncResult.INTERNAL_ERROR; |
| } |
| return UIProvider.LastSyncResult.SUCCESS; |
| } |
| } |