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