summaryrefslogtreecommitdiff
path: root/src/com/android/exchange/eas/EasOperation.java
blob: 19dc14925ae8fbc9b3a3ea41f2ac8e571ea5b157 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.exchange.eas;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateUtils;

import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.service.EasServerConnection;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;

import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.ArrayList;

/**
 * Base class for all Exchange operations that use a POST to talk to the server.
 *
 * The core of this class is {@link #performOperation}, which provides the skeleton of making
 * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one.
 * This class abstracts the connection handling from its subclasses and callers.
 *
 * {@link #performOperation} calls various abstract functions to create the request and parse the
 * response. For the most part subclasses can implement just these bits of functionality and rely
 * on {@link #performOperation} to do all the boilerplate etc.
 *
 * There are also a set of functions that a subclass may override if it's substantially
 * different from the "normal" operation (e.g. autodiscover deviates from the standard URI since
 * it's not account-specific so it needs to override {@link #getRequestUri()}), but the default
 * implementations of these functions should suffice for most operations.
 *
 * Some subclasses may need to override {@link #performOperation} to add validation and results
 * processing around a call to super.performOperation. Subclasses should avoid doing too much more
 * than wrapping some handling around the chained call; if you find that's happening, it's likely
 * a sign that the base class needs to be enhanced.
 *
 * One notable reason this wrapping happens is for operations that need to return a result directly
 * to their callers (as opposed to simply writing the results to the provider, as is common with
 * sync operations). This happens for example in
 * {@link com.android.emailcommon.service.IEmailService} message handlers. In such cases, due to
 * how {@link com.android.exchange.service.EasService} uses this class, the subclass needs to
 * store the result as a member variable and then provide an accessor to read the result. Since
 * different operations have different results (or none at all), there is no function in the base
 * class for this.
 *
 * Note that it is not practical to avoid the race between when an operation loads its account data
 * and when it uses it, as that would require some form of locking in the provider. There are three
 * interesting situations where this might happen, and that this class must handle:
 *
 * 1) Deleted from provider: Any subsequent provider access should return an error. Operations
 *    must detect this and terminate with an error.
 * 2) Account sync settings change: Generally only affects Ping. We interrupt the operation and
 *    load the new settings before proceeding.
 * 3) Sync suspended due to hold: A special case of the previous, and affects all operations, but
 *    fortunately doesn't need special handling here. Correct provider functionality must generate
 *    write failures, so the handling for #1 should cover this case as well.
 *
 * This class attempts to defer loading of account data as long as possible -- ideally we load
 * immediately before the network request -- but does not proactively check for changes after that.
 * This approach is a a practical balance between minimizing the race without adding too much
 * complexity beyond what's required.
 */
public abstract class EasOperation {
    public static final String LOG_TAG = LogUtils.TAG;

    /** The maximum number of server redirects we allow before returning failure. */
    private static final int MAX_REDIRECTS = 3;

    /** Message MIME type for EAS version 14 and later. */
    private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml";

    /**
     * EasOperation error codes below.  All subclasses should try to create error codes
     * that do not overlap these codes or the codes of other subclasses. The error
     * code values for each subclass should start in a different 100 range (i.e. -100,
     * -200, etc...).
     */

    /** Minimum value for any non failure result. There may be multiple different non-failure
     * results, if so they should all be greater than or equal to this value. */
    public static final int RESULT_MIN_OK_RESULT = 0;
    /** Error code indicating the operation was cancelled via {@link #abort}. */
    public static final int RESULT_ABORT = -1;
    /** Error code indicating the operation was cancelled via {@link #restart}. */
    public static final int RESULT_RESTART = -2;
    /** Error code indicating the Exchange servers redirected too many times. */
    public static final int RESULT_TOO_MANY_REDIRECTS = -3;
    /** Error code indicating the request failed due to a network problem. */
    public static final int RESULT_NETWORK_PROBLEM = -4;
    /** Error code indicating a 403 (forbidden) error. */
    public static final int RESULT_FORBIDDEN = -5;
    /** Error code indicating an unresolved provisioning error. */
    public static final int RESULT_PROVISIONING_ERROR = -6;
    /** Error code indicating an authentication problem. */
    public static final int RESULT_AUTHENTICATION_ERROR = -7;
    /** Error code indicating the client is missing a certificate. */
    public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8;
    /** Error code indicating we don't have a protocol version in common with the server. */
    public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9;
    /** Error code indicating a hard error when initializing the operation. */
    public static final int RESULT_INITIALIZATION_FAILURE = -10;
    /** Error code indicating a hard data layer error. */
    public static final int RESULT_HARD_DATA_FAILURE = -11;
    /** Error code indicating that this operation failed, but we should not abort the sync */
    /** TODO: This is currently only used in EasOutboxSync, no other place handles it correctly */
    public static final int RESULT_NON_FATAL_ERROR = -12;
    /** Error code indicating some other failure. */
    public static final int RESULT_OTHER_FAILURE = -99;
    /** Constant to delimit where op specific error codes begin. */
    public static final int RESULT_OP_SPECIFIC_ERROR_RESULT = -100;

