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