Home | History | Annotate | Download | only in eas
      1 /*
      2  * Copyright (C) 2013 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.eas;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.SyncResult;
     24 import android.net.Uri;
     25 import android.os.Build;
     26 import android.os.Bundle;
     27 import android.telephony.TelephonyManager;
     28 import android.text.TextUtils;
     29 import android.text.format.DateUtils;
     30 
     31 import com.android.emailcommon.provider.Account;
     32 import com.android.emailcommon.provider.EmailContent;
     33 import com.android.emailcommon.provider.HostAuth;
     34 import com.android.emailcommon.provider.Mailbox;
     35 import com.android.emailcommon.utility.Utility;
     36 import com.android.exchange.CommandStatusException;
     37 import com.android.exchange.Eas;
     38 import com.android.exchange.EasResponse;
     39 import com.android.exchange.adapter.Serializer;
     40 import com.android.exchange.adapter.Tags;
     41 import com.android.exchange.service.EasServerConnection;
     42 import com.android.mail.providers.UIProvider;
     43 import com.android.mail.utils.LogUtils;
     44 
     45 import org.apache.http.HttpEntity;
     46 import org.apache.http.client.methods.HttpUriRequest;
     47 import org.apache.http.entity.ByteArrayEntity;
     48 
     49 import java.io.IOException;
     50 import java.security.cert.CertificateException;
     51 import java.util.ArrayList;
     52 
     53 /**
     54  * Base class for all Exchange operations that use a POST to talk to the server.
     55  *
     56  * The core of this class is {@link #performOperation}, which provides the skeleton of making
     57  * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one.
     58  * This class abstracts the connection handling from its subclasses and callers.
     59  *
     60  * {@link #performOperation} calls various abstract functions to create the request and parse the
     61  * response. For the most part subclasses can implement just these bits of functionality and rely
     62  * on {@link #performOperation} to do all the boilerplate etc.
     63  *
     64  * There are also a set of functions that a subclass may override if it's substantially
     65  * different from the "normal" operation (e.g. autodiscover deviates from the standard URI since
     66  * it's not account-specific so it needs to override {@link #getRequestUri()}), but the default
     67  * implementations of these functions should suffice for most operations.
     68  *
     69  * Some subclasses may need to override {@link #performOperation} to add validation and results
     70  * processing around a call to super.performOperation. Subclasses should avoid doing too much more
     71  * than wrapping some handling around the chained call; if you find that's happening, it's likely
     72  * a sign that the base class needs to be enhanced.
     73  *
     74  * One notable reason this wrapping happens is for operations that need to return a result directly
     75  * to their callers (as opposed to simply writing the results to the provider, as is common with
     76  * sync operations). This happens for example in
     77  * {@link com.android.emailcommon.service.IEmailService} message handlers. In such cases, due to
     78  * how {@link com.android.exchange.service.EasService} uses this class, the subclass needs to
     79  * store the result as a member variable and then provide an accessor to read the result. Since
     80  * different operations have different results (or none at all), there is no function in the base
     81  * class for this.
     82  *
     83  * Note that it is not practical to avoid the race between when an operation loads its account data
     84  * and when it uses it, as that would require some form of locking in the provider. There are three
     85  * interesting situations where this might happen, and that this class must handle:
     86  *
     87  * 1) Deleted from provider: Any subsequent provider access should return an error. Operations
     88  *    must detect this and terminate with an error.
     89  * 2) Account sync settings change: Generally only affects Ping. We interrupt the operation and
     90  *    load the new settings before proceeding.
     91  * 3) Sync suspended due to hold: A special case of the previous, and affects all operations, but
     92  *    fortunately doesn't need special handling here. Correct provider functionality must generate
     93  *    write failures, so the handling for #1 should cover this case as well.
     94  *
     95  * This class attempts to defer loading of account data as long as possible -- ideally we load
     96  * immediately before the network request -- but does not proactively check for changes after that.
     97  * This approach is a a practical balance between minimizing the race without adding too much
     98  * complexity beyond what's required.
     99  */
    100 public abstract class EasOperation {
    101     public static final String LOG_TAG = Eas.LOG_TAG;
    102 
    103     /** The maximum number of server redirects we allow before returning failure. */
    104     private static final int MAX_REDIRECTS = 3;
    105 
    106     /** Message MIME type for EAS version 14 and later. */
    107     private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml";
    108 
    109     /**
    110      * EasOperation error codes below.  All subclasses should try to create error codes
    111      * that do not overlap these codes or the codes of other subclasses. The error
    112      * code values for each subclass should start in a different 100 range (i.e. -100,
    113      * -200, etc...).
    114      */
    115 
    116     /** Error code indicating the operation was cancelled via {@link #abort}. */
    117     public static final int RESULT_ABORT = -1;
    118     /** Error code indicating the operation was cancelled via {@link #restart}. */
    119     public static final int RESULT_RESTART = -2;
    120     /** Error code indicating the Exchange servers redirected too many times. */
    121     public static final int RESULT_TOO_MANY_REDIRECTS = -3;
    122     /** Error code indicating the request failed due to a network problem. */
    123     public static final int RESULT_REQUEST_FAILURE = -4;
    124     /** Error code indicating a 403 (forbidden) error. */
    125     public static final int RESULT_FORBIDDEN = -5;
    126     /** Error code indicating an unresolved provisioning error. */
    127     public static final int RESULT_PROVISIONING_ERROR = -6;
    128     /** Error code indicating an authentication problem. */
    129     public static final int RESULT_AUTHENTICATION_ERROR = -7;
    130     /** Error code indicating the client is missing a certificate. */
    131     public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8;
    132     /** Error code indicating we don't have a protocol version in common with the server. */
    133     public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9;
    134     /** Error code indicating a hard error when initializing the operation. */
    135     public static final int RESULT_INITIALIZATION_FAILURE = -10;
    136     /** Error code indicating a hard data layer error. */
    137     public static final int RESULT_HARD_DATA_FAILURE = -11;
    138     /** Error code indicating that this operation failed, but we should not abort the sync */
    139     /** TODO: This is currently only used in EasOutboxSync, no other place handles it correctly */
    140     public static final int RESULT_NON_FATAL_ERROR = -12;
    141     /** Error code indicating some other failure. */
    142     public static final int RESULT_OTHER_FAILURE = -99;
    143     /** Constant to delimit where op specific error codes begin. */
    144     public static final int RESULT_OP_SPECIFIC_ERROR_RESULT = -100;
    145 
    146     protected final Context mContext;
    147 
    148     /** The provider id for the account this operation is on. */
    149     private final long mAccountId;
    150 
    151     /** The cached {@link Account} state; can be null if it hasn't been loaded yet. */
    152     protected Account mAccount;
    153 
    154     /** The connection to use for this operation. This is created when {@link #mAccount} is set. */
    155     private EasServerConnection mConnection;
    156 
    157     public class MessageInvalidException extends Exception {
    158         public MessageInvalidException(final String message) {
    159             super(message);
    160         }
    161     }
    162 
    163     /**
    164      * Constructor which defers loading of account and connection info.
    165      * @param context
    166      * @param accountId
    167      */
    168     protected EasOperation(final Context context, final long accountId) {
    169         mContext = context;
    170         mAccountId = accountId;
    171     }
    172 
    173     protected EasOperation(final Context context, final Account account,
    174             final EasServerConnection connection) {
    175         this(context, account.mId);
    176         mAccount = account;
    177         mConnection = connection;
    178     }
    179 
    180     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
    181         this(context, account, new EasServerConnection(context, account, hostAuth));
    182     }
    183 
    184     protected EasOperation(final Context context, final Account account) {
    185         this(context, account, account.getOrCreateHostAuthRecv(context));
    186     }
    187 
    188     /**
    189      * This constructor is for use by operations that are created by other operations, e.g.
    190      * {@link EasProvision}. It reuses the account and connection of its parent.
    191      * @param parentOperation The {@link EasOperation} that is creating us.
    192      */
    193     protected EasOperation(final EasOperation parentOperation) {
    194         mContext = parentOperation.mContext;
    195         mAccountId = parentOperation.mAccountId;
    196         mAccount = parentOperation.mAccount;
    197         mConnection = parentOperation.mConnection;
    198     }
    199 
    200     /**
    201      * Some operations happen before the account exists (e.g. account validation).
    202      * These operations cannot use {@link #init}, so instead we make a dummy account and
    203      * supply a temporary {@link HostAuth}.
    204      * @param hostAuth
    205      */
    206     protected final void setDummyAccount(final HostAuth hostAuth) {
    207         mAccount = new Account();
    208         mAccount.mEmailAddress = hostAuth.mLogin;
    209         mConnection = new EasServerConnection(mContext, mAccount, hostAuth);
    210     }
    211 
    212     /**
    213      * Loads (or reloads) the {@link Account} for this operation, and sets up our connection to the
    214      * server. This can be overridden to add additional functionality, but child implementations
    215      * should always call super().
    216      * @param allowReload If false, do not perform a load if we already have an {@link Account}
    217      *                    (i.e. just keep the existing one); otherwise allow replacement of the
    218      *                    account. Note that this can result in a valid Account being replaced with
    219      *                    null if the account no longer exists.
    220      * @return Whether we now have a valid {@link Account} object.
    221      */
    222     public boolean init(final boolean allowReload) {
    223         if (mAccount == null || allowReload) {
    224             mAccount = Account.restoreAccountWithId(mContext, getAccountId());
    225             if (mAccount != null) {
    226                 mConnection = new EasServerConnection(mContext, mAccount,
    227                         mAccount.getOrCreateHostAuthRecv(mContext));
    228             }
    229         }
    230         return (mAccount != null);
    231     }
    232 
    233     public final long getAccountId() {
    234         return mAccountId;
    235     }
    236 
    237     public final Account getAccount() {
    238         return mAccount;
    239     }
    240 
    241     /**
    242      * Request that this operation terminate. Intended for use by the sync service to interrupt
    243      * running operations, primarily Ping.
    244      */
    245     public final void abort() {
    246         mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT);
    247     }
    248 
    249     /**
    250      * Request that this operation restart. Intended for use by the sync service to interrupt
    251      * running operations, primarily Ping.
    252      */
    253     public final void restart() {
    254         mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART);
    255     }
    256 
    257     /**
    258      * The skeleton of performing an operation. This function handles all the common code and
    259      * error handling, calling into virtual functions that are implemented or overridden by the
    260      * subclass to do the operation-specific logic.
    261      *
    262      * The result codes work as follows:
    263      * - Negative values indicate common error codes and are defined above (the various RESULT_*
    264      *   constants).
    265      * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously
    266      *   specific to the subclass, and may indicate success or error conditions.
    267      *
    268      * The common error codes primarily indicate conditions that occur when performing the POST
    269      * itself, such as network errors and handling of the HTTP response. However, some errors that
    270      * can be indicated in the HTTP response code can also be indicated in the payload of the
    271      * response as well, so {@link #handleResponse} should in those cases return the appropriate
    272      * negative result code, which will be handled the same as if it had been indicated in the HTTP
    273      * response code.
    274      *
    275      * @return A result code for the outcome of this operation, as described above.
    276      */
    277     public int performOperation() {
    278         // Make sure the account is loaded if it hasn't already been.
    279         if (!init(false)) {
    280             LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s",
    281                     getAccountId(), getCommand());
    282             return RESULT_INITIALIZATION_FAILURE;
    283         }
    284 
    285         // We handle server redirects by looping, but we need to protect against too much looping.
    286         int redirectCount = 0;
    287 
    288         do {
    289             // Perform the HTTP request and handle exceptions.
    290             final EasResponse response;
    291             try {
    292                 try {
    293                     response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout());
    294                 } finally {
    295                     onRequestMade();
    296                 }
    297             } catch (final IOException e) {
    298                 // If we were stopped, return the appropriate result code.
    299                 switch (mConnection.getStoppedReason()) {
    300                     case EasServerConnection.STOPPED_REASON_ABORT:
    301                         return RESULT_ABORT;
    302                     case EasServerConnection.STOPPED_REASON_RESTART:
    303                         return RESULT_RESTART;
    304                     default:
    305                         break;
    306                 }
    307                 // If we're here, then we had a IOException that's not from a stop request.
    308                 String message = e.getMessage();
    309                 if (message == null) {
    310                     message = "(no message)";
    311                 }
    312                 LogUtils.i(LOG_TAG, "IOException while sending request: %s", message);
    313                 return RESULT_REQUEST_FAILURE;
    314             } catch (final CertificateException e) {
    315                 LogUtils.i(LOG_TAG, "CertificateException while sending request: %s",
    316                         e.getMessage());
    317                 return RESULT_CLIENT_CERTIFICATE_REQUIRED;
    318             } catch (final MessageInvalidException e) {
    319                 LogUtils.d(LOG_TAG, "Exception sending request %s", e.getMessage());
    320                 return RESULT_NON_FATAL_ERROR;
    321             } catch (final IllegalStateException e) {
    322                 // Subclasses use ISE to signal a hard error when building the request.
    323                 // TODO: Switch away from ISEs.
    324                 LogUtils.e(LOG_TAG, e, "Exception while sending request");
    325                 return RESULT_HARD_DATA_FAILURE;
    326             }
    327 
    328             // The POST completed, so process the response.
    329             try {
    330                 final int result;
    331                 // First off, the success case.
    332                 if (response.isSuccess()) {
    333                     int responseResult;
    334                     try {
    335                         responseResult = handleResponse(response);
    336                     } catch (final IOException e) {
    337                         LogUtils.e(LOG_TAG, e, "Exception while handling response");
    338                         return RESULT_REQUEST_FAILURE;
    339                     } catch (final CommandStatusException e) {
    340                         // For some operations (notably Sync & FolderSync), errors are signaled in
    341                         // the payload of the response. These will have a HTTP 200 response, and the
    342                         // error condition is only detected during response parsing.
    343                         // The various parsers handle this by throwing a CommandStatusException.
    344                         // TODO: Consider having the parsers return the errors instead of throwing.
    345                         final int status = e.mStatus;
    346                         LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status);
    347                         if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
    348                             responseResult = RESULT_PROVISIONING_ERROR;
    349                         } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
    350                             responseResult = RESULT_FORBIDDEN;
    351                         } else {
    352                             responseResult = RESULT_OTHER_FAILURE;
    353                         }
    354                     }
    355                     result = responseResult;
    356                 } else {
    357                     result = handleHttpError(response.getStatus());
    358                 }
    359 
    360                 // Non-negative results indicate success. Return immediately and bypass the error
    361                 // handling.
    362                 if (result >= 0) {
    363                     return result;
    364                 }
    365 
    366                 // If this operation has distinct handling for 403 errors, do that.
    367                 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
    368                     LogUtils.e(LOG_TAG, "Forbidden response");
    369                     return RESULT_FORBIDDEN;
    370                 }
    371 
    372                 // Handle provisioning errors.
    373                 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
    374                     if (handleProvisionError()) {
    375                         // The provisioning error has been taken care of, so we should re-do this
    376                         // request.
    377                         LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying",
    378                                 getCommand());
    379                         continue;
    380                     }
    381                     return RESULT_PROVISIONING_ERROR;
    382                 }
    383 
    384                 // Handle authentication errors.
    385                 if (response.isAuthError()) {
    386                     LogUtils.e(LOG_TAG, "Authentication error");
    387                     if (response.isMissingCertificate()) {
    388                         return RESULT_CLIENT_CERTIFICATE_REQUIRED;
    389                     }
    390                     return RESULT_AUTHENTICATION_ERROR;
    391                 }
    392 
    393                 // Handle redirects.
    394                 if (response.isRedirectError()) {
    395                     ++redirectCount;
    396                     mConnection.redirectHostAuth(response.getRedirectAddress());
    397                     // Note that unlike other errors, we do NOT return here; we just keep looping.
    398                 } else {
    399                     // All other errors.
    400                     LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d",
    401                             getCommand(), response.getStatus(), result);
    402                     // TODO: This probably should return result.
    403                     return RESULT_OTHER_FAILURE;
    404                 }
    405             } finally {
    406                 response.close();
    407             }
    408         } while (redirectCount < MAX_REDIRECTS);
    409 
    410         // Non-redirects return immediately after handling, so the only way to reach here is if we
    411         // looped too many times.
    412         LogUtils.e(LOG_TAG, "Too many redirects");
    413         return RESULT_TOO_MANY_REDIRECTS;
    414     }
    415 
    416     protected void onRequestMade() {
    417         // This can be overridden to do any cleanup that must happen after the request has
    418         // been sent. It will always be called, regardless of the status of the request.
    419     }
    420 
    421     protected int handleHttpError(final int httpStatus) {
    422         // This function can be overriden if the child class needs to change the result code
    423         // based on the http response status.
    424         return RESULT_OTHER_FAILURE;
    425     }
    426 
    427     /**
    428      * Reset the protocol version to use for this connection. If it's changed, and our account is
    429      * persisted, also write back the changes to the DB.
    430      * @param protocolVersion The new protocol version to use, as a string.
    431      */
    432     protected final void setProtocolVersion(final String protocolVersion) {
    433         final long accountId = getAccountId();
    434         if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) {
    435             final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
    436             final ContentValues cv = new ContentValues(2);
    437             if (getProtocolVersion() >= 12.0) {
    438                 final int oldFlags = Utility.getFirstRowInt(mContext, uri,
    439                         Account.ACCOUNT_FLAGS_PROJECTION, null, null, null,
    440                         Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0);
    441                 final int newFlags = oldFlags
    442                         | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
    443                 if (oldFlags != newFlags) {
    444                     cv.put(EmailContent.AccountColumns.FLAGS, newFlags);
    445                 }
    446             }
    447             cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion);
    448             mContext.getContentResolver().update(uri, cv, null, null);
    449         }
    450     }
    451 
    452     /**
    453      * Create the request object for this operation.
    454      * Most operations use a POST, but some use other request types (e.g. Options).
    455      * @return An {@link HttpUriRequest}.
    456      * @throws IOException
    457      */
    458     private final HttpUriRequest makeRequest() throws IOException, MessageInvalidException {
    459         final String requestUri = getRequestUri();
    460         if (requestUri == null) {
    461             return mConnection.makeOptions();
    462         }
    463         return mConnection.makePost(requestUri, getRequestEntity(),
    464                 getRequestContentType(), addPolicyKeyHeaderToRequest());
    465     }
    466 
    467     /**
    468      * The following functions MUST be overridden by subclasses; these are things that are unique
    469      * to each operation.
    470      */
    471 
    472     /**
    473      * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note
    474      * that if you override {@link #getRequestUri}, then this function may be unused for normal
    475      * operation, but all subclasses should return something non-null for use with logging.
    476      * @return The name of the command for this operation as defined by the EAS protocol, or for
    477      *         commands that don't need it, a suitable descriptive name for logging.
    478      */
    479     protected abstract String getCommand();
    480 
    481     /**
    482      * Build the {@link HttpEntity} which is used to construct the POST. Typically this function
    483      * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
    484      * If the subclass is not using a POST, then it should override this to return null.
    485      * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}.
    486      * @throws IOException
    487      */
    488     protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException;
    489 
    490     /**
    491      * Parse the response from the Exchange perform whatever actions are dictated by that.
    492      * @param response The {@link EasResponse} to our request.
    493      * @return A result code. Non-negative values are returned directly to the caller; negative
    494      *         values
    495      *
    496      * that is returned to the caller of {@link #performOperation}.
    497      * @throws IOException
    498      */
    499     protected abstract int handleResponse(final EasResponse response)
    500             throws IOException, CommandStatusException;
    501 
    502     /**
    503      * The following functions may be overriden by a subclass, but most operations will not need
    504      * to do so.
    505      */
    506 
    507     /**
    508      * Get the URI for the Exchange server and this operation. Most (signed in) operations need
    509      * not override this; the notable operation that needs to override it is auto-discover.
    510      * @return
    511      */
    512     protected String getRequestUri() {
    513         return mConnection.makeUriString(getCommand());
    514     }
    515 
    516     /**
    517      * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header.
    518      */
    519     protected boolean addPolicyKeyHeaderToRequest() {
    520         return true;
    521     }
    522 
    523     /**
    524      * @return The content type of this request.
    525      */
    526     protected String getRequestContentType() {
    527         return EAS_14_MIME_TYPE;
    528     }
    529 
    530     /**
    531      * @return The timeout to use for the POST.
    532      */
    533     protected long getTimeout() {
    534         return 30 * DateUtils.SECOND_IN_MILLIS;
    535     }
    536 
    537     /**
    538      * If 403 responses should be handled in a special way, this function should be overridden to
    539      * do that.
    540      * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error.
    541      */
    542     protected boolean handleForbidden() {
    543         return false;
    544     }
    545 
    546     /**
    547      * Handle a provisioning error. Subclasses may override this to do something different, e.g.
    548      * to validate rather than actually do the provisioning.
    549      * @return
    550      */
    551     protected boolean handleProvisionError() {
    552         final EasProvision provisionOperation = new EasProvision(this);
    553         return provisionOperation.provision();
    554     }
    555 
    556     /**
    557      * Convenience methods for subclasses to use.
    558      */
    559 
    560     /**
    561      * Convenience method to make an {@link HttpEntity} from {@link Serializer}.
    562      */
    563     protected final HttpEntity makeEntity(final Serializer s) {
    564         return new ByteArrayEntity(s.toByteArray());
    565     }
    566 
    567     /**
    568      * Check whether we should ask the server what protocol versions it supports and set this
    569      * account to use that version.
    570      * @return Whether we need a new protocol version from the server.
    571      */
    572     protected final boolean shouldGetProtocolVersion() {
    573         // TODO: Find conditions under which we should check other than not having one yet.
    574         return !mConnection.isProtocolVersionSet();
    575     }
    576 
    577     /**
    578      * @return The protocol version to use.
    579      */
    580     protected final double getProtocolVersion() {
    581         return mConnection.getProtocolVersion();
    582     }
    583 
    584     /**
    585      * @return Our useragent.
    586      */
    587     protected final String getUserAgent() {
    588         return mConnection.getUserAgent();
    589     }
    590 
    591     /**
    592      * @return Whether we succeeeded in registering the client cert.
    593      */
    594     protected final boolean registerClientCert() {
    595         return mConnection.registerClientCert();
    596     }
    597 
    598     /**
    599      * Add the device information to the current request.
    600      * @param s The {@link Serializer} for our current request.
    601      * @param context The {@link Context} for current device.
    602      * @param userAgent The user agent string that our connection use.
    603      */
    604     protected static void expandedAddDeviceInformationToSerializer(final Serializer s,
    605             final Context context, final String userAgent) throws IOException {
    606         final String deviceId;
    607         final String phoneNumber;
    608         final String operator;
    609         final TelephonyManager tm = (TelephonyManager)context.getSystemService(
    610                 Context.TELEPHONY_SERVICE);
    611         if (tm != null) {
    612             deviceId = tm.getDeviceId();
    613             phoneNumber = tm.getLine1Number();
    614             // TODO: This is not perfect and needs to be improved, for at least two reasons:
    615             // 1) SIM cards can override this name.
    616             // 2) We don't resend this info to the server when we change networks.
    617             final String operatorName = tm.getNetworkOperatorName();
    618             final String operatorNumber = tm.getNetworkOperator();
    619             if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) {
    620                 operator = operatorName + " (" + operatorNumber + ")";
    621             } else if (!TextUtils.isEmpty(operatorName)) {
    622                 operator = operatorName;
    623             } else {
    624                 operator = operatorNumber;
    625             }
    626         } else {
    627             deviceId = null;
    628             phoneNumber = null;
    629             operator = null;
    630         }
    631 
    632         // TODO: Right now, we won't send this information unless the device is provisioned again.
    633         // Potentially, this means that our phone number could be out of date if the user
    634         // switches sims. Is there something we can do to force a reprovision?
    635         s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
    636         s.data(Tags.SETTINGS_MODEL, Build.MODEL);
    637         if (deviceId != null) {
    638             s.data(Tags.SETTINGS_IMEI, tm.getDeviceId());
    639         }
    640         // Set the device friendly name, if we have one.
    641         // TODO: Longer term, this should be done without a provider call.
    642         final Bundle deviceName = context.getContentResolver().call(
    643                 EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null);
    644         if (deviceName != null) {
    645             final String friendlyName = deviceName.getString(EmailContent.DEVICE_FRIENDLY_NAME);
    646             if (!TextUtils.isEmpty(friendlyName)) {
    647                 s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName);
    648             }
    649         }
    650         s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
    651         if (phoneNumber != null) {
    652             s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber);
    653         }
    654         // TODO: Consider setting this, but make sure we know what it's used for.
    655         // If the user changes the device's locale and we don't do a reprovision, the server's
    656         // idea of the language will be wrong. Since we're not sure what this is used for,
    657         // right now we're leaving it out.
    658         //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage());
    659         s.data(Tags.SETTINGS_USER_AGENT, userAgent);
    660         if (operator != null) {
    661             s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator);
    662         }
    663         s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
    664     }
    665 
    666     /**
    667      * Add the device information to the current request.
    668      * @param s The {@link Serializer} that contains the payload for this request.
    669      */
    670     protected final void addDeviceInformationToSerializer(final Serializer s)
    671             throws IOException {
    672         final String userAgent = getUserAgent();
    673         expandedAddDeviceInformationToSerializer(s, mContext, userAgent);
    674     }
    675 
    676     /**
    677      * Convenience method for adding a Message to an account's outbox
    678      * @param account The {@link Account} from which to send the message.
    679      * @param msg the message to send
    680      */
    681     protected final void sendMessage(final Account account, final EmailContent.Message msg) {
    682         long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
    683         // TODO: Improve system mailbox handling.
    684         if (mailboxId == Mailbox.NO_MAILBOX) {
    685             LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId);
    686             final Mailbox outbox =
    687                     Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX);
    688             outbox.save(mContext);
    689             mailboxId = outbox.mId;
    690         }
    691         msg.mMailboxKey = mailboxId;
    692         msg.mAccountKey = account.mId;
    693         msg.save(mContext);
    694         requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress,
    695                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId);
    696     }
    697 
    698     /**
    699      * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
    700      * @param amAccount The {@link android.accounts.Account} for the account we're pinging.
    701      * @param mailboxId The id of the mailbox that needs to sync.
    702      */
    703     protected static void requestSyncForMailbox(final android.accounts.Account amAccount,
    704             final long mailboxId) {
    705         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
    706         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    707         LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s",
    708                 amAccount.toString(), extras.toString());
    709     }
    710 
    711     protected static void requestSyncForMailboxes(final android.accounts.Account amAccount,
    712             final ArrayList<Long> mailboxIds) {
    713         final Bundle extras = Mailbox.createSyncBundle(mailboxIds);
    714         /**
    715          * Please note that it is very possible that we are trying to send a request to the
    716          * email sync adapter even though email push is turned off (i.e. this account might only
    717          * be syncing calendar or contacts). In this situation we need to make sure that
    718          * this request is marked as manual as to ensure that the sync manager does not drop it
    719          * on the floor. Right now, this function is only called by EasPing, if it is every called
    720          * by another caller, then we should reconsider if manual=true is the right thing to do.
    721          */
    722         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
    723         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    724         LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailboxes  %s, %s",
    725                 amAccount.toString(), extras.toString());
    726     }
    727 
    728     /**
    729      * RequestNoOpSync
    730      * This requests a sync for a particular authority purely so that that account
    731      * in settings will recognize that it is trying to sync, and will display the
    732      * appropriate UI. In fact, all exchange data syncing actually happens through the
    733      * EmailSyncAdapterService.
    734      * @param amAccount
    735      * @param authority
    736      */
    737     protected static void requestNoOpSync(final android.accounts.Account amAccount,
    738             final String authority) {
    739         final Bundle extras = new Bundle(1);
    740         extras.putBoolean(Mailbox.SYNC_EXTRA_NOOP, true);
    741         ContentResolver.requestSync(amAccount, authority, extras);
    742         LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s",
    743                 amAccount.toString(), extras.toString());
    744     }
    745 
    746     /**
    747      * Interpret a result code from an {@link EasOperation} and, if it's an error, write it to
    748      * the appropriate field in {@link SyncResult}.
    749      * @param result
    750      * @param syncResult
    751      * @return Whether an error code was written to syncResult.
    752      */
    753     public static boolean writeResultToSyncResult(final int result, final SyncResult syncResult) {
    754         switch (result) {
    755             case RESULT_TOO_MANY_REDIRECTS:
    756                 syncResult.tooManyRetries = true;
    757                 return true;
    758             case RESULT_REQUEST_FAILURE:
    759                 syncResult.stats.numIoExceptions = 1;
    760                 return true;
    761             case RESULT_FORBIDDEN:
    762             case RESULT_PROVISIONING_ERROR:
    763             case RESULT_AUTHENTICATION_ERROR:
    764             case RESULT_CLIENT_CERTIFICATE_REQUIRED:
    765                 syncResult.stats.numAuthExceptions = 1;
    766                 return true;
    767             case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
    768                 // Only used in validate, so there's never a syncResult to write to here.
    769                 break;
    770             case RESULT_INITIALIZATION_FAILURE:
    771             case RESULT_HARD_DATA_FAILURE:
    772                 syncResult.databaseError = true;
    773                 return true;
    774             case RESULT_OTHER_FAILURE:
    775                 // TODO: Is this correct?
    776                 syncResult.stats.numIoExceptions = 1;
    777                 return true;
    778         }
    779         return false;
    780     }
    781 
    782     public static int translateSyncResultToUiResult(final int result) {
    783         switch (result) {
    784               case RESULT_TOO_MANY_REDIRECTS:
    785                 return UIProvider.LastSyncResult.INTERNAL_ERROR;
    786             case RESULT_REQUEST_FAILURE:
    787                 return UIProvider.LastSyncResult.CONNECTION_ERROR;
    788             case RESULT_FORBIDDEN:
    789             case RESULT_PROVISIONING_ERROR:
    790             case RESULT_AUTHENTICATION_ERROR:
    791             case RESULT_CLIENT_CERTIFICATE_REQUIRED:
    792                 return UIProvider.LastSyncResult.AUTH_ERROR;
    793             case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
    794                 // Only used in validate, so there's never a syncResult to write to here.
    795                 break;
    796             case RESULT_INITIALIZATION_FAILURE:
    797             case RESULT_HARD_DATA_FAILURE:
    798                 return UIProvider.LastSyncResult.INTERNAL_ERROR;
    799             case RESULT_OTHER_FAILURE:
    800                 return UIProvider.LastSyncResult.INTERNAL_ERROR;
    801         }
    802         return UIProvider.LastSyncResult.SUCCESS;
    803     }
    804 }
    805