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