• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
MessageInvalidException(final String message)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      */
EasOperation(final Context context, final long accountId)168     protected EasOperation(final Context context, final long accountId) {
169         mContext = context;
170         mAccountId = accountId;
171     }
172 
EasOperation(final Context context, final Account account, final EasServerConnection connection)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 
EasOperation(final Context context, final Account account, final HostAuth hostAuth)180     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
181         this(context, account, new EasServerConnection(context, account, hostAuth));
182     }
183 
EasOperation(final Context context, final Account account)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      */
EasOperation(final EasOperation parentOperation)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      */
setDummyAccount(final HostAuth hostAuth)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      */
init(final boolean allowReload)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 
getAccountId()233     public final long getAccountId() {
234         return mAccountId;
235     }
236 
getAccount()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      */
abort()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      */
restart()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      */
performOperation()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 
onRequestMade()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 
handleHttpError(final int httpStatus)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      */
setProtocolVersion(final String protocolVersion)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      */
makeRequest()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      */
getCommand()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      */
getRequestEntity()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      */
handleResponse(final EasResponse response)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      */
getRequestUri()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      */
addPolicyKeyHeaderToRequest()519     protected boolean addPolicyKeyHeaderToRequest() {
520         return true;
521     }
522 
523     /**
524      * @return The content type of this request.
525      */
getRequestContentType()526     protected String getRequestContentType() {
527         return EAS_14_MIME_TYPE;
528     }
529 
530     /**
531      * @return The timeout to use for the POST.
532      */
getTimeout()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      */
handleForbidden()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      */
handleProvisionError()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      */
makeEntity(final Serializer s)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      */
shouldGetProtocolVersion()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      */
getProtocolVersion()580     protected final double getProtocolVersion() {
581         return mConnection.getProtocolVersion();
582     }
583 
584     /**
585      * @return Our useragent.
586      */
getUserAgent()587     protected final String getUserAgent() {
588         return mConnection.getUserAgent();
589     }
590 
591     /**
592      * @return Whether we succeeeded in registering the client cert.
593      */
registerClientCert()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      */
expandedAddDeviceInformationToSerializer(final Serializer s, final Context context, final String userAgent)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      */
addDeviceInformationToSerializer(final Serializer s)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      */
sendMessage(final Account account, final EmailContent.Message msg)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      */
requestSyncForMailbox(final android.accounts.Account amAccount, final long mailboxId)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 
requestSyncForMailboxes(final android.accounts.Account amAccount, final ArrayList<Long> mailboxIds)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      */
requestNoOpSync(final android.accounts.Account amAccount, final String authority)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      */
writeResultToSyncResult(final int result, final SyncResult syncResult)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 
translateSyncResultToUiResult(final int result)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