EasOperation.java revision f8cccaecc8148d12d58ffcba5ce7366191316ac0
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.telephony.TelephonyManager;
28import android.text.TextUtils;
29import android.text.format.DateUtils;
30
31import com.android.emailcommon.provider.Account;
32import com.android.emailcommon.provider.EmailContent;
33import com.android.emailcommon.provider.HostAuth;
34import com.android.emailcommon.provider.Mailbox;
35import com.android.emailcommon.utility.Utility;
36import com.android.exchange.CommandStatusException;
37import com.android.exchange.Eas;
38import com.android.exchange.EasResponse;
39import com.android.exchange.adapter.Serializer;
40import com.android.exchange.adapter.Tags;
41import com.android.exchange.service.EasServerConnection;
42import com.android.mail.providers.UIProvider;
43import com.android.mail.utils.LogUtils;
44import com.google.common.annotations.VisibleForTesting;
45
46import org.apache.http.HttpEntity;
47import org.apache.http.client.methods.HttpUriRequest;
48import org.apache.http.entity.ByteArrayEntity;
49
50import java.io.IOException;
51import java.security.cert.CertificateException;
52import 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 */
101public 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    private EasServerConnection mConnection;
160
161    public class MessageInvalidException extends Exception {
162        public MessageInvalidException(final String message) {
163            super(message);
164        }
165    }
166
167    @VisibleForTesting
168    public void replaceEasServerConnection(EasServerConnection connection) {
169        mConnection = connection;
170    }
171
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     */
181    protected EasOperation(final Context context, final long accountId) {
182        mContext = context;
183        mAccountId = accountId;
184    }
185
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
193    protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
194        this(context, account, new EasServerConnection(context, account, hostAuth));
195    }
196
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     */
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     */
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     */
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     */
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
259    public final long getAccountId() {
260        return mAccountId;
261    }
262
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     */
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     */
279    public final void restart() {
280        mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART);
281    }
282
283    /**
284     * The skeleton of performing an operation. This function handles all the common code and
285     * error handling, calling into virtual functions that are implemented or overridden by the
286     * subclass to do the operation-specific logic.
287     *
288     * The result codes work as follows:
289     * - Negative values indicate common error codes and are defined above (the various RESULT_*
290     *   constants).
291     * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously
292     *   specific to the subclass, and may indicate success or error conditions.
293     *
294     * The common error codes primarily indicate conditions that occur when performing the POST
295     * itself, such as network errors and handling of the HTTP response. However, some errors that
296     * can be indicated in the HTTP response code can also be indicated in the payload of the
297     * response as well, so {@link #handleResponse} should in those cases return the appropriate
298     * negative result code, which will be handled the same as if it had been indicated in the HTTP
299     * response code.
300     *
301     * @return A result code for the outcome of this operation, as described above.
302     */
303    public int performOperation() {
304        // Make sure the account is loaded if it hasn't already been.
305        if (!init(false)) {
306            LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s",
307                    getAccountId(), getCommand());
308            return RESULT_INITIALIZATION_FAILURE;
309        }
310
311        // We handle server redirects by looping, but we need to protect against too much looping.
312        int redirectCount = 0;
313
314        do {
315            // Perform the HTTP request and handle exceptions.
316            final EasResponse response;
317            try {
318                try {
319                    response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout());
320                } finally {
321                    onRequestMade();
322                }
323            } catch (final IOException e) {
324                // If we were stopped, return the appropriate result code.
325                switch (mConnection.getStoppedReason()) {
326                    case EasServerConnection.STOPPED_REASON_ABORT:
327                        return RESULT_ABORT;
328                    case EasServerConnection.STOPPED_REASON_RESTART:
329                        return RESULT_RESTART;
330                    default:
331                        break;
332                }
333                // If we're here, then we had a IOException that's not from a stop request.
334                String message = e.getMessage();
335                if (message == null) {
336                    message = "(no message)";
337                }
338                LogUtils.i(LOG_TAG, "IOException while sending request: %s", message);
339                return RESULT_NETWORK_PROBLEM;
340            } catch (final CertificateException e) {
341                LogUtils.i(LOG_TAG, "CertificateException while sending request: %s",
342                        e.getMessage());
343                return RESULT_CLIENT_CERTIFICATE_REQUIRED;
344            } catch (final MessageInvalidException e) {
345                // This indicates that there is something wrong with the message locally, and it
346                // cannot be sent. We don't want to return success, because that's misleading,
347                // but on the other hand, we don't want to abort the sync, because that would
348                // prevent other messages from being sent.
349                LogUtils.d(LOG_TAG, "Exception sending request %s", e.getMessage());
350                return RESULT_NON_FATAL_ERROR;
351            } catch (final IllegalStateException e) {
352                // Subclasses use ISE to signal a hard error when building the request.
353                // TODO: Switch away from ISEs.
354                LogUtils.e(LOG_TAG, e, "Exception while sending request");
355                return RESULT_HARD_DATA_FAILURE;
356            }
357
358            // The POST completed, so process the response.
359            try {
360                final int result;
361                // First off, the success case.
362                if (response.isSuccess()) {
363                    int responseResult;
364                    try {
365                        responseResult = handleResponse(response);
366                    } catch (final IOException e) {
367                        LogUtils.e(LOG_TAG, e, "Exception while handling response");
368                        return RESULT_NETWORK_PROBLEM;
369                    } catch (final CommandStatusException e) {
370                        // For some operations (notably Sync & FolderSync), errors are signaled in
371                        // the payload of the response. These will have a HTTP 200 response, and the
372                        // error condition is only detected during response parsing.
373                        // The various parsers handle this by throwing a CommandStatusException.
374                        // TODO: Consider having the parsers return the errors instead of throwing.
375                        final int status = e.mStatus;
376                        LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status);
377                        if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
378                            responseResult = RESULT_PROVISIONING_ERROR;
379                        } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
380                            responseResult = RESULT_FORBIDDEN;
381                        } else {
382                            responseResult = RESULT_OTHER_FAILURE;
383                        }
384                    }
385                    result = responseResult;
386                } else {
387                    result = handleHttpError(response.getStatus());
388                }
389
390                // Non-negative results indicate success. Return immediately and bypass the error
391                // handling.
392                if (result >= EasOperation.RESULT_MIN_OK_RESULT) {
393                    return result;
394                }
395
396                // If this operation has distinct handling for 403 errors, do that.
397                if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
398                    LogUtils.e(LOG_TAG, "Forbidden response");
399                    return RESULT_FORBIDDEN;
400                }
401
402                // Handle provisioning errors.
403                if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
404                    if (handleProvisionError()) {
405                        // The provisioning error has been taken care of, so we should re-do this
406                        // request.
407                        LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying",
408                                getCommand());
409                        continue;
410                    }
411                    return RESULT_PROVISIONING_ERROR;
412                }
413
414                // Handle authentication errors.
415                if (response.isAuthError()) {
416                    LogUtils.e(LOG_TAG, "Authentication error");
417                    if (response.isMissingCertificate()) {
418                        return RESULT_CLIENT_CERTIFICATE_REQUIRED;
419                    }
420                    return RESULT_AUTHENTICATION_ERROR;
421                }
422
423                // Handle redirects.
424                if (response.isRedirectError()) {
425                    ++redirectCount;
426                    mConnection.redirectHostAuth(response.getRedirectAddress());
427                    // Note that unlike other errors, we do NOT return here; we just keep looping.
428                } else {
429                    // All other errors.
430                    LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d",
431                            getCommand(), response.getStatus(), result);
432                    // TODO: This probably should return result.
433                    return RESULT_OTHER_FAILURE;
434                }
435            } finally {
436                response.close();
437            }
438        } while (redirectCount < MAX_REDIRECTS);
439
440        // Non-redirects return immediately after handling, so the only way to reach here is if we
441        // looped too many times.
442        LogUtils.e(LOG_TAG, "Too many redirects");
443        return RESULT_TOO_MANY_REDIRECTS;
444    }
445
446    protected void onRequestMade() {
447        // This can be overridden to do any cleanup that must happen after the request has
448        // been sent. It will always be called, regardless of the status of the request.
449    }
450
451    protected int handleHttpError(final int httpStatus) {
452        // This function can be overriden if the child class needs to change the result code
453        // based on the http response status.
454        return RESULT_OTHER_FAILURE;
455    }
456
457    /**
458     * Reset the protocol version to use for this connection. If it's changed, and our account is
459     * persisted, also write back the changes to the DB. Note that this function is called at
460     * the time of Account creation but does not update the Account object with the various flags
461     * at that point in time.
462     * TODO: Make sure that the Account flags are set properly in this function or a similar
463     * function in the future. Right now the Account setup activity sets the flags, this is not
464     * the right design.
465     * @param protocolVersion The new protocol version to use, as a string.
466     */
467    protected final void setProtocolVersion(final String protocolVersion) {
468        final long accountId = getAccountId();
469        if (mConnection.setProtocolVersion(protocolVersion) && accountId != Account.NOT_SAVED) {
470            final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
471            final ContentValues cv = new ContentValues(2);
472            if (getProtocolVersion() >= 12.0) {
473                final int oldFlags = Utility.getFirstRowInt(mContext, uri,
474                        Account.ACCOUNT_FLAGS_PROJECTION, null, null, null,
475                        Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0);
476                final int newFlags = oldFlags |
477                        Account.FLAGS_SUPPORTS_GLOBAL_SEARCH | Account.FLAGS_SUPPORTS_SEARCH |
478                                Account.FLAGS_SUPPORTS_SMART_FORWARD;
479                if (oldFlags != newFlags) {
480                    cv.put(EmailContent.AccountColumns.FLAGS, newFlags);
481                }
482            }
483            cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion);
484            mContext.getContentResolver().update(uri, cv, null, null);
485        }
486    }
487
488    /**
489     * Create the request object for this operation.
490     * Most operations use a POST, but some use other request types (e.g. Options).
491     * @return An {@link HttpUriRequest}.
492     * @throws IOException
493     */
494    private final HttpUriRequest makeRequest() throws IOException, MessageInvalidException {
495        final String requestUri = getRequestUri();
496        if (requestUri == null) {
497            return mConnection.makeOptions();
498        }
499
500        HttpUriRequest req = mConnection.makePost(requestUri, getRequestEntity(),
501                getRequestContentType(), addPolicyKeyHeaderToRequest());
502        return req;
503    }
504
505    /**
506     * The following functions MUST be overridden by subclasses; these are things that are unique
507     * to each operation.
508     */
509
510    /**
511     * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note
512     * that if you override {@link #getRequestUri}, then this function may be unused for normal
513     * operation, but all subclasses should return something non-null for use with logging.
514     * @return The name of the command for this operation as defined by the EAS protocol, or for
515     *         commands that don't need it, a suitable descriptive name for logging.
516     */
517    protected abstract String getCommand();
518
519    /**
520     * Build the {@link HttpEntity} which is used to construct the POST. Typically this function
521     * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
522     * If the subclass is not using a POST, then it should override this to return null.
523     * @return The {@link HttpEntity} to pass to {@link com.android.exchange.service.EasServerConnection#makePost}.
524     * @throws IOException
525     */
526    protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException;
527
528    /**
529     * Parse the response from the Exchange perform whatever actions are dictated by that.
530     * @param response The {@link EasResponse} to our request.
531     * @return A result code. Non-negative values are returned directly to the caller; negative
532     *         values
533     *
534     * that is returned to the caller of {@link #performOperation}.
535     * @throws IOException
536     */
537    protected abstract int handleResponse(final EasResponse response)
538            throws IOException, CommandStatusException;
539
540    /**
541     * The following functions may be overriden by a subclass, but most operations will not need
542     * to do so.
543     */
544
545    /**
546     * Get the URI for the Exchange server and this operation. Most (signed in) operations need
547     * not override this; the notable operation that needs to override it is auto-discover.
548     * @return
549     */
550    protected String getRequestUri() {
551        return mConnection.makeUriString(getCommand());
552    }
553
554    /**
555     * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header.
556     */
557    protected boolean addPolicyKeyHeaderToRequest() {
558        return true;
559    }
560
561    /**
562     * @return The content type of this request.
563     */
564    protected String getRequestContentType() {
565        return EAS_14_MIME_TYPE;
566    }
567
568    /**
569     * @return The timeout to use for the POST.
570     */
571    protected long getTimeout() {
572        return 30 * DateUtils.SECOND_IN_MILLIS;
573    }
574
575    /**
576     * If 403 responses should be handled in a special way, this function should be overridden to
577     * do that.
578     * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error.
579     */
580    protected boolean handleForbidden() {
581        return false;
582    }
583
584    /**
585     * Handle a provisioning error. Subclasses may override this to do something different, e.g.
586     * to validate rather than actually do the provisioning.
587     * @return
588     */
589    protected boolean handleProvisionError() {
590        final EasProvision provisionOperation = new EasProvision(this);
591        return provisionOperation.provision();
592    }
593
594    /**
595     * Convenience methods for subclasses to use.
596     */
597
598    /**
599     * Convenience method to make an {@link HttpEntity} from {@link Serializer}.
600     */
601    protected final HttpEntity makeEntity(final Serializer s) {
602        return new ByteArrayEntity(s.toByteArray());
603    }
604
605    /**
606     * Check whether we should ask the server what protocol versions it supports and set this
607     * account to use that version.
608     * @return Whether we need a new protocol version from the server.
609     */
610    protected final boolean shouldGetProtocolVersion() {
611        // TODO: Find conditions under which we should check other than not having one yet.
612        return !mConnection.isProtocolVersionSet();
613    }
614
615    /**
616     * @return The protocol version to use.
617     */
618    protected final double getProtocolVersion() {
619        return mConnection.getProtocolVersion();
620    }
621
622    /**
623     * @return Our useragent.
624     */
625    protected final String getUserAgent() {
626        return mConnection.getUserAgent();
627    }
628
629    /**
630     * @return Whether we succeeeded in registering the client cert.
631     */
632    protected final boolean registerClientCert() {
633        return mConnection.registerClientCert();
634    }
635
636    /**
637     * Add the device information to the current request.
638     * @param s The {@link Serializer} for our current request.
639     * @param context The {@link Context} for current device.
640     * @param userAgent The user agent string that our connection use.
641     */
642    protected static void expandedAddDeviceInformationToSerializer(final Serializer s,
643            final Context context, final String userAgent) throws IOException {
644        final String deviceId;
645        final String phoneNumber;
646        final String operator;
647        final TelephonyManager tm = (TelephonyManager)context.getSystemService(
648                Context.TELEPHONY_SERVICE);
649        if (tm != null) {
650            deviceId = tm.getDeviceId();
651            phoneNumber = tm.getLine1Number();
652            // TODO: This is not perfect and needs to be improved, for at least two reasons:
653            // 1) SIM cards can override this name.
654            // 2) We don't resend this info to the server when we change networks.
655            final String operatorName = tm.getNetworkOperatorName();
656            final String operatorNumber = tm.getNetworkOperator();
657            if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) {
658                operator = operatorName + " (" + operatorNumber + ")";
659            } else if (!TextUtils.isEmpty(operatorName)) {
660                operator = operatorName;
661            } else {
662                operator = operatorNumber;
663            }
664        } else {
665            deviceId = null;
666            phoneNumber = null;
667            operator = null;
668        }
669
670        // TODO: Right now, we won't send this information unless the device is provisioned again.
671        // Potentially, this means that our phone number could be out of date if the user
672        // switches sims. Is there something we can do to force a reprovision?
673        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
674        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
675        if (deviceId != null) {
676            s.data(Tags.SETTINGS_IMEI, tm.getDeviceId());
677        }
678        // Set the device friendly name, if we have one.
679        // TODO: Longer term, this should be done without a provider call.
680        final Bundle deviceName = context.getContentResolver().call(
681                EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null);
682        if (deviceName != null) {
683            final String friendlyName = deviceName.getString(EmailContent.DEVICE_FRIENDLY_NAME);
684            if (!TextUtils.isEmpty(friendlyName)) {
685                s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName);
686            }
687        }
688        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
689        if (phoneNumber != null) {
690            s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber);
691        }
692        // TODO: Consider setting this, but make sure we know what it's used for.
693        // If the user changes the device's locale and we don't do a reprovision, the server's
694        // idea of the language will be wrong. Since we're not sure what this is used for,
695        // right now we're leaving it out.
696        //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage());
697        s.data(Tags.SETTINGS_USER_AGENT, userAgent);
698        if (operator != null) {
699            s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator);
700        }
701        s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
702    }
703
704    /**
705     * Add the device information to the current request.
706     * @param s The {@link Serializer} that contains the payload for this request.
707     */
708    protected final void addDeviceInformationToSerializer(final Serializer s)
709            throws IOException {
710        final String userAgent = getUserAgent();
711        expandedAddDeviceInformationToSerializer(s, mContext, userAgent);
712    }
713
714    /**
715     * Convenience method for adding a Message to an account's outbox
716     * @param account The {@link Account} from which to send the message.
717     * @param msg the message to send
718     */
719    protected final void sendMessage(final Account account, final EmailContent.Message msg) {
720        long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
721        // TODO: Improve system mailbox handling.
722        if (mailboxId == Mailbox.NO_MAILBOX) {
723            LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId);
724            final Mailbox outbox =
725                    Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX);
726            outbox.save(mContext);
727            mailboxId = outbox.mId;
728        }
729        msg.mMailboxKey = mailboxId;
730        msg.mAccountKey = account.mId;
731        msg.save(mContext);
732        requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress,
733                Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId);
734    }
735
736    /**
737     * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
738     * @param amAccount The {@link android.accounts.Account} for the account we're pinging.
739     * @param mailboxId The id of the mailbox that needs to sync.
740     */
741    protected static void requestSyncForMailbox(final android.accounts.Account amAccount,
742            final long mailboxId) {
743        final Bundle extras = Mailbox.createSyncBundle(mailboxId);
744        ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
745        LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s",
746                amAccount.toString(), extras.toString());
747    }
748
749    protected static void requestSyncForMailboxes(final android.accounts.Account amAccount,
750            final String authority, final ArrayList<Long> mailboxIds) {
751        final Bundle extras = Mailbox.createSyncBundle(mailboxIds);
752        /**
753         * Please note that it is very possible that we are trying to send a request to the
754         * email sync adapter even though email push is turned off (i.e. this account might only
755         * be syncing calendar or contacts). In this situation we need to make sure that
756         * this request is marked as manual as to ensure that the sync manager does not drop it
757         * on the floor. Right now, this function is only called by EasPing, if it is every called
758         * by another caller, then we should reconsider if manual=true is the right thing to do.
759         */
760        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
761        ContentResolver.requestSync(amAccount, authority, extras);
762        LogUtils.i(LOG_TAG, "EasOperation requestSyncForMailboxes  %s, %s",
763                amAccount.toString(), extras.toString());
764    }
765
766    public static int translateSyncResultToUiResult(final int result) {
767        switch (result) {
768              case RESULT_TOO_MANY_REDIRECTS:
769                return UIProvider.LastSyncResult.INTERNAL_ERROR;
770            case RESULT_NETWORK_PROBLEM:
771                return UIProvider.LastSyncResult.CONNECTION_ERROR;
772            case RESULT_FORBIDDEN:
773            case RESULT_PROVISIONING_ERROR:
774            case RESULT_AUTHENTICATION_ERROR:
775            case RESULT_CLIENT_CERTIFICATE_REQUIRED:
776                return UIProvider.LastSyncResult.AUTH_ERROR;
777            case RESULT_PROTOCOL_VERSION_UNSUPPORTED:
778                // Only used in validate, so there's never a syncResult to write to here.
779                break;
780            case RESULT_INITIALIZATION_FAILURE:
781            case RESULT_HARD_DATA_FAILURE:
782                return UIProvider.LastSyncResult.INTERNAL_ERROR;
783            case RESULT_OTHER_FAILURE:
784                return UIProvider.LastSyncResult.INTERNAL_ERROR;
785        }
786        return UIProvider.LastSyncResult.SUCCESS;
787    }
788}
789