    protected final Context mContext;

    /** The provider id for the account this operation is on. */
    private final long mAccountId;

    /** The cached {@link Account} state; can be null if it hasn't been loaded yet. */
    protected Account mAccount;

    /** The connection to use for this operation. This is created when {@link #mAccount} is set. */
    private EasServerConnection mConnection;

    public class MessageInvalidException extends Exception {
        public MessageInvalidException(final String message) {
            super(message);
        }
    }

    @VisibleForTesting
    public void replaceEasServerConnection(EasServerConnection connection) {
        mConnection = connection;
    }

    public static boolean isFatal(int result) {
        return result < RESULT_MIN_OK_RESULT;
    }

    /**
     * Constructor which defers loading of account and connection info.
     * @param context
     * @param accountId
     */
    protected EasOperation(final Context context, final long accountId) {
        mContext = context;
        mAccountId = accountId;
    }

    protected EasOperation(final Context context, final Account account,
            final EasServerConnection connection) {
        this(context, account.mId);
        mAccount = account;
        mConnection = connection;
    }

    protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
        this(context, account, new EasServerConnection(context, account, hostAuth));
    }

    protected EasOperation(final Context context, final Account account) {
        this(context, account, account.getOrCreateHostAuthRecv(context));
    }

    /**
     * This constructor is for use by operations that are created by other operations, e.g.
     * {@link EasProvision}. It reuses the account and connection of its parent.
     * @param parentOperation The {@link EasOperation} that is creating us.
     */
    protected EasOperation(final EasOperation parentOperation) {
        mContext = parentOperation.mContext;
        mAccountId = parentOperation.mAccountId;
        mAccount = parentOperation.mAccount;
        mConnection = parentOperation.mConnection;
    }

    /**
     * Some operations happen before the account exists (e.g. account validation).
     * These operations cannot use {@link #init}, so instead we make a dummy account and
     * supply a temporary {@link HostAuth}.
     * @param hostAuth
     */
    protected final void setDummyAccount(final HostAuth hostAuth) {
        mAccount = new Account();
        mAccount.mEmailAddress = hostAuth.mLogin;
        mConnection = new EasServerConnection(mContext, mAccount, hostAuth);
    }

    /**
     * Loads (or reloads) the {@link Account} for this operation, and sets up our connection to the
     * server. This can be overridden to add additional functionality, but child implementations
     * should always call super().
     * @param allowReload If false, do not perform a load if we already have an {@link Account}
     *                    (i.e. just keep the existing one); otherwise allow replacement of the
     *                    account. Note that this can result in a valid Account being replaced with
     *                    null if the account no longer exists.
     * @return Whether we now have a valid {@link Account} object.
     */
    public boolean init(final boolean allowReload) {
        if (mAccount == null || allowReload) {
            mAccount = Account.restoreAccountWithId(mContext, getAccountId());
            if (mAccount != null) {
                mConnection = new EasServerConnection(mContext, mAccount,
                        mAccount.getOrCreateHostAuthRecv(mContext));
            }
        }
        return (mAccount != null);
    }

    /**
     * Sets the account. This is for use in cases where the account is not available upon
     * construction. This will also create the EasServerConnection.
     * @param account
     * @param hostAuth
     */
    protected void setAccount(final Account account, final HostAuth hostAuth) {
        mAccount = account;
        if (mAccount != null) {
            mConnection = new EasServerConnection(mContext, mAccount, hostAuth);
        }
    }

    public final long getAccountId() {
        return mAccountId;
    }

    public final Account getAccount() {
        return mAccount;
    }

    /**
     * Request that this operation terminate. Intended for use by the sync service to interrupt
     * running operations, primarily Ping.
     */
    public final void abort() {
        mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT);
    }

    /**
     * Request that this operation restart. Intended for use by the sync service to interrupt
     * running operations, primarily Ping.
     */
    public final void restart() {
        mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART);
    }

    /**
     * The skeleton of performing an operation. This function handles all the common code and
     * error handling, calling into virtual functions that are implemented or overridden by the
     * subclass to do the operation-specific logic.
     *
     * The result codes work as follows:
     * - Negative values indicate common error codes and are defined above (the various RESULT_*
     *   constants).
     * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously
     *   specific to the subclass, and may indicate success or error conditions.
     *
     * The common error codes primarily indicate conditions that occur when performing the POST
     * itself, such as network errors and handling of the HTTP response. However, some errors that
     * can be indicated in the HTTP response code can also be indicated in the payload of the
     * response as well, so {@link #handleResponse} should in those cases return the appropriate
     * negative result code, which will be handled the same as if it had been indicated in the HTTP
     * response code.
     *
     * @return A result code for the outcome of this operation, as described above.
     */
    public int performOperation() {
        // Make sure the account is loaded if it hasn't already been.
        if (!init(false)) {
            LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s",
                    getAccountId(), getCommand());
            return RESULT_INITIALIZATION_FAILURE;
        }

        // We handle server redirects by looping, but we need to protect against too much looping.
        int redirectCount = 0;

        do {
            // Perform the HTTP request and handle exceptions.
            final EasResponse response;
            try {
                try {
                    response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout());
                } finally {
                    onRequestMade();
                }
            } catch (final IOException e) {
                // If we were stopped, return the appropriate result code.
                switch (mConnection.getStoppedReason()) {
                    case EasServerConnection.STOPPED_REASON_ABORT:
                        return RESULT_ABORT;
                    case EasServerConnection.STOPPED_REASON_RESTART:
                        return RESULT_RESTART;
                    default:
                        break;
                }
                // If we're here, then we had a IOException that's not from a stop request.
                String message = e.getMessage();
                if (message == null) {
                    message = "(no message)";
                }
                LogUtils.i(LOG_TAG, "IOException while sending request: %s", message);
                return RESULT_NETWORK_PROBLEM;
            } catch (final CertificateException e) {
                LogUtils.i(LOG_TAG, "CertificateException while sending request: %s",
                        e.getMessage());
                return RESULT_CLIENT_CERTIFICATE_REQUIRED;
            } catch (final MessageInvalidException e) {
                // This indicates that there is something wrong with the message locally, and it
                // cannot be sent. We don't want to return success, because that's misleading,
                // but on the other hand, we don't want to abort the sync, because that would
                // prevent other messages from being sent.
                LogUtils.d(LOG_TAG, "Exception sending request %s", e.getMessage());
                return RESULT_NON_FATAL_ERROR;
            } catch (final IllegalStateException e) {
                // Subclasses use ISE to signal a hard error when building the request.
                // TODO: Switch away from ISEs.
                LogUtils.e(LOG_TAG, e, "Exception while sending request");
                return RESULT_HARD_DATA_FAILURE;
            }

            // The POST completed, so process the response.
            try {
                final int result;
                // First off, the success case.
                if (response.isSuccess()) {
                    int responseResult;
                    try {
                        responseResult = handleResponse(response);
                    } catch (final IOException e) {
                        LogUtils.e(LOG_TAG, e, "Exception while handling response");
                        return RESULT_NETWORK_PROBLEM;
                    } catch (final CommandStatusException e) {
                        // For some operations (notably Sync & FolderSync), errors are signaled in
                        // the payload of the response. These will have a HTTP 200 response, and the
                        // error condition is only detected during response parsing.
                        // The various parsers handle this by throwing a CommandStatusException.
                        // TODO: Consider having the parsers return the errors instead of throwing.
                        final int status = e.mStatus;
                        LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status);
                        if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
                            responseResult = RESULT_PROVISIONING_ERROR;
                        } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
                            responseResult = RESULT_FORBIDDEN;
                        } else {
                            responseResult = RESULT_OTHER_FAILURE;
                        }
                    }
                    result = responseResult;
                } else {
                    result = handleHttpError(response.getStatus());
                }

                // Non-negative results indicate success. Return immediately and bypass the error
                // handling.
                if (result >= EasOperation.RESULT_MIN_OK_RESULT) {
                    return result;
                }

                // If this operation has distinct handling for 403 errors, do that.
                if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
                    LogUtils.e(LOG_TAG, "Forbidden response");
                    return RESULT_FORBIDDEN;
                }

                // Handle provisioning errors.
                if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
                    if (handleProvisionError()) {
                        // The provisioning error has been taken care of, so we should re-do this
                        // request.
                        LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying",
                                getCommand());
                        continue;
                    }
                    return RESULT_PROVISIONING_ERROR;
                }

                // Handle authentication errors.
                if (response.isAuthError()) {
                    LogUtils.e(LOG_TAG, "Authentication error");
                    if (response.isMissingCertificate()) {
                        return RESULT_CLIENT_CERTIFICATE_REQUIRED;
                    }
                    return RESULT_AUTHENTICATION_ERROR;
                }

                // Handle redirects.
                if (response.isRedirectError()) {
                    ++redirectCount;
                    mConnection.redirectHostAuth(response.getRedirectAddress());
                    // Note that unlike other errors, we do NOT return here; we just keep looping.
                } else {
                    // All other errors.
                    LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d",
                            getCommand(), response.getStatus(), result);
                    // TODO: This probably should return result.
                    return RESULT_OTHER_FAILURE;
                }
            } finally {
                response.close();
            }
        } while (redirectCount < MAX_REDIRECTS);

        // Non-redirects return immediately after handling, so the only way to reach here is if we
        // looped too many times.
        LogUtils.e(LOG_TAG, "Too many redirects");
        return RESULT_TOO_MANY_REDIRECTS;
    }

    protected void onRequestMade() {
        // This can be overridden to do any cleanup that must happen after the request has
        // been sent. It will always be called, regardless of the status of the request.
    }

    protected int handleHttpError(final int httpStatus) {
        // This function can be overriden if the child class needs to change the result code
        // based on the http response status.
        return RESULT_OTHER_FAILURE;
    }

    /**
     * Reset the protocol version to use for this connection. If it's changed, and our account is
     * persisted, also write back the changes to the DB. Note that this function is called at
     * the time of Account creation but does not update the Account object with the various flags
     * at that point in time.
     * TODO: Make sure that the Account flags are set properly in this function or a similar
     * function in the future. Right now the Account setup activity sets the flags, this is not
     * the right design.
     * @param protocolVersion The new protocol version to use, as a string.
     */
    protected final void setProtocolVersion(final String protocolVersion) {
        final long accountId = getAccountId();
        if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) {
            final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
            final ContentValues cv = new ContentValues(2);
            if (getProtocolVersion() >= 12.0) {
                final int oldFlags = Utility.getFirstRowInt(mContext, uri,
                        Account.ACCOUNT_FLAGS_PROJECTION, null, null, null,
                        Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0);
                final int newFlags = oldFlags |
                        Account.FLAGS_SUPPORTS_GLOBAL_SEARCH | Account.FLAGS_SUPPORTS_SEARCH |
                                Account.FLAGS_SUPPORTS_SMART_FORWARD;
                if (oldFlags != newFlags) {
                    cv.put(EmailContent.AccountColumns.FLAGS, newFlags);
                }
            }
            cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion);
            mContext.getContentResolver().update(uri, cv, null, null);
        }
    }

    /**
     * Create the request object for this operation.
     * Most operations use a POST, but some use other request types (e.g. Options).
     * @return An {@link HttpUriRequest}.
     * @throws IOException
     */
    private final HttpUriRequest makeRequest() throws IOException, MessageInvalidException {
        final String requestUri = getRequestUri();
        if (requestUri == null) {
            return mConnection.makeOptions();
        }

        HttpUriRequest req = mConnection.makePost(requestUri, getRequestEntity(),
                getRequestContentType(), addPolicyKeyHeaderToRequest());
        return req;
    }

    /**
     * The following functions MUST be overridden by subclasses; these are things that are unique
     * to each operation.
     */

    /**
     * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note
     * that if you override {@link #getRequestUri}, then this function may be unused for normal
     * operation, but all subclasses should return something non-null for use with logging.
     * @return The name of the command for this operation as defined by the EAS protocol, or for
     *         commands that don't need it, a suitable descriptive name for logging.
     */
    protected abstract String getCommand();

    /**
     * Build the {@link HttpEntity} which is used to construct the POST. Typically this function
     * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
     * If the subclass is not using a POST, then it should override this to return null.
     * @return The {@link HttpEntity} to pass to {@link com.android.exchange.service.EasServerConnection#makePost}.
     * @throws IOException
     */
    protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException;

    /**
     * Parse the response from the Exchange perform whatever actions are dictated by that.
     * @param response The {@link EasResponse} to our request.
     * @return A result code. Non-negative values are returned directly to the caller; negative
     *         values
     *
     * that is returned to the caller of {@link #performOperation}.
     * @throws IOException
     */
    protected abstract int handleResponse(final EasResponse response)
            throws IOException, CommandStatusException;

    /**
     * The following functions may be overriden by a subclass, but most operations will not need
     * to do so.
     */

    /**
     * Get the URI for the Exchange server and this operation. Most (signed in) operations need
     * not override this; the notable operation that needs to override it is auto-discover.
     * @return
     */
    protected String getRequestUri() {
        return mConnection.makeUriString(getCommand());
    }

    /**
     * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header.
     */
    protected boolean addPolicyKeyHeaderToRequest() {
        return true;
    }

    /**
     * @return The content type of this request.
     */
    protected String getRequestContentType() {
        return EAS_14_MIME_TYPE;
    }

    /**
     * @return The timeout to use for the POST.
     */
    protected long getTimeout() {
        return 30 * DateUtils.SECOND_IN_MILLIS;
    }

    /**
     * If 403 responses should be handled in a special way, this function should be overridden to
     * do that.
     * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error.
     */
    protected boolean handleForbidden() {
        return false;
    }

    /**
     * Handle a provisioning error. Subclasses may override this to do something different, e.g.
     * to validate rather than actually do the provisioning.
     * @return
     */
    protected boolean handleProvisionError() {
        final EasProvision provisionOperation = new EasProvision(this);
        return provisionOperation.provision();
    }

    /**
     * Convenience methods for subclasses to use.
     */

    /**
     * Convenience method to make an {@link HttpEntity} from {@link Serializer}.
     */
    protected final HttpEntity makeEntity(final Serializer s) {
        return new ByteArrayEntity(s.toByteArray());
    }

    /**
     * Check whether we should ask the server what protocol versions it supports and set this
     * account to use that version.
     * @return Whether we need a new protocol version from the server.
     */
    protected final boolean shouldGetProtocolVersion() {
        // TODO: Find conditions under which we should check other than not having one yet.
        return !mConnection.isProtocolVersionSet();
    }

    /**
     * @return The protocol version to use.
     */
    protected final double getProtocolVersion() {
        return mConnection.getProtocolVersion();
    }

    /**
     * @return Our useragent.
     */
    protected final String getUserAgent() {
        return mConnection.getUserAgent();
    }

    /**
     * @return Whether we succeeeded in registering the client cert.
     */
    protected final boolean registerClientCert() {
        return mConnection.registerClientCert();
    }

    /**
     * Add the device information to the current request.
     * @param s The {@link Serializer} for our current request.
     * @param context The {@link Context} for current device.
     * @param userAgent The user agent string that our connection use.
     */
    protected static void expandedAddDeviceInformationToSerializer(final Serializer s,
            final Context context, final String userAgent) throws IOException {
        final String deviceId;
        final String phoneNumber;
        final String operator;
        final TelephonyManager tm = (TelephonyManager)context.getSystemService(
                Context.TELEPHONY_SERVICE);
        if (tm != null) {
            deviceId = tm.getDeviceId();
            phoneNumber = tm.getLine1Number();
            // TODO: This is not perfect and needs to be improved, for at least two reasons:
            // 1) SIM cards can override this name.
            // 2) We don't resend this info to the server when we change networks.
            final String operatorName = tm.getNetworkOperatorName();
            final String operatorNumber = tm.getNetworkOperator();
            if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) {
                operator = operatorName + " (" + operatorNumber + ")";
            } else if (!TextUtils.isEmpty(operatorName)) {
                operator = operatorName;
            } else {
                operator = operatorNumber;
            }
        } else {
            deviceId = null;
            phoneNumber = null;
            operator = null;
        }

        // TODO: Right now, we won't send this information unless the device is provisioned again.
        // Potentially, this means that our phone number could be out of date if the user
        // switches sims. Is there something we can do to force a reprovision?
        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
        if (deviceId != null) {
            s.data(Tags.SETTINGS_IMEI, tm.getDeviceId());
        }
        // Set the device friendly name, if we have one.
        // TODO: Longer term, this should be done without a provider call.
        final Bundle deviceName = context.getContentResolver().call(
                EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null);
        if (deviceName != null) {
            final String friendlyName = deviceName.getString(EmailContent.DEVICE_FRIENDLY_NAME);
            if (!TextUtils.isEmpty(friendlyName)) {
                s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName);
            }
        }
        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
        if (phoneNumber != null) {
            s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber);
        }
        // TODO: Consider setting this, but make sure we know what it's used for.
        // If the user changes the device's locale and we don't do a reprovision, the server's
        // idea of the language will be wrong. Since we're not sure what this is used for,
        // right now we're leaving it out.
        //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage());
        s.data(Tags.SETTINGS_USER_AGENT, userAgent);
        if (operator != null) {
            s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator);
        }
        s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
    }

    /**
     * Add the device information to the current request.
     * @param s The {@link Serializer} that contains the payload for this request.
     */
    protected final void addDeviceInformationToSerializer(final Serializer s)
            throws IOException {
        final String userAgent = getUserAgent();
        expandedAddDeviceInformationToSerializer(s, mContext, userAgent);
    }

    /**
     * Convenience method for adding a Message to an account's outbox
     * @param account The {@link Account} from which to send the message.
     * @param msg the message to send
     */
    protected final void sendMessage(final Account account, final EmailContent.Message msg) {
        long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
        // TODO: Improve system mailbox handling.
        if (mailboxId == Mailbox.NO_MAILBOX) {
            LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId);
            final Mailbox outbox =
                    Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX);
            outbox.save(mContext);
            mailboxId = outbox.mId;
        }
        msg.mMailboxKey = mailboxId;
        msg.mAccountKey = account.mId;
        msg.save(mContext);
        requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress,
                Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId);
    }

    /**
     * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
     * @param amAccount The {@link android.accounts.Account} for the account we're pinging.
     * @param mailboxId The id of the mailbox that needs to sync.
     */
    protected static void requestSyncForMailbox(final android.accounts.Account amAccount,
            final long mailboxId) {
        final Bundle extras = Mailbox.createSyncBundle(mailboxId);
        ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
        LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s",
                amAccount.toString(), extras.toString());
    }

    protected static void requestSyncForMailboxes(final android.accounts.Account amAccount,
            final String authority, final ArrayList<Long> mailboxIds) {
        final Bundle extras = Mailbox.createSyncBundle(mailboxIds);
        /**
         * TODO: Right now, this function is only called by EasPing, should this function be
         * moved there?
         */
        ContentResolver.requestSync(amAccount, authority, extras);
        LogUtils.i(LOG_TAG, "EasOperation requestSyncForMailboxes  %s, %s",
                amAccount.toString(), extras.toString());
    }

    public static int translateSyncResultToUiResult(final int result) {
        switch (result) {
              case RESULT_TOO_MANY_REDIRECTS:
                return UIProvider.LastSyncResult.INTERNAL_ERROR;
            case RESULT_NETWORK_PROBLEM:
                return UIProvider.LastSyncResult.CONNECTION_ERROR;
            case RESULT_FORBIDDEN:
            case RESULT_PROVISIONING_ERROR:
            case RESULT_AUTHENTICATION_ERROR:
            case RESULT_CLIENT_CERTIFICATE_REQUIRED:
                return UIProvider.LastSyncResult.AUTH_ERROR;
            case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
                // Only used in validate, so there's never a syncResult to write to here.
                break;
            case RESULT_INITIALIZATION_FAILURE:
            case RESULT_HARD_DATA_FAILURE:
                return UIProvider.LastSyncResult.INTERNAL_ERROR;
            case RESULT_OTHER_FAILURE:
                return UIProvider.LastSyncResult.INTERNAL_ERROR;
        }
        return UIProvider.LastSyncResult.SUCCESS;
    }
}