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 {
MessageInvalidException(final String message)160         public MessageInvalidException(final String message) {
161             super(message);
162         }
163     }
164 
isFatal(int result)165     public static boolean isFatal(int result) {
166         return result < RESULT_MIN_OK_RESULT;
167     }
168 
EasOperation(final Context context, @NonNull final Account account, final EasServerConnection connection)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 
EasOperation(final Context context, final Account account, final HostAuth hostAuth)179     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
180         this(context, account, new EasServerConnection(context, account, hostAuth));
181     }
182 
EasOperation(final Context context, final Account account)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      */
EasOperation(final EasOperation parentOperation)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      */
init()203     public boolean init() {
204         return true;
205     }
206 
getAccountId()207     public final long getAccountId() {
208         return mAccount.getId();
209     }
210 
getAccount()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      */
abort()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      */
restart()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      */
lastSyncHadError()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      */
performOperation()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 
performOperationInternal()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 
onRequestMade()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 
onRequestComplete()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 
handleHttpError(final int httpStatus)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      */
setProtocolVersion(final String protocolVersion)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      */
makeRequest()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      */
getCommand()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      */
getRequestEntity()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      */
handleResponse(final EasResponse response)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      */
getRequestUri()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      */
addPolicyKeyHeaderToRequest()520     protected boolean addPolicyKeyHeaderToRequest() {
521         return true;
522     }
523 
524     /**
525      * @return The content type of this request.
526      */
getRequestContentType()527     protected String getRequestContentType() {
528         return EAS_14_MIME_TYPE;
529     }
530 
531     /**
532      * @return The timeout to use for the POST.
533      */
getTimeout()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      */
handleForbidden()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      */
handleProvisionError()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      */
makeEntity(final Serializer s)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      */
shouldGetProtocolVersion()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      */
getProtocolVersion()581     protected final double getProtocolVersion() {
582         return mConnection.getProtocolVersion();
583     }
584 
585     /**
586      * @return Our useragent.
587      */
getUserAgent()588     protected final String getUserAgent() {
589         return mConnection.getUserAgent();
590     }
591 
592     /**
593      * @return Whether we succeeeded in registering the client cert.
594      */
registerClientCert()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      */
expandedAddDeviceInformationToSerializer(final Serializer s, final Context context, final String userAgent)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      */
addDeviceInformationToSerializer(final Serializer s)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      */
sendMessage(final Account account, final EmailContent.Message msg)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      */
requestSyncForMailbox(final android.accounts.Account amAccount, final long mailboxId)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 
requestSyncForMailboxes(final android.accounts.Account amAccount, final String authority, final ArrayList<Long> mailboxIds)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 
translateSyncResultToUiResult(final int result)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