EasSyncService.java revision e6c2456aa6c00ef78c6d1d1621511d7ef8507f83
1/*
2 * Copyright (C) 2008-2009 Marc Blank
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.exchange;
19
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Entity;
25import android.database.Cursor;
26import android.net.TrafficStats;
27import android.net.Uri;
28import android.os.Build;
29import android.os.Bundle;
30import android.os.RemoteException;
31import android.provider.CalendarContract.Attendees;
32import android.provider.CalendarContract.Events;
33import android.text.TextUtils;
34import android.util.Base64;
35import android.util.Log;
36import android.util.Xml;
37
38import com.android.emailcommon.TrafficFlags;
39import com.android.emailcommon.mail.Address;
40import com.android.emailcommon.mail.MeetingInfo;
41import com.android.emailcommon.mail.MessagingException;
42import com.android.emailcommon.mail.PackedString;
43import com.android.emailcommon.provider.Account;
44import com.android.emailcommon.provider.EmailContent.AccountColumns;
45import com.android.emailcommon.provider.EmailContent.Message;
46import com.android.emailcommon.provider.EmailContent.MessageColumns;
47import com.android.emailcommon.provider.EmailContent.SyncColumns;
48import com.android.emailcommon.provider.HostAuth;
49import com.android.emailcommon.provider.Mailbox;
50import com.android.emailcommon.provider.Policy;
51import com.android.emailcommon.provider.ProviderUnavailableException;
52import com.android.emailcommon.service.EmailServiceConstants;
53import com.android.emailcommon.service.EmailServiceProxy;
54import com.android.emailcommon.service.EmailServiceStatus;
55import com.android.emailcommon.service.PolicyServiceProxy;
56import com.android.emailsync.AbstractSyncService;
57import com.android.emailsync.PartRequest;
58import com.android.emailsync.Request;
59import com.android.emailcommon.utility.EmailClientConnectionManager;
60import com.android.emailcommon.utility.Utility;
61import com.android.exchange.CommandStatusException.CommandStatus;
62import com.android.exchange.adapter.AbstractSyncAdapter;
63import com.android.exchange.adapter.AccountSyncAdapter;
64import com.android.exchange.adapter.AttachmentLoader;
65import com.android.exchange.adapter.CalendarSyncAdapter;
66import com.android.exchange.adapter.ContactsSyncAdapter;
67import com.android.exchange.adapter.EmailSyncAdapter;
68import com.android.exchange.adapter.FolderSyncParser;
69import com.android.exchange.adapter.GalParser;
70import com.android.exchange.adapter.MeetingResponseParser;
71import com.android.exchange.adapter.MoveItemsParser;
72import com.android.exchange.adapter.Parser.EmptyStreamException;
73import com.android.exchange.adapter.ProvisionParser;
74import com.android.exchange.adapter.Serializer;
75import com.android.exchange.adapter.SettingsParser;
76import com.android.exchange.adapter.Tags;
77import com.android.exchange.provider.GalResult;
78import com.android.exchange.utility.CalendarUtilities;
79import com.google.common.annotations.VisibleForTesting;
80
81import org.apache.http.Header;
82import org.apache.http.HttpEntity;
83import org.apache.http.HttpResponse;
84import org.apache.http.HttpStatus;
85import org.apache.http.client.HttpClient;
86import org.apache.http.client.methods.HttpOptions;
87import org.apache.http.client.methods.HttpPost;
88import org.apache.http.client.methods.HttpRequestBase;
89import org.apache.http.entity.ByteArrayEntity;
90import org.apache.http.entity.StringEntity;
91import org.apache.http.impl.client.DefaultHttpClient;
92import org.apache.http.params.BasicHttpParams;
93import org.apache.http.params.HttpConnectionParams;
94import org.apache.http.params.HttpParams;
95import org.xmlpull.v1.XmlPullParser;
96import org.xmlpull.v1.XmlPullParserException;
97import org.xmlpull.v1.XmlPullParserFactory;
98import org.xmlpull.v1.XmlSerializer;
99
100import java.io.ByteArrayOutputStream;
101import java.io.IOException;
102import java.io.InputStream;
103import java.lang.Thread.State;
104import java.net.URI;
105import java.security.cert.CertificateException;
106
107public class EasSyncService extends AbstractSyncService {
108    // DO NOT CHECK IN SET TO TRUE
109    public static final boolean DEBUG_GAL_SERVICE = false;
110
111    protected static final String PING_COMMAND = "Ping";
112    // Command timeout is the the time allowed for reading data from an open connection before an
113    // IOException is thrown.  After a small added allowance, our watchdog alarm goes off (allowing
114    // us to detect a silently dropped connection).  The allowance is defined below.
115    static public final int COMMAND_TIMEOUT = 30*SECONDS;
116    // Connection timeout is the time given to connect to the server before reporting an IOException
117    static private final int CONNECTION_TIMEOUT = 20*SECONDS;
118    // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
119    static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
120
121    static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
122        "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
123    static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
124    static protected final int EAS_REDIRECT_CODE = 451;
125
126    static public final int INTERNAL_SERVER_ERROR_CODE = 500;
127
128    static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
129    static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
130
131    static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
132    // The amount of time we allow for a thread to release its post lock after receiving an alert
133    static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
134
135    // The EAS protocol Provision status for "we implement all of the policies"
136    static private final String PROVISION_STATUS_OK = "1";
137    // The EAS protocol Provision status meaning "we partially implement the policies"
138    static private final String PROVISION_STATUS_PARTIAL = "2";
139
140    static /*package*/ final String DEVICE_TYPE = "Android";
141    static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
142        Eas.CLIENT_VERSION;
143
144    // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
145    // forcing it to stop.  This number has been determined empirically.
146    static private final int MAX_LOOPING_COUNT = 100;
147    // Reasonable default
148    public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
149    public Double mProtocolVersionDouble;
150    protected String mDeviceId = null;
151    @VisibleForTesting
152    String mAuthString = null;
153    @VisibleForTesting
154    String mUserString = null;
155    @VisibleForTesting
156    String mBaseUriString = null;
157    public String mHostAddress;
158    public String mUserName;
159    public String mPassword;
160
161    // The HttpPost in progress
162    private volatile HttpPost mPendingPost = null;
163    // Whether a POST was aborted due to alarm (watchdog alarm)
164    protected boolean mPostAborted = false;
165    // Whether a POST was aborted due to reset
166    protected boolean mPostReset = false;
167
168    // The parameters for the connection must be modified through setConnectionParameters
169    private boolean mSsl = true;
170    private boolean mTrustSsl = false;
171    private String mClientCertAlias = null;
172    private int mPort;
173
174    public ContentResolver mContentResolver;
175    // Whether or not the sync service is valid (usable)
176    public boolean mIsValid = true;
177
178    // Whether the most recent upsync failed (status 7)
179    public boolean mUpsyncFailed = false;
180
181    protected EasSyncService(Context _context, Mailbox _mailbox) {
182        super(_context, _mailbox);
183        mContentResolver = _context.getContentResolver();
184        if (mAccount == null) {
185            mIsValid = false;
186            return;
187        }
188        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
189        if (ha == null) {
190            mIsValid = false;
191            return;
192        }
193        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
194        mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
195    }
196
197    private EasSyncService(String prefix) {
198        super(prefix);
199    }
200
201    public EasSyncService() {
202        this("EAS Validation");
203    }
204
205    public static EasSyncService getServiceForMailbox(Context context, Mailbox m) {
206        switch(m.mType) {
207            case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
208                return new EasAccountService(context, m);
209            case Mailbox.TYPE_OUTBOX:
210                return new EasOutboxService(context, m);
211            default:
212                return new EasSyncService(context, m);
213        }
214    }
215
216    public void resetCalendarSyncKey() {
217        CalendarSyncAdapter adapter = new CalendarSyncAdapter(this);
218            try {
219                adapter.setSyncKey("0", false);
220            } catch (IOException e) {
221                // The provider can't be reached; nothing to be done
222            }
223    }
224
225    /**
226     * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
227     * socket timeout without having thrown an Exception
228     *
229     * @return true if the POST was successfully stopped; false if we've failed and interrupted
230     * the thread
231     */
232    @Override
233    public boolean alarm() {
234        HttpPost post;
235        if (mThread == null) return true;
236        String threadName = mThread.getName();
237
238        // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
239        // executePostWithTimeout (which executes the HttpPost) also uses this lock
240        synchronized(getSynchronizer()) {
241            // Get a reference to the current post lock
242            post = mPendingPost;
243            if (post != null) {
244                if (Eas.USER_LOG) {
245                    URI uri = post.getURI();
246                    if (uri != null) {
247                        String query = uri.getQuery();
248                        if (query == null) {
249                            query = "POST";
250                        }
251                        userLog(threadName, ": Alert, aborting ", query);
252                    } else {
253                        userLog(threadName, ": Alert, no URI?");
254                    }
255                }
256                // Abort the POST
257                mPostAborted = true;
258                post.abort();
259            } else {
260                // If there's no POST, we're done
261                userLog("Alert, no pending POST");
262                return true;
263            }
264        }
265
266        // Wait for the POST to finish
267        try {
268            Thread.sleep(POST_LOCK_TIMEOUT);
269        } catch (InterruptedException e) {
270        }
271
272        State s = mThread.getState();
273        if (Eas.USER_LOG) {
274            userLog(threadName + ": State = " + s.name());
275        }
276
277        synchronized (getSynchronizer()) {
278            // If the thread is still hanging around and the same post is pending, let's try to
279            // stop the thread with an interrupt.
280            if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
281                mStop = true;
282                mThread.interrupt();
283                userLog("Interrupting...");
284                // Let the caller know we had to interrupt the thread
285                return false;
286            }
287        }
288        // Let the caller know that the alarm was handled normally
289        return true;
290    }
291
292    @Override
293    public void reset() {
294        synchronized(getSynchronizer()) {
295            if (mPendingPost != null) {
296                URI uri = mPendingPost.getURI();
297                if (uri != null) {
298                    String query = uri.getQuery();
299                    if (query.startsWith("Cmd=Ping")) {
300                        userLog("Reset, aborting Ping");
301                        mPostReset = true;
302                        mPendingPost.abort();
303                    }
304                }
305            }
306        }
307    }
308
309    @Override
310    public void stop() {
311        mStop = true;
312        synchronized(getSynchronizer()) {
313            if (mPendingPost != null) {
314                mPendingPost.abort();
315            }
316        }
317    }
318
319    void setupProtocolVersion(EasSyncService service, Header versionHeader)
320            throws MessagingException {
321        // The string is a comma separated list of EAS versions in ascending order
322        // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
323        String supportedVersions = versionHeader.getValue();
324        userLog("Server supports versions: ", supportedVersions);
325        String[] supportedVersionsArray = supportedVersions.split(",");
326        String ourVersion = null;
327        // Find the most recent version we support
328        for (String version: supportedVersionsArray) {
329            if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
330                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
331                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) ||
332                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) ||
333                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) {
334                ourVersion = version;
335            }
336        }
337        // If we don't support any of the servers supported versions, throw an exception here
338        // This will cause validation to fail
339        if (ourVersion == null) {
340            Log.w(TAG, "No supported EAS versions: " + supportedVersions);
341            throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
342        } else {
343            // Debug code for testing EAS 14.0; disables support for EAS 14.1
344            // "adb shell setprop log.tag.Exchange14 VERBOSE"
345            if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) &&
346                    Log.isLoggable("Exchange14", Log.VERBOSE)) {
347                ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010;
348            }
349            service.mProtocolVersion = ourVersion;
350            service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
351            Account account = service.mAccount;
352            if (account != null) {
353                account.mProtocolVersion = ourVersion;
354                // Fixup search flags, if they're not set
355                if (service.mProtocolVersionDouble >= 12.0 &&
356                        (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
357                    if (account.isSaved()) {
358                        ContentValues cv = new ContentValues();
359                        account.mFlags |=
360                            Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
361                        cv.put(AccountColumns.FLAGS, account.mFlags);
362                        account.update(service.mContext, cv);
363                    }
364                }
365            }
366        }
367    }
368
369    /**
370     * Create an EasSyncService for the specified account
371     *
372     * @param context the caller's context
373     * @param account the account
374     * @return the service, or null if the account is on hold or hasn't been initialized
375     */
376    public static EasSyncService setupServiceForAccount(Context context, Account account) {
377        // Just return null if we're on security hold
378        if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
379            return null;
380        }
381        // If there's no protocol version, we're not initialized
382        String protocolVersion = account.mProtocolVersion;
383        if (protocolVersion == null) {
384            return null;
385        }
386        EasSyncService svc = new EasSyncService("OutOfBand");
387        HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
388        svc.mProtocolVersion = protocolVersion;
389        svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
390        svc.mContext = context;
391        svc.mHostAddress = ha.mAddress;
392        svc.mUserName = ha.mLogin;
393        svc.mPassword = ha.mPassword;
394        try {
395            svc.setConnectionParameters(ha);
396            svc.mDeviceId = ExchangeService.getDeviceId(context);
397        } catch (IOException e) {
398            return null;
399        } catch (CertificateException e) {
400            return null;
401        }
402        svc.mAccount = account;
403        return svc;
404    }
405
406    /**
407     * Get a redirect address and validate against it
408     * @param resp the EasResponse to our POST
409     * @param hostAuth the HostAuth we're using to validate
410     * @return true if we have an updated HostAuth (with redirect address); false otherwise
411     */
412    protected boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
413        Header locHeader = resp.getHeader("X-MS-Location");
414        if (locHeader != null) {
415            String loc;
416            try {
417                loc = locHeader.getValue();
418                // Reset our host address and uncache our base uri
419                mHostAddress = Uri.parse(loc).getHost();
420                mBaseUriString = null;
421                hostAuth.mAddress = mHostAddress;
422                userLog("Redirecting to: " + loc);
423                return true;
424            } catch (RuntimeException e) {
425                // Just don't crash if the Uri is illegal
426            }
427        }
428        return false;
429    }
430
431    private static final int MAX_REDIRECTS = 3;
432    private int mRedirectCount = 0;
433
434    @Override
435    public Bundle validateAccount(HostAuth hostAuth, Context context) {
436        Bundle bundle = new Bundle();
437        int resultCode = MessagingException.NO_ERROR;
438        try {
439            userLog("Testing EAS: ", hostAuth.mAddress, ", ", hostAuth.mLogin,
440                    ", ssl = ", hostAuth.shouldUseSsl() ? "1" : "0");
441            mContext = context;
442            mHostAddress = hostAuth.mAddress;
443            mUserName = hostAuth.mLogin;
444            mPassword = hostAuth.mPassword;
445
446            setConnectionParameters(hostAuth);
447            mDeviceId = ExchangeService.getDeviceId(context);
448            mAccount = new Account();
449            mAccount.mEmailAddress = hostAuth.mLogin;
450            EasResponse resp = sendHttpClientOptions();
451            try {
452                int code = resp.getStatus();
453                userLog("Validation (OPTIONS) response: " + code);
454                if (code == HttpStatus.SC_OK) {
455                    // No exception means successful validation
456                    Header commands = resp.getHeader("MS-ASProtocolCommands");
457                    Header versions = resp.getHeader("ms-asprotocolversions");
458                    // Make sure we've got the right protocol version set up
459                    try {
460                        if (commands == null || versions == null) {
461                            userLog("OPTIONS response without commands or versions");
462                            // We'll treat this as a protocol exception
463                            throw new MessagingException(0);
464                        }
465                        setupProtocolVersion(this, versions);
466                    } catch (MessagingException e) {
467                        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
468                                MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
469                        return bundle;
470                    }
471
472                    // Run second test here for provisioning failures using FolderSync
473                    userLog("Try folder sync");
474                    // Send "0" as the sync key for new accounts; otherwise, use the current key
475                    String syncKey = "0";
476                    Account existingAccount = Utility.findExistingAccount(
477                            context, -1L, hostAuth.mAddress, hostAuth.mLogin);
478                    if (existingAccount != null && existingAccount.mSyncKey != null) {
479                        syncKey = existingAccount.mSyncKey;
480                    }
481                    Serializer s = new Serializer();
482                    s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
483                        .end().end().done();
484                    resp = sendHttpClientPost("FolderSync", s.toByteArray());
485                    code = resp.getStatus();
486                    // Handle HTTP error responses accordingly
487                    if (code == HttpStatus.SC_FORBIDDEN) {
488                        // For validation only, we take 403 as ACCESS_DENIED (the account isn't
489                        // authorized, possibly due to device type)
490                        resultCode = MessagingException.ACCESS_DENIED;
491                    } else if (EasResponse.isProvisionError(code)) {
492                        // The device needs to have security policies enforced
493                        throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
494                    } else if (code == HttpStatus.SC_NOT_FOUND) {
495                        // We get a 404 from OWA addresses (which are NOT EAS addresses)
496                        resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
497                    } else if (code == HttpStatus.SC_UNAUTHORIZED) {
498                        resultCode = resp.isMissingCertificate()
499                                ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
500                                : MessagingException.AUTHENTICATION_FAILED;
501                    } else if (code != HttpStatus.SC_OK) {
502                        if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
503                                getValidateRedirect(resp, hostAuth)) {
504                            return validateAccount(hostAuth, context);
505                        }
506                        // Fail generically with anything other than success
507                        userLog("Unexpected response for FolderSync: ", code);
508                        resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
509                    } else {
510                        // We need to parse the result to see if we've got a provisioning issue
511                        // (EAS 14.0 only)
512                        if (!resp.isEmpty()) {
513                            InputStream is = resp.getInputStream();
514                            // Create the parser with statusOnly set to true; we only care about
515                            // seeing if a CommandStatusException is thrown (indicating a
516                            // provisioning failure)
517                            new FolderSyncParser(is, new AccountSyncAdapter(this), true).parse();
518                        }
519                        userLog("Validation successful");
520                    }
521                } else if (EasResponse.isAuthError(code)) {
522                    userLog("Authentication failed");
523                    resultCode = resp.isMissingCertificate()
524                            ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
525                            : MessagingException.AUTHENTICATION_FAILED;
526                } else if (code == INTERNAL_SERVER_ERROR_CODE) {
527                    // For Exchange 2003, this could mean an authentication failure OR server error
528                    userLog("Internal server error");
529                    resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
530                } else {
531                    if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
532                            getValidateRedirect(resp, hostAuth)) {
533                        return validateAccount(hostAuth, context);
534                    }
535                    // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
536                    userLog("Validation failed, reporting I/O error: ", code);
537                    resultCode = MessagingException.IOERROR;
538                }
539            } catch (CommandStatusException e) {
540                int status = e.mStatus;
541                if (CommandStatus.isNeedsProvisioning(status)) {
542                    // Get the policies and see if we are able to support them
543                    ProvisionParser pp = canProvision(this);
544                    if (pp != null && pp.hasSupportablePolicySet()) {
545                        // Set the proper result code and save the PolicySet in our Bundle
546                        resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
547                        bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
548                                pp.getPolicy());
549                        if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
550                            mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
551                            if (!sendSettings()) {
552                                userLog("Denied access: ", CommandStatus.toString(status));
553                                resultCode = MessagingException.ACCESS_DENIED;
554                            }
555                        }
556                    } else {
557                        // If not, set the proper code (the account will not be created)
558                        resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
559                        bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
560                                pp.getPolicy());
561                    }
562                } else if (CommandStatus.isDeniedAccess(status)) {
563                    userLog("Denied access: ", CommandStatus.toString(status));
564                    resultCode = MessagingException.ACCESS_DENIED;
565                } else if (CommandStatus.isTransientError(status)) {
566                    userLog("Transient error: ", CommandStatus.toString(status));
567                    resultCode = MessagingException.IOERROR;
568                } else {
569                    userLog("Unexpected response: ", CommandStatus.toString(status));
570                    resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
571                }
572            } finally {
573                resp.close();
574           }
575        } catch (IOException e) {
576            Throwable cause = e.getCause();
577            if (cause != null && cause instanceof CertificateException) {
578                // This could be because the server's certificate failed to validate.
579                userLog("CertificateException caught: ", e.getMessage());
580                resultCode = MessagingException.GENERAL_SECURITY;
581            }
582            userLog("IOException caught: ", e.getMessage());
583            resultCode = MessagingException.IOERROR;
584        } catch (CertificateException e) {
585            // This occurs if the client certificate the user specified is invalid/inaccessible.
586            userLog("CertificateException caught: ", e.getMessage());
587            resultCode = MessagingException.CLIENT_CERTIFICATE_ERROR;
588        }
589        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
590        return bundle;
591    }
592
593    /**
594     * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
595     * it can be reused
596     *
597     * @param resp the HttpResponse that indicates a redirect (451)
598     * @param post the HttpPost that was originally sent to the server
599     * @return the HttpPost, updated with the redirect location
600     */
601    private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
602        Header locHeader = resp.getFirstHeader("X-MS-Location");
603        if (locHeader != null) {
604            String loc = locHeader.getValue();
605            // If we've gotten one and it shows signs of looking like an address, we try
606            // sending our request there
607            if (loc != null && loc.startsWith("http")) {
608                post.setURI(URI.create(loc));
609                return post;
610            }
611        }
612        return null;
613    }
614
615    /**
616     * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
617     * return the HttpResponse.  If we get a 401 (unauthorized) error and we're using the
618     * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
619     *
620     * @param client the HttpClient to be used for the request
621     * @param post the HttpPost we're going to send
622     * @param canRetry whether we can retry using the bare name on an authentication failure (401)
623     * @return an HttpResponse from the original or redirect server
624     * @throws IOException on any IOException within the HttpClient code
625     * @throws MessagingException
626     */
627    private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
628            throws IOException, MessagingException {
629        userLog("Posting autodiscover to: " + post.getURI());
630        EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
631        int code = resp.getStatus();
632        // On a redirect, try the new location
633        if (code == EAS_REDIRECT_CODE) {
634            post = getRedirect(resp.mResponse, post);
635            if (post != null) {
636                userLog("Posting autodiscover to redirect: " + post.getURI());
637                return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
638            }
639        // 401 (Unauthorized) is for true auth errors when used in Autodiscover
640        } else if (code == HttpStatus.SC_UNAUTHORIZED) {
641            if (canRetry && mUserName.contains("@")) {
642                // Try again using the bare user name
643                int atSignIndex = mUserName.indexOf('@');
644                mUserName = mUserName.substring(0, atSignIndex);
645                cacheAuthUserAndBaseUriStrings();
646                userLog("401 received; trying username: ", mUserName);
647                // Recreate the basic authentication string and reset the header
648                post.removeHeaders("Authorization");
649                post.setHeader("Authorization", mAuthString);
650                return postAutodiscover(client, post, false);
651            }
652            throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
653        // 403 (and others) we'll just punt on
654        } else if (code != HttpStatus.SC_OK) {
655            // We'll try the next address if this doesn't work
656            userLog("Code: " + code + ", throwing IOException");
657            throw new IOException();
658        }
659        return resp;
660    }
661
662    /**
663     * Convert an EAS server url to a HostAuth host address
664     * @param url a url, as provided by the Exchange server
665     * @return our equivalent host address
666     */
667    protected String autodiscoverUrlToHostAddress(String url) {
668        if (url == null) return null;
669        // We need to extract the server address from a url
670        return Uri.parse(url).getHost();
671    }
672
673    /**
674     * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
675     * only an email address and the password
676     *
677     * @param userName the user's email address
678     * @param password the user's password
679     * @return a HostAuth ready to be saved in an Account or null (failure)
680     */
681    public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
682        XmlSerializer s = Xml.newSerializer();
683        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
684        HostAuth hostAuth = new HostAuth();
685        Bundle bundle = new Bundle();
686        bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
687                MessagingException.NO_ERROR);
688        try {
689            // Build the XML document that's sent to the autodiscover server(s)
690            s.setOutput(os, "UTF-8");
691            s.startDocument("UTF-8", false);
692            s.startTag(null, "Autodiscover");
693            s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
694            s.startTag(null, "Request");
695            s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
696            s.startTag(null, "AcceptableResponseSchema");
697            s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
698            s.endTag(null, "AcceptableResponseSchema");
699            s.endTag(null, "Request");
700            s.endTag(null, "Autodiscover");
701            s.endDocument();
702            String req = os.toString();
703
704            // Initialize the user name and password
705            mUserName = userName;
706            mPassword = password;
707            // Port is always 443 and SSL is used
708            mPort = 443;
709            mSsl = true;
710
711            // Make sure the authentication string is recreated and cached
712            cacheAuthUserAndBaseUriStrings();
713
714            // Split out the domain name
715            int amp = userName.indexOf('@');
716            // The UI ensures that userName is a valid email address
717            if (amp < 0) {
718                throw new RemoteException();
719            }
720            String domain = userName.substring(amp + 1);
721
722            // There are up to four attempts here; the two URLs that we're supposed to try per the
723            // specification, and up to one redirect for each (handled in postAutodiscover)
724            // Note: The expectation is that, of these four attempts, only a single server will
725            // actually be identified as the autodiscover server.  For the identified server,
726            // we may also try a 2nd connection with a different format (bare name).
727
728            // Try the domain first and see if we can get a response
729            HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
730            setHeaders(post, false);
731            post.setHeader("Content-Type", "text/xml");
732            post.setEntity(new StringEntity(req));
733            HttpClient client = getHttpClient(COMMAND_TIMEOUT);
734            EasResponse resp;
735            try {
736                resp = postAutodiscover(client, post, true /*canRetry*/);
737            } catch (IOException e1) {
738                userLog("IOException in autodiscover; trying alternate address");
739                // We catch the IOException here because we have an alternate address to try
740                post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
741                // If we fail here, we're out of options, so we let the outer try catch the
742                // IOException and return null
743                resp = postAutodiscover(client, post, true /*canRetry*/);
744            }
745
746            try {
747                // Get the "final" code; if it's not 200, just return null
748                int code = resp.getStatus();
749                userLog("Code: " + code);
750                if (code != HttpStatus.SC_OK) return null;
751
752                InputStream is = resp.getInputStream();
753                // The response to Autodiscover is regular XML (not WBXML)
754                // If we ever get an error in this process, we'll just punt and return null
755                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
756                XmlPullParser parser = factory.newPullParser();
757                parser.setInput(is, "UTF-8");
758                int type = parser.getEventType();
759                if (type == XmlPullParser.START_DOCUMENT) {
760                    type = parser.next();
761                    if (type == XmlPullParser.START_TAG) {
762                        String name = parser.getName();
763                        if (name.equals("Autodiscover")) {
764                            hostAuth = new HostAuth();
765                            parseAutodiscover(parser, hostAuth);
766                            // On success, we'll have a server address and login
767                            if (hostAuth.mAddress != null) {
768                                // Fill in the rest of the HostAuth
769                                // We use the user name and password that were successful during
770                                // the autodiscover process
771                                hostAuth.mLogin = mUserName;
772                                hostAuth.mPassword = mPassword;
773                                // Note: there is no way we can auto-discover the proper client
774                                // SSL certificate to use, if one is needed.
775                                hostAuth.mPort = 443;
776                                hostAuth.mProtocol = "eas";
777                                hostAuth.mFlags =
778                                    HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
779                                bundle.putParcelable(
780                                        EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
781                            } else {
782                                bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
783                                        MessagingException.UNSPECIFIED_EXCEPTION);
784                            }
785                        }
786                    }
787                }
788            } catch (XmlPullParserException e1) {
789                // This would indicate an I/O error of some sort
790                // We will simply return null and user can configure manually
791            } finally {
792               resp.close();
793            }
794        // There's no reason at all for exceptions to be thrown, and it's ok if so.
795        // We just won't do auto-discover; user can configure manually
796       } catch (IllegalArgumentException e) {
797             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
798                     MessagingException.UNSPECIFIED_EXCEPTION);
799       } catch (IllegalStateException e) {
800            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
801                    MessagingException.UNSPECIFIED_EXCEPTION);
802       } catch (IOException e) {
803            userLog("IOException in Autodiscover", e);
804            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
805                    MessagingException.IOERROR);
806        } catch (MessagingException e) {
807            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
808                    MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
809        }
810        return bundle;
811    }
812
813    void parseServer(XmlPullParser parser, HostAuth hostAuth)
814            throws XmlPullParserException, IOException {
815        boolean mobileSync = false;
816        while (true) {
817            int type = parser.next();
818            if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
819                break;
820            } else if (type == XmlPullParser.START_TAG) {
821                String name = parser.getName();
822                if (name.equals("Type")) {
823                    if (parser.nextText().equals("MobileSync")) {
824                        mobileSync = true;
825                    }
826                } else if (mobileSync && name.equals("Url")) {
827                    String hostAddress =
828                        autodiscoverUrlToHostAddress(parser.nextText());
829                    if (hostAddress != null) {
830                        hostAuth.mAddress = hostAddress;
831                        userLog("Autodiscover, server: " + hostAddress);
832                    }
833                }
834            }
835        }
836    }
837
838    void parseSettings(XmlPullParser parser, HostAuth hostAuth)
839            throws XmlPullParserException, IOException {
840        while (true) {
841            int type = parser.next();
842            if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
843                break;
844            } else if (type == XmlPullParser.START_TAG) {
845                String name = parser.getName();
846                if (name.equals("Server")) {
847                    parseServer(parser, hostAuth);
848                }
849            }
850        }
851    }
852
853    void parseAction(XmlPullParser parser, HostAuth hostAuth)
854            throws XmlPullParserException, IOException {
855        while (true) {
856            int type = parser.next();
857            if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
858                break;
859            } else if (type == XmlPullParser.START_TAG) {
860                String name = parser.getName();
861                if (name.equals("Error")) {
862                    // Should parse the error
863                } else if (name.equals("Redirect")) {
864                    Log.d(TAG, "Redirect: " + parser.nextText());
865                } else if (name.equals("Settings")) {
866                    parseSettings(parser, hostAuth);
867                }
868            }
869        }
870    }
871
872    void parseUser(XmlPullParser parser, HostAuth hostAuth)
873            throws XmlPullParserException, IOException {
874        while (true) {
875            int type = parser.next();
876            if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
877                break;
878            } else if (type == XmlPullParser.START_TAG) {
879                String name = parser.getName();
880                if (name.equals("EMailAddress")) {
881                    String addr = parser.nextText();
882                    userLog("Autodiscover, email: " + addr);
883                } else if (name.equals("DisplayName")) {
884                    String dn = parser.nextText();
885                    userLog("Autodiscover, user: " + dn);
886                }
887            }
888        }
889    }
890
891    void parseResponse(XmlPullParser parser, HostAuth hostAuth)
892            throws XmlPullParserException, IOException {
893        while (true) {
894            int type = parser.next();
895            if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
896                break;
897            } else if (type == XmlPullParser.START_TAG) {
898                String name = parser.getName();
899                if (name.equals("User")) {
900                    parseUser(parser, hostAuth);
901                } else if (name.equals("Action")) {
902                    parseAction(parser, hostAuth);
903                }
904            }
905        }
906    }
907
908    void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
909            throws XmlPullParserException, IOException {
910        while (true) {
911            int type = parser.nextTag();
912            if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
913                break;
914            } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
915                parseResponse(parser, hostAuth);
916            }
917        }
918    }
919
920    /**
921     * Contact the GAL and obtain a list of matching accounts
922     * @param context caller's context
923     * @param accountId the account Id to search
924     * @param filter the characters entered so far
925     * @return a result record or null for no data
926     *
927     * TODO: shorter timeout for interactive lookup
928     * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
929     * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
930     */
931    static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
932        Account acct = Account.restoreAccountWithId(context, accountId);
933        if (acct != null) {
934            EasSyncService svc = setupServiceForAccount(context, acct);
935            if (svc == null) return null;
936            try {
937                Serializer s = new Serializer();
938                s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
939                s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
940                s.start(Tags.SEARCH_OPTIONS);
941                s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
942                s.end().end().end().done();
943                EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
944                try {
945                    int code = resp.getStatus();
946                    if (code == HttpStatus.SC_OK) {
947                        InputStream is = resp.getInputStream();
948                        try {
949                            GalParser gp = new GalParser(is, svc);
950                            if (gp.parse()) {
951                                return gp.getGalResult();
952                            }
953                        } finally {
954                            is.close();
955                        }
956                    } else {
957                        svc.userLog("GAL lookup returned " + code);
958                    }
959                } finally {
960                    resp.close();
961                }
962            } catch (IOException e) {
963                // GAL is non-critical; we'll just go on
964                svc.userLog("GAL lookup exception " + e);
965            }
966        }
967        return null;
968    }
969    /**
970     * Send an email responding to a Message that has been marked as a meeting request.  The message
971     * will consist a little bit of event information and an iCalendar attachment
972     * @param msg the meeting request email
973     */
974    private void sendMeetingResponseMail(Message msg, int response) {
975        // Get the meeting information; we'd better have some...
976        if (msg.mMeetingInfo == null) return;
977        PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
978
979        // This will come as "First Last" <box@server.blah>, so we use Address to
980        // parse it into parts; we only need the email address part for the ics file
981        Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
982        // It shouldn't be possible, but handle it anyway
983        if (addrs.length != 1) return;
984        String organizerEmail = addrs[0].getAddress();
985
986        String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
987        String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
988        String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
989
990        // What we're doing here is to create an Entity that looks like an Event as it would be
991        // stored by CalendarProvider
992        ContentValues entityValues = new ContentValues();
993        Entity entity = new Entity(entityValues);
994
995        // Fill in times, location, title, and organizer
996        entityValues.put("DTSTAMP",
997                CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
998        entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
999        entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
1000        entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
1001        entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
1002        entityValues.put(Events.ORGANIZER, organizerEmail);
1003
1004        // Add ourselves as an attendee, using our account email address
1005        ContentValues attendeeValues = new ContentValues();
1006        attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1007                Attendees.RELATIONSHIP_ATTENDEE);
1008        attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
1009        entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
1010
1011        // Add the organizer
1012        ContentValues organizerValues = new ContentValues();
1013        organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1014                Attendees.RELATIONSHIP_ORGANIZER);
1015        organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
1016        entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
1017
1018        // Create a message from the Entity we've built.  The message will have fields like
1019        // to, subject, date, and text filled in.  There will also be an "inline" attachment
1020        // which is in iCalendar format
1021        int flag;
1022        switch(response) {
1023            case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
1024                flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
1025                break;
1026            case EmailServiceConstants.MEETING_REQUEST_DECLINED:
1027                flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
1028                break;
1029            case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
1030            default:
1031                flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
1032                break;
1033        }
1034        Message outgoingMsg =
1035            CalendarUtilities.createMessageForEntity(mContext, entity, flag,
1036                    meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
1037        // Assuming we got a message back (we might not if the event has been deleted), send it
1038        if (outgoingMsg != null) {
1039            EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
1040        }
1041    }
1042
1043    /**
1044     * Responds to a move request.  The MessageMoveRequest is basically our
1045     * wrapper for the MoveItems service call
1046     * @param req the request (message id and "to" mailbox id)
1047     * @throws IOException
1048     */
1049    protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
1050        // Retrieve the message and mailbox; punt if either are null
1051        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
1052        if (msg == null) return;
1053        Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
1054                msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
1055        if (c == null) throw new ProviderUnavailableException();
1056        Mailbox srcMailbox = null;
1057        try {
1058            if (!c.moveToNext()) return;
1059            srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
1060        } finally {
1061            c.close();
1062        }
1063        if (srcMailbox == null) return;
1064        Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
1065        if (dstMailbox == null) return;
1066        Serializer s = new Serializer();
1067        s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
1068        s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
1069        s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
1070        s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
1071        s.end().end().done();
1072        EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
1073        try {
1074            int status = resp.getStatus();
1075            if (status == HttpStatus.SC_OK) {
1076                if (!resp.isEmpty()) {
1077                    InputStream is = resp.getInputStream();
1078                    MoveItemsParser p = new MoveItemsParser(is, this);
1079                    p.parse();
1080                    int statusCode = p.getStatusCode();
1081                    ContentValues cv = new ContentValues();
1082                    if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
1083                        // Restore the old mailbox id
1084                        cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
1085                        mContentResolver.update(
1086                                ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
1087                                cv, null, null);
1088                    } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
1089                        // Update with the new server id
1090                        cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
1091                        cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
1092                        mContentResolver.update(
1093                                ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
1094                                cv, null, null);
1095                    }
1096                    if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
1097                            || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
1098                        // If we revert or succeed, we no longer need the update information
1099                        // OR the now-duplicate email (the new copy will be synced down)
1100                        mContentResolver.delete(ContentUris.withAppendedId(
1101                                Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
1102                    } else {
1103                        // In this case, we're retrying, so do nothing.  The request will be
1104                        // handled next sync
1105                    }
1106                }
1107            } else if (EasResponse.isAuthError(status)) {
1108                throw new EasAuthenticationException();
1109            } else {
1110                userLog("Move items request failed, code: " + status);
1111                throw new IOException();
1112            }
1113        } finally {
1114            resp.close();
1115        }
1116    }
1117
1118    /**
1119     * Responds to a meeting request.  The MeetingResponseRequest is basically our
1120     * wrapper for the meetingResponse service call
1121     * @param req the request (message id and response code)
1122     * @throws IOException
1123     */
1124    protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
1125        // Retrieve the message and mailbox; punt if either are null
1126        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
1127        if (msg == null) return;
1128        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
1129        if (mailbox == null) return;
1130        Serializer s = new Serializer();
1131        s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
1132        s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
1133        s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId);
1134        s.data(Tags.MREQ_REQ_ID, msg.mServerId);
1135        s.end().end().done();
1136        EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
1137        try {
1138            int status = resp.getStatus();
1139            if (status == HttpStatus.SC_OK) {
1140                if (!resp.isEmpty()) {
1141                    InputStream is = resp.getInputStream();
1142                    new MeetingResponseParser(is, this).parse();
1143                    String meetingInfo = msg.mMeetingInfo;
1144                    if (meetingInfo != null) {
1145                        String responseRequested = new PackedString(meetingInfo).get(
1146                                MeetingInfo.MEETING_RESPONSE_REQUESTED);
1147                        // If there's no tag, or a non-zero tag, we send the response mail
1148                        if ("0".equals(responseRequested)) {
1149                            return;
1150                        }
1151                    }
1152                    sendMeetingResponseMail(msg, req.mResponse);
1153                }
1154            } else if (EasResponse.isAuthError(status)) {
1155                throw new EasAuthenticationException();
1156            } else {
1157                userLog("Meeting response request failed, code: " + status);
1158                throw new IOException();
1159            }
1160        } finally {
1161            resp.close();
1162       }
1163    }
1164
1165    /**
1166     * Using mUserName and mPassword, lazily create the strings that are commonly used in our HTTP
1167     * POSTs, including the authentication header string, the base URI we use to communicate with
1168     * EAS, and the user information string (user, deviceId, and deviceType)
1169     */
1170    private void cacheAuthUserAndBaseUriStrings() {
1171        if (mAuthString == null || mUserString == null || mBaseUriString == null) {
1172            String safeUserName = Uri.encode(mUserName);
1173            String cs = mUserName + ':' + mPassword;
1174            mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
1175            mUserString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
1176                "&DeviceType=" + DEVICE_TYPE;
1177            String scheme =
1178                EmailClientConnectionManager.makeScheme(mSsl, mTrustSsl, mClientCertAlias);
1179            mBaseUriString = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync";
1180        }
1181    }
1182
1183    @VisibleForTesting
1184    String makeUriString(String cmd, String extra) {
1185        cacheAuthUserAndBaseUriStrings();
1186        String uriString = mBaseUriString;
1187        if (cmd != null) {
1188            uriString += "?Cmd=" + cmd + mUserString;
1189        }
1190        if (extra != null) {
1191            uriString += extra;
1192        }
1193        return uriString;
1194    }
1195
1196    /**
1197     * Set standard HTTP headers, using a policy key if required
1198     * @param method the method we are going to send
1199     * @param usePolicyKey whether or not a policy key should be sent in the headers
1200     */
1201    /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
1202        method.setHeader("Authorization", mAuthString);
1203        method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
1204        method.setHeader("User-Agent", USER_AGENT);
1205        method.setHeader("Accept-Encoding", "gzip");
1206        if (usePolicyKey) {
1207            // If there's an account in existence, use its key; otherwise (we're creating the
1208            // account), send "0".  The server will respond with code 449 if there are policies
1209            // to be enforced
1210            String key = "0";
1211            if (mAccount != null) {
1212                String accountKey = mAccount.mSecuritySyncKey;
1213                if (!TextUtils.isEmpty(accountKey)) {
1214                    key = accountKey;
1215                }
1216            }
1217            method.setHeader("X-MS-PolicyKey", key);
1218        }
1219    }
1220
1221    protected void setConnectionParameters(HostAuth hostAuth) throws CertificateException {
1222        mSsl = hostAuth.shouldUseSsl();
1223        mTrustSsl = hostAuth.shouldTrustAllServerCerts();
1224        mClientCertAlias = hostAuth.mClientCertAlias;
1225        mPort = hostAuth.mPort;
1226
1227        // Register the new alias, if needed.
1228        if (mClientCertAlias != null) {
1229            // Ensure that the connection manager knows to use the proper client certificate
1230            // when establishing connections for this service.
1231            EmailClientConnectionManager connManager = getClientConnectionManager();
1232            connManager.registerClientCert(mContext, hostAuth);
1233        }
1234    }
1235
1236    private EmailClientConnectionManager getClientConnectionManager() {
1237        return ExchangeService.getClientConnectionManager(mSsl, mPort);
1238    }
1239
1240    private HttpClient getHttpClient(int timeout) {
1241        HttpParams params = new BasicHttpParams();
1242        HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
1243        HttpConnectionParams.setSoTimeout(params, timeout);
1244        HttpConnectionParams.setSocketBufferSize(params, 8192);
1245        HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
1246        return client;
1247    }
1248
1249    public EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
1250        return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
1251    }
1252
1253    protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
1254        return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
1255    }
1256
1257    protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
1258       Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
1259       return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
1260    }
1261
1262    /**
1263     * Convenience method for executePostWithTimeout for use other than with the Ping command
1264     */
1265    protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
1266            throws IOException {
1267        return executePostWithTimeout(client, method, timeout, false);
1268    }
1269
1270    /**
1271     * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
1272     * @param client the HttpClient
1273     * @param method the HttpPost
1274     * @param timeout the timeout before failure, in ms
1275     * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
1276     * @return the HttpResponse
1277     * @throws IOException
1278     */
1279    protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout,
1280            boolean isPingCommand) throws IOException {
1281        synchronized(getSynchronizer()) {
1282            mPendingPost = method;
1283            long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
1284            if (isPingCommand) {
1285                ExchangeService.runAsleep(mMailboxId, alarmTime);
1286            } else {
1287                ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
1288            }
1289        }
1290        try {
1291            return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
1292        } finally {
1293            synchronized(getSynchronizer()) {
1294                if (isPingCommand) {
1295                    ExchangeService.runAwake(mMailboxId);
1296                } else {
1297                    ExchangeService.clearWatchdogAlarm(mMailboxId);
1298                }
1299                mPendingPost = null;
1300            }
1301        }
1302    }
1303
1304    public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
1305            throws IOException {
1306        HttpClient client = getHttpClient(timeout);
1307        boolean isPingCommand = cmd.equals(PING_COMMAND);
1308
1309        // Split the mail sending commands
1310        String extra = null;
1311        boolean msg = false;
1312        if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
1313            int cmdLength = cmd.indexOf('&');
1314            extra = cmd.substring(cmdLength);
1315            cmd = cmd.substring(0, cmdLength);
1316            msg = true;
1317        } else if (cmd.startsWith("SendMail&")) {
1318            msg = true;
1319        }
1320
1321        String us = makeUriString(cmd, extra);
1322        HttpPost method = new HttpPost(URI.create(us));
1323        // Send the proper Content-Type header; it's always wbxml except for messages when
1324        // the EAS protocol version is < 14.0
1325        // If entity is null (e.g. for attachments), don't set this header
1326        if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
1327            method.setHeader("Content-Type", "message/rfc822");
1328        } else if (entity != null) {
1329            method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
1330        }
1331        setHeaders(method, !isPingCommand);
1332        // NOTE
1333        // The next lines are added at the insistence of $VENDOR, who is seeing inappropriate
1334        // network activity related to the Ping command on some networks with some servers.
1335        // This code should be removed when the underlying issue is resolved
1336        if (isPingCommand) {
1337            method.setHeader("Connection", "close");
1338        }
1339        method.setEntity(entity);
1340        return executePostWithTimeout(client, method, timeout, isPingCommand);
1341    }
1342
1343    protected EasResponse sendHttpClientOptions() throws IOException {
1344        cacheAuthUserAndBaseUriStrings();
1345        // For OPTIONS, just use the base string and the single header
1346        String uriString = mBaseUriString;
1347        HttpOptions method = new HttpOptions(URI.create(uriString));
1348        method.setHeader("Authorization", mAuthString);
1349        method.setHeader("User-Agent", USER_AGENT);
1350        HttpClient client = getHttpClient(COMMAND_TIMEOUT);
1351        return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
1352    }
1353
1354    String getTargetCollectionClassFromCursor(Cursor c) {
1355        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
1356        if (type == Mailbox.TYPE_CONTACTS) {
1357            return "Contacts";
1358        } else if (type == Mailbox.TYPE_CALENDAR) {
1359            return "Calendar";
1360        } else {
1361            return "Email";
1362        }
1363    }
1364
1365    /**
1366     * Negotiate provisioning with the server.  First, get policies form the server and see if
1367     * the policies are supported by the device.  Then, write the policies to the account and
1368     * tell SecurityPolicy that we have policies in effect.  Finally, see if those policies are
1369     * active; if so, acknowledge the policies to the server and get a final policy key that we
1370     * use in future EAS commands and write this key to the account.
1371     * @return whether or not provisioning has been successful
1372     * @throws IOException
1373     */
1374    public static boolean tryProvision(EasSyncService svc) throws IOException {
1375        // First, see if provisioning is even possible, i.e. do we support the policies required
1376        // by the server
1377        ProvisionParser pp = canProvision(svc);
1378        if (pp == null) return false;
1379        Context context = svc.mContext;
1380        Account account = svc.mAccount;
1381        // Get the policies from ProvisionParser
1382        Policy policy = pp.getPolicy();
1383        Policy oldPolicy = null;
1384        // Grab the old policy (if any)
1385        if (svc.mAccount.mPolicyKey > 0) {
1386            oldPolicy = Policy.restorePolicyWithId(context, account.mPolicyKey);
1387        }
1388        // Update the account with a null policyKey (the key we've gotten is
1389        // temporary and cannot be used for syncing)
1390        PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, null);
1391        // Make sure mAccount is current (with latest policy key)
1392        account.refresh(context);
1393        if (pp.getRemoteWipe()) {
1394            // We've gotten a remote wipe command
1395            ExchangeService.alwaysLog("!!! Remote wipe request received");
1396            // Start by setting the account to security hold
1397            PolicyServiceProxy.setAccountHoldFlag(context, account, true);
1398            // Force a stop to any running syncs for this account (except this one)
1399            ExchangeService.stopNonAccountMailboxSyncsForAccount(account.mId);
1400
1401            // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
1402            // we wipe the device regardless of any errors in acknowledgment
1403            try {
1404                ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
1405                acknowledgeRemoteWipe(svc, pp.getSecuritySyncKey());
1406            } catch (Exception e) {
1407                // Because remote wipe is such a high priority task, we don't want to
1408                // circumvent it if there's an exception in acknowledgment
1409            }
1410            // Then, tell SecurityPolicy to wipe the device
1411            ExchangeService.alwaysLog("!!! Executing remote wipe");
1412            PolicyServiceProxy.remoteWipe(context);
1413            return false;
1414        } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(context, policy)) {
1415            // See if the required policies are in force; if they are, acknowledge the policies
1416            // to the server and get the final policy key
1417            // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
1418            String securitySyncKey;
1419            if (svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
1420                securitySyncKey = pp.getSecuritySyncKey();
1421            } else {
1422                securitySyncKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
1423                        PROVISION_STATUS_OK);
1424            }
1425            if (securitySyncKey != null) {
1426                // If attachment policies have changed, fix up any affected attachment records
1427                if (oldPolicy != null) {
1428                    if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
1429                            (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
1430                        Policy.setAttachmentFlagsForNewPolicy(context, account, policy);
1431                    }
1432                }
1433                // Write the final policy key to the Account and say we've been successful
1434                PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, securitySyncKey);
1435                // Release any mailboxes that might be in a security hold
1436                ExchangeService.releaseSecurityHold(account);
1437                return true;
1438            }
1439        }
1440        return false;
1441    }
1442
1443    private static String getPolicyType(Double protocolVersion) {
1444        return (protocolVersion >=
1445            Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
1446    }
1447
1448    /**
1449     * Obtain a set of policies from the server and determine whether those policies are supported
1450     * by the device.
1451     * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
1452     * @throws IOException
1453     */
1454    public static ProvisionParser canProvision(EasSyncService svc) throws IOException {
1455        Serializer s = new Serializer();
1456        Double protocolVersion = svc.mProtocolVersionDouble;
1457        s.start(Tags.PROVISION_PROVISION);
1458        if (svc.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
1459            // Send settings information in 14.1 and greater
1460            s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
1461            s.data(Tags.SETTINGS_MODEL, Build.MODEL);
1462            //s.data(Tags.SETTINGS_IMEI, "");
1463            //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
1464            s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
1465            //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
1466            //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
1467            //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
1468            s.data(Tags.SETTINGS_USER_AGENT, EasSyncService.USER_AGENT);
1469            s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
1470        }
1471        s.start(Tags.PROVISION_POLICIES);
1472        s.start(Tags.PROVISION_POLICY);
1473        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(protocolVersion));
1474        s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION
1475        EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
1476        try {
1477            int code = resp.getStatus();
1478            if (code == HttpStatus.SC_OK) {
1479                InputStream is = resp.getInputStream();
1480                ProvisionParser pp = new ProvisionParser(is, svc);
1481                if (pp.parse()) {
1482                    // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
1483                    // policies.  If others are required, hasSupportablePolicySet will be false
1484                    if (pp.hasSupportablePolicySet() &&
1485                            svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
1486                        // In EAS 14.0, we need the final security key in order to use the settings
1487                        // command
1488                        String policyKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
1489                                PROVISION_STATUS_OK);
1490                        if (policyKey != null) {
1491                            pp.setSecuritySyncKey(policyKey);
1492                        }
1493                    } else if (!pp.hasSupportablePolicySet())  {
1494                        // Try to acknowledge using the "partial" status (i.e. we can partially
1495                        // accommodate the required policies).  The server will agree to this if the
1496                        // "allow non-provisionable devices" setting is enabled on the server
1497                        ExchangeService.log("PolicySet is NOT fully supportable");
1498                        if (acknowledgeProvision(svc, pp.getSecuritySyncKey(),
1499                                PROVISION_STATUS_PARTIAL) != null) {
1500                            // The server's ok with our inability to support policies, so we'll
1501                            // clear them
1502                            pp.clearUnsupportablePolicies();
1503                        }
1504                    }
1505                    return pp;
1506                }
1507            }
1508        } finally {
1509            resp.close();
1510        }
1511
1512        // On failures, simply return null
1513        return null;
1514    }
1515
1516    /**
1517     * Acknowledge that we support the policies provided by the server, and that these policies
1518     * are in force.
1519     * @param tempKey the initial (temporary) policy key sent by the server
1520     * @return the final policy key, which can be used for syncing
1521     * @throws IOException
1522     */
1523    private static void acknowledgeRemoteWipe(EasSyncService svc, String tempKey)
1524            throws IOException {
1525        acknowledgeProvisionImpl(svc, tempKey, PROVISION_STATUS_OK, true);
1526    }
1527
1528    private static String acknowledgeProvision(EasSyncService svc, String tempKey, String result)
1529            throws IOException {
1530        return acknowledgeProvisionImpl(svc, tempKey, result, false);
1531    }
1532
1533    private static String acknowledgeProvisionImpl(EasSyncService svc, String tempKey,
1534            String status, boolean remoteWipe) throws IOException {
1535        Serializer s = new Serializer();
1536        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
1537        s.start(Tags.PROVISION_POLICY);
1538
1539        // Use the proper policy type, depending on EAS version
1540        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(svc.mProtocolVersionDouble));
1541
1542        s.data(Tags.PROVISION_POLICY_KEY, tempKey);
1543        s.data(Tags.PROVISION_STATUS, status);
1544        s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
1545        if (remoteWipe) {
1546            s.start(Tags.PROVISION_REMOTE_WIPE);
1547            s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
1548            s.end();
1549        }
1550        s.end().done(); // PROVISION_PROVISION
1551        EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
1552        try {
1553            int code = resp.getStatus();
1554            if (code == HttpStatus.SC_OK) {
1555                InputStream is = resp.getInputStream();
1556                ProvisionParser pp = new ProvisionParser(is, svc);
1557                if (pp.parse()) {
1558                    // Return the final policy key from the ProvisionParser
1559                    String result = (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed";
1560                    ExchangeService.log("Provision " + result + " for " +
1561                            (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
1562                    return pp.getSecuritySyncKey();
1563                }
1564            }
1565        } finally {
1566            resp.close();
1567        }
1568        // On failures, log issue and return null
1569        ExchangeService.log("Provisioning failed for" +
1570                (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
1571        return null;
1572    }
1573
1574    private boolean sendSettings() throws IOException {
1575        Serializer s = new Serializer();
1576        s.start(Tags.SETTINGS_SETTINGS);
1577        s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
1578        s.data(Tags.SETTINGS_MODEL, Build.MODEL);
1579        s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
1580        s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
1581        s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
1582        EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
1583        try {
1584            int code = resp.getStatus();
1585            if (code == HttpStatus.SC_OK) {
1586                InputStream is = resp.getInputStream();
1587                SettingsParser sp = new SettingsParser(is, this);
1588                return sp.parse();
1589            }
1590        } finally {
1591            resp.close();
1592        }
1593        // On failures, simply return false
1594        return false;
1595    }
1596
1597    /**
1598     * Common code to sync E+PIM data
1599     * @param target an EasMailbox, EasContacts, or EasCalendar object
1600     */
1601    public void sync(AbstractSyncAdapter target) throws IOException {
1602        Mailbox mailbox = target.mMailbox;
1603
1604        boolean moreAvailable = true;
1605        int loopingCount = 0;
1606        while (!mStop && (moreAvailable || hasPendingRequests())) {
1607            // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
1608            // when connectivity has returned
1609            if (!hasConnectivity()) {
1610                userLog("No connectivity in sync; finishing sync");
1611                mExitStatus = EXIT_DONE;
1612                return;
1613            }
1614
1615            // Every time through the loop we check to see if we're still syncable
1616            if (!target.isSyncable()) {
1617                mExitStatus = EXIT_DONE;
1618                return;
1619            }
1620
1621            // Now, handle various requests
1622            while (true) {
1623                Request req = null;
1624
1625                if (mRequestQueue.isEmpty()) {
1626                    break;
1627                } else {
1628                    req = mRequestQueue.peek();
1629                }
1630
1631                // Our two request types are PartRequest (loading attachment) and
1632                // MeetingResponseRequest (respond to a meeting request)
1633                if (req instanceof PartRequest) {
1634                    TrafficStats.setThreadStatsTag(
1635                            TrafficFlags.getAttachmentFlags(mContext, mAccount));
1636                    new AttachmentLoader(this, (PartRequest)req).loadAttachment();
1637                    TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, mAccount));
1638                } else if (req instanceof MeetingResponseRequest) {
1639                    sendMeetingResponse((MeetingResponseRequest)req);
1640                } else if (req instanceof MessageMoveRequest) {
1641                    messageMoveRequest((MessageMoveRequest)req);
1642                }
1643
1644                // If there's an exception handling the request, we'll throw it
1645                // Otherwise, we remove the request
1646                mRequestQueue.remove();
1647            }
1648
1649            // Don't sync if we've got nothing to do
1650            if (!moreAvailable) {
1651                continue;
1652            }
1653
1654            Serializer s = new Serializer();
1655
1656            String className = target.getCollectionName();
1657            String syncKey = target.getSyncKey();
1658            userLog("sync, sending ", className, " syncKey: ", syncKey);
1659            s.start(Tags.SYNC_SYNC)
1660                .start(Tags.SYNC_COLLECTIONS)
1661                .start(Tags.SYNC_COLLECTION);
1662            // The "Class" element is removed in EAS 12.1 and later versions
1663            if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
1664                s.data(Tags.SYNC_CLASS, className);
1665            }
1666            s.data(Tags.SYNC_SYNC_KEY, syncKey)
1667                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
1668
1669            // Start with the default timeout
1670            int timeout = COMMAND_TIMEOUT;
1671            boolean initialSync = syncKey.equals("0");
1672            // EAS doesn't allow GetChanges in an initial sync; sending other options
1673            // appears to cause the server to delay its response in some cases, and this delay
1674            // can be long enough to result in an IOException and total failure to sync.
1675            // Therefore, we don't send any options with the initial sync.
1676            // Set the truncation amount, body preference, lookback, etc.
1677            target.sendSyncOptions(mProtocolVersionDouble, s, initialSync);
1678            if (initialSync) {
1679                // Use enormous timeout for initial sync, which empirically can take a while longer
1680                timeout = 120*SECONDS;
1681            }
1682            // Send our changes up to the server
1683            if (mUpsyncFailed) {
1684                if (Eas.USER_LOG) {
1685                    Log.d(TAG, "Inhibiting upsync this cycle");
1686                }
1687            } else {
1688                target.sendLocalChanges(s);
1689            }
1690
1691            s.end().end().end().done();
1692            EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
1693                    timeout);
1694            try {
1695                int code = resp.getStatus();
1696                if (code == HttpStatus.SC_OK) {
1697                    // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
1698                    // no changes in the mailbox; handle that case here
1699                    // There are two cases here; if we get back a compressed stream (GZIP), we won't
1700                    // know until we try to parse it (and generate an EmptyStreamException). If we
1701                    // get uncompressed data, the response will be empty (i.e. have zero length)
1702                    boolean emptyStream = false;
1703                    if (!resp.isEmpty()) {
1704                        InputStream is = resp.getInputStream();
1705                        try {
1706                            moreAvailable = target.parse(is);
1707                            // If we inhibited upsync, we need yet another sync
1708                            if (mUpsyncFailed) {
1709                                moreAvailable = true;
1710                            }
1711
1712                            if (target.isLooping()) {
1713                                loopingCount++;
1714                                userLog("** Looping: " + loopingCount);
1715                                // After the maximum number of loops, we'll set moreAvailable to
1716                                // false and allow the sync loop to terminate
1717                                if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
1718                                    userLog("** Looping force stopped");
1719                                    moreAvailable = false;
1720                                }
1721                            } else {
1722                                loopingCount = 0;
1723                            }
1724
1725                            // Cleanup clears out the updated/deleted tables, and we don't want to
1726                            // do that if our upsync failed; clear the flag otherwise
1727                            if (!mUpsyncFailed) {
1728                                target.cleanup();
1729                            } else {
1730                                mUpsyncFailed = false;
1731                            }
1732                        } catch (EmptyStreamException e) {
1733                            userLog("Empty stream detected in GZIP response");
1734                            emptyStream = true;
1735                        } catch (CommandStatusException e) {
1736                            // TODO 14.1
1737                            int status = e.mStatus;
1738                            if (CommandStatus.isNeedsProvisioning(status)) {
1739                                mExitStatus = EXIT_SECURITY_FAILURE;
1740                            } else if (CommandStatus.isDeniedAccess(status)) {
1741                                mExitStatus = EXIT_ACCESS_DENIED;
1742                            } else if (CommandStatus.isTransientError(status)) {
1743                                mExitStatus = EXIT_IO_ERROR;
1744                            } else {
1745                                mExitStatus = EXIT_EXCEPTION;
1746                            }
1747                            return;
1748                        }
1749                    } else {
1750                        emptyStream = true;
1751                    }
1752
1753                    if (emptyStream) {
1754                        // If this happens, exit cleanly, and change the interval from push to ping
1755                        // if necessary
1756                        userLog("Empty sync response; finishing");
1757                        if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
1758                            userLog("Changing mailbox from push to ping");
1759                            ContentValues cv = new ContentValues();
1760                            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
1761                            mContentResolver.update(
1762                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
1763                                    cv, null, null);
1764                        }
1765                        if (mRequestQueue.isEmpty()) {
1766                            mExitStatus = EXIT_DONE;
1767                            return;
1768                        } else {
1769                            continue;
1770                        }
1771                    }
1772                } else {
1773                    userLog("Sync response error: ", code);
1774                    if (EasResponse.isProvisionError(code)) {
1775                        mExitStatus = EXIT_SECURITY_FAILURE;
1776                    } else if (EasResponse.isAuthError(code)) {
1777                        mExitStatus = EXIT_LOGIN_FAILURE;
1778                    } else {
1779                        mExitStatus = EXIT_IO_ERROR;
1780                    }
1781                    return;
1782                }
1783            } finally {
1784                resp.close();
1785            }
1786        }
1787        mExitStatus = EXIT_DONE;
1788    }
1789
1790    protected boolean setupService() {
1791        synchronized(getSynchronizer()) {
1792            mThread = Thread.currentThread();
1793            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
1794            TAG = mThread.getName();
1795        }
1796        // Make sure account and mailbox are always the latest from the database
1797        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
1798        if (mAccount == null) return false;
1799        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
1800        if (mMailbox == null) return false;
1801        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
1802        if (ha == null) return false;
1803        mHostAddress = ha.mAddress;
1804        mUserName = ha.mLogin;
1805        mPassword = ha.mPassword;
1806
1807        try {
1808            setConnectionParameters(ha);
1809        } catch (CertificateException e) {
1810            userLog("Couldn't retrieve certificate for connection");
1811            try {
1812                ExchangeService.callback().syncMailboxStatus(mMailboxId,
1813                        EmailServiceStatus.CLIENT_CERTIFICATE_ERROR, 0);
1814            } catch (RemoteException e1) {
1815                // Don't care if this fails.
1816            }
1817            return false;
1818        }
1819
1820        // Set up our protocol version from the Account
1821        mProtocolVersion = mAccount.mProtocolVersion;
1822        // If it hasn't been set up, start with default version
1823        if (mProtocolVersion == null) {
1824            mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
1825        }
1826        mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
1827
1828        // Do checks to address historical policy sets.
1829        Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
1830        if ((policy != null) && policy.mRequireEncryptionExternal) {
1831            // External storage encryption is not supported at this time. In a previous release,
1832            // prior to the system supporting true removable storage on Honeycomb, we accepted
1833            // this since we emulated external storage on partitions that could be encrypted.
1834            // If that was set before, we must clear it out now that the system supports true
1835            // removable storage (which can't be encrypted).
1836            resetSecurityPolicies();
1837        }
1838        return true;
1839    }
1840
1841    /**
1842     * Clears out the security policies associated with the account, forcing a provision error
1843     * and a re-sync of the policy information for the account.
1844     */
1845    @SuppressWarnings("deprecation")
1846    void resetSecurityPolicies() {
1847        ContentValues cv = new ContentValues();
1848        cv.put(AccountColumns.SECURITY_FLAGS, 0);
1849        cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
1850        long accountId = mAccount.mId;
1851        mContentResolver.update(ContentUris.withAppendedId(
1852                Account.CONTENT_URI, accountId), cv, null, null);
1853    }
1854
1855    @Override
1856    public void run() {
1857        try {
1858            // Make sure account and mailbox are still valid
1859            if (!setupService()) return;
1860            // If we've been stopped, we're done
1861            if (mStop) return;
1862
1863            // Whether or not we're the account mailbox
1864            try {
1865                mDeviceId = ExchangeService.getDeviceId(mContext);
1866                int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
1867                if ((mMailbox == null) || (mAccount == null)) {
1868                    return;
1869                } else {
1870                    AbstractSyncAdapter target;
1871                    if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
1872                        TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CONTACTS);
1873                        target = new ContactsSyncAdapter( this);
1874                    } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
1875                        TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CALENDAR);
1876                        target = new CalendarSyncAdapter(this);
1877                    } else {
1878                        TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
1879                        target = new EmailSyncAdapter(this);
1880                    }
1881                    // We loop because someone might have put a request in while we were syncing
1882                    // and we've missed that opportunity...
1883                    do {
1884                        if (mRequestTime != 0) {
1885                            userLog("Looping for user request...");
1886                            mRequestTime = 0;
1887                        }
1888                        String syncKey = target.getSyncKey();
1889                        if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START ||
1890                                "0".equals(syncKey)) {
1891                            try {
1892                                ExchangeService.callback().syncMailboxStatus(mMailboxId,
1893                                        EmailServiceStatus.IN_PROGRESS, 0);
1894                            } catch (RemoteException e1) {
1895                                // Don't care if this fails
1896                            }
1897                        }
1898                        sync(target);
1899                    } while (mRequestTime != 0);
1900                }
1901            } catch (EasAuthenticationException e) {
1902                userLog("Caught authentication error");
1903                mExitStatus = EXIT_LOGIN_FAILURE;
1904            } catch (IOException e) {
1905                String message = e.getMessage();
1906                userLog("Caught IOException: ", (message == null) ? "No message" : message);
1907                mExitStatus = EXIT_IO_ERROR;
1908            } catch (Exception e) {
1909                userLog("Uncaught exception in EasSyncService", e);
1910            } finally {
1911                int status;
1912                ExchangeService.done(this);
1913                if (!mStop) {
1914                    userLog("Sync finished");
1915                    switch (mExitStatus) {
1916                        case EXIT_IO_ERROR:
1917                            status = EmailServiceStatus.CONNECTION_ERROR;
1918                            break;
1919                        case EXIT_DONE:
1920                            status = EmailServiceStatus.SUCCESS;
1921                            ContentValues cv = new ContentValues();
1922                            cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1923                            String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
1924                            cv.put(Mailbox.SYNC_STATUS, s);
1925                            mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
1926                                    mMailboxId), cv, null, null);
1927                            break;
1928                        case EXIT_LOGIN_FAILURE:
1929                            status = EmailServiceStatus.LOGIN_FAILED;
1930                            break;
1931                        case EXIT_SECURITY_FAILURE:
1932                            status = EmailServiceStatus.SECURITY_FAILURE;
1933                            // Ask for a new folder list. This should wake up the account mailbox; a
1934                            // security error in account mailbox should start provisioning
1935                            ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
1936                            break;
1937                        case EXIT_ACCESS_DENIED:
1938                            status = EmailServiceStatus.ACCESS_DENIED;
1939                            break;
1940                        default:
1941                            status = EmailServiceStatus.REMOTE_EXCEPTION;
1942                            errorLog("Sync ended due to an exception.");
1943                            break;
1944                    }
1945                } else {
1946                    userLog("Stopped sync finished.");
1947                    status = EmailServiceStatus.SUCCESS;
1948                }
1949
1950                // Send a callback (doesn't matter how the sync was started)
1951                try {
1952                    // Unless the user specifically asked for a sync, we don't want to report
1953                    // connection issues, as they are likely to be transient.  In this case, we
1954                    // simply report success, so that the progress indicator terminates without
1955                    // putting up an error banner
1956                    if (mSyncReason != ExchangeService.SYNC_UI_REQUEST &&
1957                            status == EmailServiceStatus.CONNECTION_ERROR) {
1958                        status = EmailServiceStatus.SUCCESS;
1959                    }
1960                    ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0);
1961                } catch (RemoteException e1) {
1962                    // Don't care if this fails
1963                }
1964
1965                // Make sure ExchangeService knows about this
1966                ExchangeService.kick("sync finished");
1967            }
1968        } catch (ProviderUnavailableException e) {
1969            Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
1970        }
1971    }
1972}
1973