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