EasSyncService.java revision 9cbd5664b81e31772377aa91cbbad29301e547ee
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 com.android.email.SecurityPolicy;
21import com.android.email.SecurityPolicy.PolicySet;
22import com.android.email.mail.AuthenticationFailedException;
23import com.android.email.mail.MessagingException;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.AccountColumns;
26import com.android.email.provider.EmailContent.Attachment;
27import com.android.email.provider.EmailContent.AttachmentColumns;
28import com.android.email.provider.EmailContent.HostAuth;
29import com.android.email.provider.EmailContent.Mailbox;
30import com.android.email.provider.EmailContent.MailboxColumns;
31import com.android.email.provider.EmailContent.Message;
32import com.android.email.service.EmailServiceProxy;
33import com.android.email.service.EmailServiceStatus;
34import com.android.exchange.adapter.AbstractSyncAdapter;
35import com.android.exchange.adapter.AccountSyncAdapter;
36import com.android.exchange.adapter.CalendarSyncAdapter;
37import com.android.exchange.adapter.ContactsSyncAdapter;
38import com.android.exchange.adapter.EmailSyncAdapter;
39import com.android.exchange.adapter.FolderSyncParser;
40import com.android.exchange.adapter.MeetingResponseParser;
41import com.android.exchange.adapter.Parser.EasParserException;
42import com.android.exchange.adapter.PingParser;
43import com.android.exchange.adapter.ProvisionParser;
44import com.android.exchange.adapter.Serializer;
45import com.android.exchange.adapter.Tags;
46
47import org.apache.http.Header;
48import org.apache.http.HttpEntity;
49import org.apache.http.HttpResponse;
50import org.apache.http.HttpStatus;
51import org.apache.http.client.ClientProtocolException;
52import org.apache.http.client.HttpClient;
53import org.apache.http.client.methods.HttpOptions;
54import org.apache.http.client.methods.HttpPost;
55import org.apache.http.client.methods.HttpRequestBase;
56import org.apache.http.conn.ClientConnectionManager;
57import org.apache.http.entity.ByteArrayEntity;
58import org.apache.http.entity.StringEntity;
59import org.apache.http.impl.client.DefaultHttpClient;
60import org.apache.http.params.BasicHttpParams;
61import org.apache.http.params.HttpConnectionParams;
62import org.apache.http.params.HttpParams;
63import org.xmlpull.v1.XmlPullParser;
64import org.xmlpull.v1.XmlPullParserException;
65import org.xmlpull.v1.XmlPullParserFactory;
66import org.xmlpull.v1.XmlSerializer;
67
68import android.content.ContentResolver;
69import android.content.ContentUris;
70import android.content.ContentValues;
71import android.content.Context;
72import android.database.Cursor;
73import android.os.Bundle;
74import android.os.RemoteException;
75import android.os.SystemClock;
76import android.util.Log;
77import android.util.Xml;
78import android.util.base64.Base64;
79
80import java.io.ByteArrayOutputStream;
81import java.io.File;
82import java.io.FileOutputStream;
83import java.io.IOException;
84import java.io.InputStream;
85import java.net.URI;
86import java.net.URLEncoder;
87import java.security.cert.CertificateException;
88import java.util.ArrayList;
89import java.util.HashMap;
90
91public class EasSyncService extends AbstractSyncService {
92    private static final String EMAIL_WINDOW_SIZE = "5";
93    public static final String PIM_WINDOW_SIZE = "5";
94    private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
95        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
96    private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
97        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
98        '=' + Mailbox.CHECK_INTERVAL_PING;
99    private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
100        MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
101        ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
102        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
103    private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
104        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
105        '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
106    static private final int CHUNK_SIZE = 16*1024;
107
108    static private final String PING_COMMAND = "Ping";
109    static private final int COMMAND_TIMEOUT = 20*SECONDS;
110
111    // Define our default protocol version as 2.5 (Exchange 2003)
112    static private final String DEFAULT_PROTOCOL_VERSION = "2.5";
113
114    static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
115        "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
116    static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
117    static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
118
119    static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
120    static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
121
122    /**
123     * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time.  There's
124     * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
125     * the ping exception out.  The maximum I use is 17 minutes, which is really an empirical
126     * choice; too long and we risk silent connection loss and loss of push for that period.  Too
127     * short and we lose efficiency/battery life.
128     *
129     * If we ever have to drop the ping timeout, we'll never increase it again.  There's no point
130     * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
131     * which will cause the sync service to be restarted at the starting heartbeat and going through
132     * the process again.
133     */
134    static private final int PING_MINUTES = 60; // in seconds
135    static private final int PING_FUDGE_LOW = 10;
136    static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
137    static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW;
138    static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW;
139    static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
140    static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES;
141
142    static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
143
144    // Fallbacks (in minutes) for ping loop failures
145    static private final int MAX_PING_FAILURES = 1;
146    static private final int PING_FALLBACK_INBOX = 5;
147    static private final int PING_FALLBACK_PIM = 25;
148
149    // MSFT's custom HTTP result code indicating the need to provision
150    static private final int HTTP_NEED_PROVISIONING = 449;
151
152    // Reasonable default
153    public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION;
154    public Double mProtocolVersionDouble;
155    protected String mDeviceId = null;
156    private String mDeviceType = "Android";
157    private String mAuthString = null;
158    private String mCmdString = null;
159    public String mHostAddress;
160    public String mUserName;
161    public String mPassword;
162    private boolean mSsl = true;
163    private boolean mTrustSsl = false;
164    public ContentResolver mContentResolver;
165    private String[] mBindArguments = new String[2];
166    private ArrayList<String> mPingChangeList;
167    private HttpPost mPendingPost = null;
168    // The ping time (in seconds)
169    private int mPingHeartbeat = PING_STARTING_HEARTBEAT;
170    // The longest successful ping heartbeat
171    private int mPingHighWaterMark = 0;
172    // Whether we've ever lowered the heartbeat
173    private boolean mPingHeartbeatDropped = false;
174    // Whether a POST was aborted due to watchdog timeout
175    private boolean mPostAborted = false;
176    // Whether or not the sync service is valid (usable)
177    public boolean mIsValid = true;
178
179    public EasSyncService(Context _context, Mailbox _mailbox) {
180        super(_context, _mailbox);
181        mContentResolver = _context.getContentResolver();
182        if (mAccount == null) {
183            mIsValid = false;
184            return;
185        }
186        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
187        if (ha == null) {
188            mIsValid = false;
189            return;
190        }
191        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
192        mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
193    }
194
195    private EasSyncService(String prefix) {
196        super(prefix);
197    }
198
199    public EasSyncService() {
200        this("EAS Validation");
201    }
202
203    @Override
204    public void ping() {
205        userLog("Alarm ping received!");
206        synchronized(getSynchronizer()) {
207            if (mPendingPost != null) {
208                userLog("Aborting pending POST!");
209                mPostAborted = true;
210                mPendingPost.abort();
211            }
212        }
213    }
214
215    @Override
216    public void stop() {
217        mStop = true;
218        synchronized(getSynchronizer()) {
219            if (mPendingPost != null) {
220                mPendingPost.abort();
221            }
222        }
223    }
224
225    /**
226     * Determine whether an HTTP code represents an authentication error
227     * @param code the HTTP code returned by the server
228     * @return whether or not the code represents an authentication error
229     */
230    protected boolean isAuthError(int code) {
231        return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
232    }
233
234    /**
235     * Determine whether an HTTP code represents a provisioning error
236     * @param code the HTTP code returned by the server
237     * @return whether or not the code represents an provisioning error
238     */
239    protected boolean isProvisionError(int code) {
240        return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN);
241    }
242
243    private void setupProtocolVersion(EasSyncService service, Header versionHeader) {
244        String versions = versionHeader.getValue();
245        if (versions != null) {
246            if (versions.contains("12.0")) {
247                service.mProtocolVersion = "12.0";
248            }
249            service.mProtocolVersionDouble = Double.parseDouble(service.mProtocolVersion);
250            if (service.mAccount != null) {
251                service.mAccount.mProtocolVersion = service.mProtocolVersion;
252            }
253        }
254    }
255
256    @Override
257    public void validateAccount(String hostAddress, String userName, String password, int port,
258            boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
259        try {
260            userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
261            EasSyncService svc = new EasSyncService("%TestAccount%");
262            svc.mContext = context;
263            svc.mHostAddress = hostAddress;
264            svc.mUserName = userName;
265            svc.mPassword = password;
266            svc.mSsl = ssl;
267            svc.mTrustSsl = trustCertificates;
268            // We mustn't use the "real" device id or we'll screw up current accounts
269            // Any string will do, but we'll go for "validate"
270            svc.mDeviceId = "validate";
271            HttpResponse resp = svc.sendHttpClientOptions();
272            int code = resp.getStatusLine().getStatusCode();
273            userLog("Validation (OPTIONS) response: " + code);
274            if (code == HttpStatus.SC_OK) {
275                // No exception means successful validation
276                Header commands = resp.getFirstHeader("MS-ASProtocolCommands");
277                Header versions = resp.getFirstHeader("ms-asprotocolversions");
278                if (commands == null || versions == null) {
279                    userLog("OPTIONS response without commands or versions; reporting I/O error");
280                    throw new MessagingException(MessagingException.IOERROR);
281                }
282
283                // Make sure we've got the right protocol version set up
284                setupProtocolVersion(svc, versions);
285
286                // Run second test here for provisioning failures...
287                Serializer s = new Serializer();
288                userLog("Try folder sync");
289                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0")
290                    .end().end().done();
291                resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
292                code = resp.getStatusLine().getStatusCode();
293                // We'll get one of the following responses if policies are required by the server
294                if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
295                    // Get the policies and see if we are able to support them
296                    if (svc.canProvision() != null) {
297                        // If so, send the advisory Exception (the account may be created later)
298                        throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
299                    } else
300                        // If not, send the unsupported Exception (the account won't be created)
301                        throw new MessagingException(
302                                MessagingException.SECURITY_POLICIES_UNSUPPORTED);
303                }
304                userLog("Validation successful");
305                return;
306            }
307            if (isAuthError(code)) {
308                userLog("Authentication failed");
309                throw new AuthenticationFailedException("Validation failed");
310            } else {
311                // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
312                userLog("Validation failed, reporting I/O error: ", code);
313                throw new MessagingException(MessagingException.IOERROR);
314            }
315        } catch (IOException e) {
316            Throwable cause = e.getCause();
317            if (cause != null && cause instanceof CertificateException) {
318                userLog("CertificateException caught: ", e.getMessage());
319                throw new MessagingException(MessagingException.GENERAL_SECURITY);
320            }
321            userLog("IOException caught: ", e.getMessage());
322            throw new MessagingException(MessagingException.IOERROR);
323        }
324
325    }
326
327    /**
328     * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
329     * it can be reused
330     *
331     * @param resp the HttpResponse that indicates a redirect (451)
332     * @param post the HttpPost that was originally sent to the server
333     * @return the HttpPost, updated with the redirect location
334     */
335    private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
336        Header locHeader = resp.getFirstHeader("X-MS-Location");
337        if (locHeader != null) {
338            String loc = locHeader.getValue();
339            // If we've gotten one and it shows signs of looking like an address, we try
340            // sending our request there
341            if (loc != null && loc.startsWith("http")) {
342                post.setURI(URI.create(loc));
343                return post;
344            }
345        }
346        return null;
347    }
348
349    /**
350     * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
351     * return the HttpResponse
352     *
353     * @param client the HttpClient to be used for the request
354     * @param post the HttpPost we're going to send
355     * @return an HttpResponse from the original or redirect server
356     * @throws IOException on any IOException within the HttpClient code
357     * @throws MessagingException
358     */
359    private HttpResponse postAutodiscover(HttpClient client, HttpPost post)
360            throws IOException, MessagingException {
361        userLog("Posting autodiscover to: " + post.getURI());
362        HttpResponse resp = client.execute(post);
363        int code = resp.getStatusLine().getStatusCode();
364        // On a redirect, try the new location
365        if (code == AUTO_DISCOVER_REDIRECT_CODE) {
366            post = getRedirect(resp, post);
367            if (post != null) {
368                userLog("Posting autodiscover to redirect: " + post.getURI());
369                return client.execute(post);
370            }
371        } else if (code == HttpStatus.SC_UNAUTHORIZED) {
372            // 401 (Unauthorized) is for true auth errors when used in Autodiscover
373            // 403 (and others) we'll just punt on
374            throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
375        } else if (code != HttpStatus.SC_OK) {
376            // We'll try the next address if this doesn't work
377            userLog("Code: " + code + ", throwing IOException");
378            throw new IOException();
379        }
380        return resp;
381    }
382
383    /**
384     * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
385     * only an email address and the password
386     *
387     * @param userName the user's email address
388     * @param password the user's password
389     * @return a HostAuth ready to be saved in an Account or null (failure)
390     */
391    public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
392        XmlSerializer s = Xml.newSerializer();
393        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
394        HostAuth hostAuth = new HostAuth();
395        Bundle bundle = new Bundle();
396        bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
397                MessagingException.NO_ERROR);
398        try {
399            // Build the XML document that's sent to the autodiscover server(s)
400            s.setOutput(os, "UTF-8");
401            s.startDocument("UTF-8", false);
402            s.startTag(null, "Autodiscover");
403            s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
404            s.startTag(null, "Request");
405            s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
406            s.startTag(null, "AcceptableResponseSchema");
407            s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
408            s.endTag(null, "AcceptableResponseSchema");
409            s.endTag(null, "Request");
410            s.endTag(null, "Autodiscover");
411            s.endDocument();
412            String req = os.toString();
413
414            // Initialize the user name and password
415            mUserName = userName;
416            mPassword = password;
417            // Make sure the authentication string is created (mAuthString)
418            makeUriString("foo", null);
419
420            // Split out the domain name
421            int amp = userName.indexOf('@');
422            // The UI ensures that userName is a valid email address
423            if (amp < 0) {
424                throw new RemoteException();
425            }
426            String domain = userName.substring(amp + 1);
427
428            // There are up to four attempts here; the two URLs that we're supposed to try per the
429            // specification, and up to one redirect for each (handled in postAutodiscover)
430
431            // Try the domain first and see if we can get a response
432            HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
433            setHeaders(post, false);
434            post.setHeader("Content-Type", "text/xml");
435            post.setEntity(new StringEntity(req));
436            HttpClient client = getHttpClient(COMMAND_TIMEOUT);
437            HttpResponse resp;
438            try {
439                resp = postAutodiscover(client, post);
440            } catch (ClientProtocolException e1) {
441                return null;
442            } catch (IOException e1) {
443                // We catch the IOException here because we have an alternate address to try
444                post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
445                // If we fail here, we're out of options, so we let the outer try catch the
446                // IOException and return null
447                resp = postAutodiscover(client, post);
448            }
449
450            // Get the "final" code; if it's not 200, just return null
451            int code = resp.getStatusLine().getStatusCode();
452            userLog("Code: " + code);
453            if (code != HttpStatus.SC_OK) return null;
454
455            // At this point, we have a 200 response (SC_OK)
456            HttpEntity e = resp.getEntity();
457            InputStream is = e.getContent();
458            try {
459                // The response to Autodiscover is regular XML (not WBXML)
460                // If we ever get an error in this process, we'll just punt and return null
461                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
462                XmlPullParser parser = factory.newPullParser();
463                parser.setInput(is, "UTF-8");
464                int type = parser.getEventType();
465                if (type == XmlPullParser.START_DOCUMENT) {
466                    type = parser.next();
467                    if (type == XmlPullParser.START_TAG) {
468                        String name = parser.getName();
469                        if (name.equals("Autodiscover")) {
470                            hostAuth = new HostAuth();
471                            parseAutodiscover(parser, hostAuth);
472                            // On success, we'll have a server address and login
473                            if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
474                                // Fill in the rest of the HostAuth
475                                hostAuth.mPassword = password;
476                                hostAuth.mPort = 443;
477                                hostAuth.mProtocol = "eas";
478                                hostAuth.mFlags =
479                                    HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
480                                bundle.putParcelable(
481                                        EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
482                            } else {
483                                bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
484                                        MessagingException.UNSPECIFIED_EXCEPTION);
485                            }
486                        }
487                    }
488                }
489            } catch (XmlPullParserException e1) {
490                // This would indicate an I/O error of some sort
491                // We will simply return null and user can configure manually
492            }
493        // There's no reason at all for exceptions to be thrown, and it's ok if so.
494        // We just won't do auto-discover; user can configure manually
495       } catch (IllegalArgumentException e) {
496             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
497                     MessagingException.UNSPECIFIED_EXCEPTION);
498       } catch (IllegalStateException e) {
499            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
500                    MessagingException.UNSPECIFIED_EXCEPTION);
501       } catch (IOException e) {
502            userLog("IOException in Autodiscover", e);
503            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
504                    MessagingException.IOERROR);
505        } catch (MessagingException e) {
506            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
507                    MessagingException.AUTHENTICATION_FAILED);
508        }
509        return bundle;
510    }
511
512    void parseServer(XmlPullParser parser, HostAuth hostAuth)
513            throws XmlPullParserException, IOException {
514        boolean mobileSync = false;
515        while (true) {
516            int type = parser.next();
517            if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
518                break;
519            } else if (type == XmlPullParser.START_TAG) {
520                String name = parser.getName();
521                if (name.equals("Type")) {
522                    if (parser.nextText().equals("MobileSync")) {
523                        mobileSync = true;
524                    }
525                } else if (mobileSync && name.equals("Url")) {
526                    String url = parser.nextText().toLowerCase();
527                    // This will look like https://<server address>/Microsoft-Server-ActiveSync
528                    // We need to extract the <server address>
529                    if (url.startsWith("https://") &&
530                            url.endsWith("/microsoft-server-activesync")) {
531                        int lastSlash = url.lastIndexOf('/');
532                        hostAuth.mAddress = url.substring(8, lastSlash);
533                        userLog("Autodiscover, server: " + hostAuth.mAddress);
534                    }
535                }
536            }
537        }
538    }
539
540    void parseSettings(XmlPullParser parser, HostAuth hostAuth)
541            throws XmlPullParserException, IOException {
542        while (true) {
543            int type = parser.next();
544            if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
545                break;
546            } else if (type == XmlPullParser.START_TAG) {
547                String name = parser.getName();
548                if (name.equals("Server")) {
549                    parseServer(parser, hostAuth);
550                }
551            }
552        }
553    }
554
555    void parseAction(XmlPullParser parser, HostAuth hostAuth)
556            throws XmlPullParserException, IOException {
557        while (true) {
558            int type = parser.next();
559            if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
560                break;
561            } else if (type == XmlPullParser.START_TAG) {
562                String name = parser.getName();
563                if (name.equals("Error")) {
564                    // Should parse the error
565                } else if (name.equals("Redirect")) {
566                    Log.d(TAG, "Redirect: " + parser.nextText());
567                } else if (name.equals("Settings")) {
568                    parseSettings(parser, hostAuth);
569                }
570            }
571        }
572    }
573
574    void parseUser(XmlPullParser parser, HostAuth hostAuth)
575            throws XmlPullParserException, IOException {
576        while (true) {
577            int type = parser.next();
578            if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
579                break;
580            } else if (type == XmlPullParser.START_TAG) {
581                String name = parser.getName();
582                if (name.equals("EMailAddress")) {
583                    String addr = parser.nextText();
584                    hostAuth.mLogin = addr;
585                    userLog("Autodiscover, login: " + addr);
586                } else if (name.equals("DisplayName")) {
587                    String dn = parser.nextText();
588                    userLog("Autodiscover, user: " + dn);
589                }
590            }
591        }
592    }
593
594    void parseResponse(XmlPullParser parser, HostAuth hostAuth)
595            throws XmlPullParserException, IOException {
596        while (true) {
597            int type = parser.next();
598            if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
599                break;
600            } else if (type == XmlPullParser.START_TAG) {
601                String name = parser.getName();
602                if (name.equals("User")) {
603                    parseUser(parser, hostAuth);
604                } else if (name.equals("Action")) {
605                    parseAction(parser, hostAuth);
606                }
607            }
608        }
609    }
610
611    void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
612            throws XmlPullParserException, IOException {
613        while (true) {
614            int type = parser.nextTag();
615            if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
616                break;
617            } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
618                parseResponse(parser, hostAuth);
619            }
620        }
621    }
622
623    private void doStatusCallback(long messageId, long attachmentId, int status) {
624        try {
625            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
626        } catch (RemoteException e) {
627            // No danger if the client is no longer around
628        }
629    }
630
631    private void doProgressCallback(long messageId, long attachmentId, int progress) {
632        try {
633            SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
634                    EmailServiceStatus.IN_PROGRESS, progress);
635        } catch (RemoteException e) {
636            // No danger if the client is no longer around
637        }
638    }
639
640    public File createUniqueFileInternal(String dir, String filename) {
641        File directory;
642        if (dir == null) {
643            directory = mContext.getFilesDir();
644        } else {
645            directory = new File(dir);
646        }
647        if (!directory.exists()) {
648            directory.mkdirs();
649        }
650        File file = new File(directory, filename);
651        if (!file.exists()) {
652            return file;
653        }
654        // Get the extension of the file, if any.
655        int index = filename.lastIndexOf('.');
656        String name = filename;
657        String extension = "";
658        if (index != -1) {
659            name = filename.substring(0, index);
660            extension = filename.substring(index);
661        }
662        for (int i = 2; i < Integer.MAX_VALUE; i++) {
663            file = new File(directory, name + '-' + i + extension);
664            if (!file.exists()) {
665                return file;
666            }
667        }
668        return null;
669    }
670
671    /**
672     * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
673     * wrapper for Attachment
674     * @param req the part (attachment) to be retrieved
675     * @throws IOException
676     */
677    protected void getAttachment(PartRequest req) throws IOException {
678        Attachment att = req.mAttachment;
679        Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
680        doProgressCallback(msg.mId, att.mId, 0);
681
682        String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
683        HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
684
685        int status = res.getStatusLine().getStatusCode();
686        if (status == HttpStatus.SC_OK) {
687            HttpEntity e = res.getEntity();
688            int len = (int)e.getContentLength();
689            InputStream is = res.getEntity().getContent();
690            File f = (req.mDestination != null)
691                    ? new File(req.mDestination)
692                    : createUniqueFileInternal(req.mDestination, att.mFileName);
693            if (f != null) {
694                // Ensure that the target directory exists
695                File destDir = f.getParentFile();
696                if (!destDir.exists()) {
697                    destDir.mkdirs();
698                }
699                FileOutputStream os = new FileOutputStream(f);
700                // len > 0 means that Content-Length was set in the headers
701                // len < 0 means "chunked" transfer-encoding
702                if (len != 0) {
703                    try {
704                        mPendingRequest = req;
705                        byte[] bytes = new byte[CHUNK_SIZE];
706                        int length = len;
707                        // Loop terminates 1) when EOF is reached or 2) if an IOException occurs
708                        // One of these is guaranteed to occur
709                        int totalRead = 0;
710                        userLog("Attachment content-length: ", len);
711                        while (true) {
712                            int read = is.read(bytes, 0, CHUNK_SIZE);
713
714                            // read < 0 means that EOF was reached
715                            if (read < 0) {
716                                userLog("Attachment load reached EOF, totalRead: ", totalRead);
717                                break;
718                            }
719
720                            // Keep track of how much we've read for progress callback
721                            totalRead += read;
722
723                            // Write these bytes out
724                            os.write(bytes, 0, read);
725
726                            // We can't report percentages if this is chunked; by definition, the
727                            // length of incoming data is unknown
728                            if (length > 0) {
729                                // Belt and suspenders check to prevent runaway reading
730                                if (totalRead > length) {
731                                    errorLog("totalRead is greater than attachment length?");
732                                    break;
733                                }
734                                int pct = (totalRead * 100) / length;
735                                doProgressCallback(msg.mId, att.mId, pct);
736                            }
737                       }
738                    } finally {
739                        mPendingRequest = null;
740                    }
741                }
742                os.flush();
743                os.close();
744
745                // EmailProvider will throw an exception if we try to update an unsaved attachment
746                if (att.isSaved()) {
747                    String contentUriString = (req.mContentUriString != null)
748                            ? req.mContentUriString
749                            : "file://" + f.getAbsolutePath();
750                    ContentValues cv = new ContentValues();
751                    cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
752                    att.update(mContext, cv);
753                    doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
754                }
755            }
756        } else {
757            doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
758        }
759    }
760
761    /**
762     * Responds to a meeting request.  The MeetingResponseRequest is basically our
763     * wrapper for the meetingResponse service call
764     * @param req the request (message id and response code)
765     * @throws IOException
766     */
767    protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
768        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
769        Serializer s = new Serializer();
770        s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
771        s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
772        s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey));
773        s.data(Tags.MREQ_REQ_ID, msg.mServerId);
774        s.end().end().done();
775        HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
776        int status = res.getStatusLine().getStatusCode();
777        if (status == HttpStatus.SC_OK) {
778            HttpEntity e = res.getEntity();
779            int len = (int)e.getContentLength();
780            InputStream is = res.getEntity().getContent();
781            if (len != 0) {
782                new MeetingResponseParser(is, this).parse();
783            }
784        } else if (isAuthError(status)) {
785            throw new EasAuthenticationException();
786        } else {
787            userLog("Meeting response request failed, code: " + status);
788            throw new IOException();
789        }
790    }
791
792    @SuppressWarnings("deprecation")
793    private String makeUriString(String cmd, String extra) throws IOException {
794         // Cache the authentication string and the command string
795        String safeUserName = URLEncoder.encode(mUserName);
796        if (mAuthString == null) {
797            String cs = mUserName + ':' + mPassword;
798            mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
799            mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
800                    + mDeviceType;
801        }
802        String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
803            "/Microsoft-Server-ActiveSync";
804        if (cmd != null) {
805            us += "?Cmd=" + cmd + mCmdString;
806        }
807        if (extra != null) {
808            us += extra;
809        }
810        return us;
811    }
812
813    /**
814     * Set standard HTTP headers, using a policy key if required
815     * @param method the method we are going to send
816     * @param usePolicyKey whether or not a policy key should be sent in the headers
817     */
818    private void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
819        method.setHeader("Authorization", mAuthString);
820        method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
821        method.setHeader("Connection", "keep-alive");
822        method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
823        if (usePolicyKey && (mAccount != null)) {
824            String key = mAccount.mSecuritySyncKey;
825            if (key == null || key.length() == 0) {
826                return;
827            }
828            method.setHeader("X-MS-PolicyKey", key);
829        }
830    }
831
832    private ClientConnectionManager getClientConnectionManager() {
833        return SyncManager.getClientConnectionManager();
834    }
835
836    private HttpClient getHttpClient(int timeout) {
837        HttpParams params = new BasicHttpParams();
838        HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS);
839        HttpConnectionParams.setSoTimeout(params, timeout);
840        HttpConnectionParams.setSocketBufferSize(params, 8192);
841        HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
842        return client;
843    }
844
845    protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
846        return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
847    }
848
849    protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
850        return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
851    }
852
853    protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
854       Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
855       if (Eas.USER_LOG) {
856           userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
857       }
858       return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
859    }
860
861    protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
862            throws IOException {
863        HttpClient client = getHttpClient(timeout);
864        boolean sleepAllowed = cmd.equals(PING_COMMAND);
865
866        // Split the mail sending commands
867        String extra = null;
868        boolean msg = false;
869        if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
870            int cmdLength = cmd.indexOf('&');
871            extra = cmd.substring(cmdLength);
872            cmd = cmd.substring(0, cmdLength);
873            msg = true;
874        } else if (cmd.startsWith("SendMail&")) {
875            msg = true;
876        }
877
878        String us = makeUriString(cmd, extra);
879        HttpPost method = new HttpPost(URI.create(us));
880        // Send the proper Content-Type header
881        // If entity is null (e.g. for attachments), don't set this header
882        if (msg) {
883            method.setHeader("Content-Type", "message/rfc822");
884        } else if (entity != null) {
885            method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
886        }
887        setHeaders(method, !cmd.equals(PING_COMMAND));
888        method.setEntity(entity);
889        synchronized(getSynchronizer()) {
890            mPendingPost = method;
891            if (sleepAllowed) {
892                SyncManager.runAsleep(mMailboxId, timeout+(10*SECONDS));
893            }
894        }
895        try {
896            return client.execute(method);
897        } finally {
898            synchronized(getSynchronizer()) {
899                if (sleepAllowed) {
900                    SyncManager.runAwake(mMailboxId);
901                }
902                mPendingPost = null;
903            }
904        }
905    }
906
907    protected HttpResponse sendHttpClientOptions() throws IOException {
908        HttpClient client = getHttpClient(COMMAND_TIMEOUT);
909        String us = makeUriString("OPTIONS", null);
910        HttpOptions method = new HttpOptions(URI.create(us));
911        setHeaders(method, false);
912        return client.execute(method);
913    }
914
915    String getTargetCollectionClassFromCursor(Cursor c) {
916        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
917        if (type == Mailbox.TYPE_CONTACTS) {
918            return "Contacts";
919        } else if (type == Mailbox.TYPE_CALENDAR) {
920            return "Calendar";
921        } else {
922            return "Email";
923        }
924    }
925
926    /**
927     * Negotiate provisioning with the server.  First, get policies form the server and see if
928     * the policies are supported by the device.  Then, write the policies to the account and
929     * tell SecurityPolicy that we have policies in effect.  Finally, see if those policies are
930     * active; if so, acknowledge the policies to the server and get a final policy key that we
931     * use in future EAS commands and write this key to the account.
932     * @return whether or not provisioning has been successful
933     * @throws IOException
934     */
935    private boolean tryProvision() throws IOException {
936        // First, see if provisioning is even possible, i.e. do we support the policies required
937        // by the server
938        ProvisionParser pp = canProvision();
939        if (pp != null) {
940            SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
941            // Get the policies from ProvisionParser
942            PolicySet ps = pp.getPolicySet();
943            // Update the account with a null policyKey (the key we've gotten is
944            // temporary and cannot be used for syncing)
945            if (ps.writeAccount(mAccount, null, true, mContext)) {
946                sp.updatePolicies(mAccount.mId);
947            }
948            if (pp.getRemoteWipe()) {
949                // We've gotten a remote wipe command
950                // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
951                // we wipe the device regardless of any errors in acknowledgment
952                try {
953                    acknowledgeRemoteWipe(pp.getPolicyKey());
954                } catch (Exception e) {
955                    // Because remote wipe is such a high priority task, we don't want to
956                    // circumvent it if there's an exception in acknowledgment
957                }
958                // Then, tell SecurityPolicy to wipe the device
959                sp.remoteWipe();
960                return false;
961            } else if (sp.isActive(ps)) {
962                // See if the required policies are in force; if they are, acknowledge the policies
963                // to the server and get the final policy key
964                String policyKey = acknowledgeProvision(pp.getPolicyKey());
965                if (policyKey != null) {
966                    // Write the final policy key to the Account and say we've been successful
967                    ps.writeAccount(mAccount, policyKey, true, mContext);
968                    return true;
969                }
970            } else {
971                // Notify that we are blocked because of policies
972                sp.policiesRequired(mAccount.mId);
973            }
974        }
975        return false;
976    }
977
978    private String getPolicyType() {
979        return (mProtocolVersionDouble >= 12.0) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
980    }
981
982    // TODO This is Exchange 2007 only at this point
983    /**
984     * Obtain a set of policies from the server and determine whether those policies are supported
985     * by the device.
986     * @return the ProvisionParser (holds policies and key) if we receive policies and they are
987     * supported by the device; null otherwise
988     * @throws IOException
989     */
990    private ProvisionParser canProvision() throws IOException {
991        Serializer s = new Serializer();
992        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
993        s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType())
994            .end().end().end().done();
995        HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
996        int code = resp.getStatusLine().getStatusCode();
997        if (code == HttpStatus.SC_OK) {
998            InputStream is = resp.getEntity().getContent();
999            ProvisionParser pp = new ProvisionParser(is, this);
1000            if (pp.parse()) {
1001                // If true, we received policies from the server; see if they are supported by
1002                // the framework; if so, return the ProvisionParser containing the policy set and
1003                // temporary key
1004                PolicySet ps = pp.getPolicySet();
1005                if (SecurityPolicy.getInstance(mContext).isSupported(ps)) {
1006                    return pp;
1007                }
1008            }
1009        }
1010        // On failures, simply return null
1011        return null;
1012    }
1013
1014    // TODO This is Exchange 2007 only at this point
1015    /**
1016     * Acknowledge that we support the policies provided by the server, and that these policies
1017     * are in force.
1018     * @param tempKey the initial (temporary) policy key sent by the server
1019     * @return the final policy key, which can be used for syncing
1020     * @throws IOException
1021     */
1022    private void acknowledgeRemoteWipe(String tempKey) throws IOException {
1023        acknowledgeProvisionImpl(tempKey, true);
1024    }
1025
1026    private String acknowledgeProvision(String tempKey) throws IOException {
1027        return acknowledgeProvisionImpl(tempKey, false);
1028    }
1029
1030    private String acknowledgeProvisionImpl(String tempKey, boolean remoteWipe) throws IOException {
1031        Serializer s = new Serializer();
1032        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
1033        s.start(Tags.PROVISION_POLICY);
1034
1035        // Use the proper policy type, depending on EAS version
1036        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
1037
1038        s.data(Tags.PROVISION_POLICY_KEY, tempKey);
1039        s.data(Tags.PROVISION_STATUS, "1");
1040        if (remoteWipe) {
1041            s.start(Tags.PROVISION_REMOTE_WIPE);
1042            s.data(Tags.PROVISION_STATUS, "1");
1043            s.end();
1044        }
1045        s.end(); // PROVISION_POLICY
1046        s.end().end().done(); // PROVISION_POLICIES, PROVISION_PROVISION
1047        HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
1048        int code = resp.getStatusLine().getStatusCode();
1049        if (code == HttpStatus.SC_OK) {
1050            InputStream is = resp.getEntity().getContent();
1051            ProvisionParser pp = new ProvisionParser(is, this);
1052            if (pp.parse()) {
1053                // Return the final polic key from the ProvisionParser
1054                return pp.getPolicyKey();
1055            }
1056        }
1057        // On failures, return null
1058        return null;
1059    }
1060
1061    /**
1062     * Performs FolderSync
1063     *
1064     * @throws IOException
1065     * @throws EasParserException
1066     */
1067    public void runAccountMailbox() throws IOException, EasParserException {
1068        // Initialize exit status to success
1069        mExitStatus = EmailServiceStatus.SUCCESS;
1070        try {
1071            try {
1072                SyncManager.callback()
1073                    .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
1074            } catch (RemoteException e1) {
1075                // Don't care if this fails
1076            }
1077
1078            if (mAccount.mSyncKey == null) {
1079                mAccount.mSyncKey = "0";
1080                userLog("Account syncKey INIT to 0");
1081                ContentValues cv = new ContentValues();
1082                cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
1083                mAccount.update(mContext, cv);
1084            }
1085
1086            boolean firstSync = mAccount.mSyncKey.equals("0");
1087            if (firstSync) {
1088                userLog("Initial FolderSync");
1089            }
1090
1091            // When we first start up, change all mailboxes to push.
1092            ContentValues cv = new ContentValues();
1093            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1094            if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1095                    WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
1096                    new String[] {Long.toString(mAccount.mId)}) > 0) {
1097                SyncManager.kick("change ping boxes to push");
1098            }
1099
1100            // Determine our protocol version, if we haven't already and save it in the Account
1101            if (mAccount.mProtocolVersion == null) {
1102                userLog("Determine EAS protocol version");
1103                HttpResponse resp = sendHttpClientOptions();
1104                int code = resp.getStatusLine().getStatusCode();
1105                userLog("OPTIONS response: ", code);
1106                if (code == HttpStatus.SC_OK) {
1107                    Header header = resp.getFirstHeader("MS-ASProtocolCommands");
1108                    userLog(header.getValue());
1109                    header = resp.getFirstHeader("ms-asprotocolversions");
1110                    String versions = header.getValue();
1111                    if (versions != null) {
1112                        if (versions.contains("12.0")) {
1113                            mProtocolVersion = "12.0";
1114                        }
1115                        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
1116                        mAccount.mProtocolVersion = mProtocolVersion;
1117                        // Save the protocol version
1118                        cv.clear();
1119                        cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
1120                        mAccount.update(mContext, cv);
1121                        userLog(versions);
1122                        userLog("Using version ", mProtocolVersion);
1123                    } else {
1124                        errorLog("No protocol versions in OPTIONS response");
1125                        throw new IOException();
1126                    }
1127                } else {
1128                    errorLog("OPTIONS command failed; throwing IOException");
1129                    throw new IOException();
1130                }
1131            }
1132
1133            // Change all pushable boxes to push when we start the account mailbox
1134            if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
1135                cv.clear();
1136                cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1137                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1138                        SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE,
1139                        new String[] {Long.toString(mAccount.mId)}) > 0) {
1140                    userLog("Push account; set pushable boxes to push...");
1141                }
1142            }
1143
1144            while (!mStop) {
1145                userLog("Sending Account syncKey: ", mAccount.mSyncKey);
1146                Serializer s = new Serializer();
1147                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
1148                    .text(mAccount.mSyncKey).end().end().done();
1149                HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
1150                if (mStop) break;
1151                int code = resp.getStatusLine().getStatusCode();
1152                if (code == HttpStatus.SC_OK) {
1153                    HttpEntity entity = resp.getEntity();
1154                    int len = (int)entity.getContentLength();
1155                    if (len != 0) {
1156                        InputStream is = entity.getContent();
1157                        // Returns true if we need to sync again
1158                        if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
1159                                .parse()) {
1160                            continue;
1161                        }
1162                    }
1163                } else if (isProvisionError(code)) {
1164                    // If the sync error is a provisioning failure (perhaps the policies changed),
1165                    // let's try the provisining procedure
1166                    if (!tryProvision()) {
1167                        // Set the appropriate failure status
1168                        mExitStatus = EXIT_SECURITY_FAILURE;
1169                        return;
1170                    }
1171                } else if (isAuthError(code)) {
1172                    mExitStatus = EXIT_LOGIN_FAILURE;
1173                    return;
1174                } else {
1175                    userLog("FolderSync response error: ", code);
1176                }
1177
1178                // Change all push/hold boxes to push
1179                cv.clear();
1180                cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
1181                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1182                        WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
1183                        new String[] {Long.toString(mAccount.mId)}) > 0) {
1184                    userLog("Set push/hold boxes to push...");
1185                }
1186
1187                try {
1188                    SyncManager.callback()
1189                        .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
1190                } catch (RemoteException e1) {
1191                    // Don't care if this fails
1192                }
1193
1194                // Wait for push notifications.
1195                String threadName = Thread.currentThread().getName();
1196                try {
1197                    runPingLoop();
1198                } catch (StaleFolderListException e) {
1199                    // We break out if we get told about a stale folder list
1200                    userLog("Ping interrupted; folder list requires sync...");
1201                } finally {
1202                    Thread.currentThread().setName(threadName);
1203                }
1204            }
1205         } catch (IOException e) {
1206            // We catch this here to send the folder sync status callback
1207            // A folder sync failed callback will get sent from run()
1208            try {
1209                if (!mStop) {
1210                    SyncManager.callback()
1211                        .syncMailboxListStatus(mAccount.mId,
1212                                EmailServiceStatus.CONNECTION_ERROR, 0);
1213                }
1214            } catch (RemoteException e1) {
1215                // Don't care if this fails
1216            }
1217            throw e;
1218        }
1219    }
1220
1221    void pushFallback(long mailboxId) {
1222        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1223        ContentValues cv = new ContentValues();
1224        int mins = PING_FALLBACK_PIM;
1225        if (mailbox.mType == Mailbox.TYPE_INBOX) {
1226            mins = PING_FALLBACK_INBOX;
1227        }
1228        cv.put(Mailbox.SYNC_INTERVAL, mins);
1229        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
1230                cv, null, null);
1231        errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
1232        SyncManager.kick("push fallback");
1233    }
1234
1235    void runPingLoop() throws IOException, StaleFolderListException {
1236        int pingHeartbeat = mPingHeartbeat;
1237        userLog("runPingLoop");
1238        // Do push for all sync services here
1239        long endTime = System.currentTimeMillis() + (30*MINUTES);
1240        HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
1241        ArrayList<String> readyMailboxes = new ArrayList<String>();
1242        ArrayList<String> notReadyMailboxes = new ArrayList<String>();
1243        int pingWaitCount = 0;
1244
1245        while ((System.currentTimeMillis() < endTime) && !mStop) {
1246            // Count of pushable mailboxes
1247            int pushCount = 0;
1248            // Count of mailboxes that can be pushed right now
1249            int canPushCount = 0;
1250            // Count of uninitialized boxes
1251            int uninitCount = 0;
1252
1253            Serializer s = new Serializer();
1254            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
1255                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
1256                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
1257            notReadyMailboxes.clear();
1258            readyMailboxes.clear();
1259            try {
1260                // Loop through our pushed boxes seeing what is available to push
1261                while (c.moveToNext()) {
1262                    pushCount++;
1263                    // Two requirements for push:
1264                    // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
1265                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
1266                    long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
1267                    int pingStatus = SyncManager.pingStatus(mailboxId);
1268                    String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
1269                    if (pingStatus == SyncManager.PING_STATUS_OK) {
1270                        String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
1271                        if ((syncKey == null) || syncKey.equals("0")) {
1272                            // We can't push until the initial sync is done
1273                            pushCount--;
1274                            uninitCount++;
1275                            continue;
1276                        }
1277
1278                        if (canPushCount++ == 0) {
1279                            // Initialize the Ping command
1280                            s.start(Tags.PING_PING)
1281                                .data(Tags.PING_HEARTBEAT_INTERVAL,
1282                                        Integer.toString(pingHeartbeat))
1283                                .start(Tags.PING_FOLDERS);
1284                        }
1285
1286                        String folderClass = getTargetCollectionClassFromCursor(c);
1287                        s.start(Tags.PING_FOLDER)
1288                            .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
1289                            .data(Tags.PING_CLASS, folderClass)
1290                            .end();
1291                        readyMailboxes.add(mailboxName);
1292                    } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) ||
1293                            (pingStatus == SyncManager.PING_STATUS_WAITING)) {
1294                        notReadyMailboxes.add(mailboxName);
1295                    } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) {
1296                        pushCount--;
1297                        userLog(mailboxName, " in error state; ignore");
1298                        continue;
1299                    }
1300                }
1301            } finally {
1302                c.close();
1303            }
1304
1305            if (Eas.USER_LOG) {
1306                if (!notReadyMailboxes.isEmpty()) {
1307                    userLog("Ping not ready for: " + notReadyMailboxes);
1308                }
1309                if (!readyMailboxes.isEmpty()) {
1310                    userLog("Ping ready for: " + readyMailboxes);
1311                }
1312            }
1313
1314            // If we've waited 10 seconds or more, just ping with whatever boxes are ready
1315            // But use a shorter than normal heartbeat
1316            boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
1317
1318            if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
1319                // If all pingable boxes are ready for push, send Ping to the server
1320                s.end().end().done();
1321                pingWaitCount = 0;
1322
1323                // If we've been stopped, this is a good time to return
1324                if (mStop) return;
1325
1326                long pingTime = SystemClock.elapsedRealtime();
1327                try {
1328                    // Send the ping, wrapped by appropriate timeout/alarm
1329                    if (forcePing) {
1330                        userLog("Forcing ping after waiting for all boxes to be ready");
1331                    }
1332                    HttpResponse res =
1333                        sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat);
1334
1335                    int code = res.getStatusLine().getStatusCode();
1336                    userLog("Ping response: ", code);
1337
1338                    // Return immediately if we've been asked to stop during the ping
1339                    if (mStop) {
1340                        userLog("Stopping pingLoop");
1341                        return;
1342                    }
1343
1344                    if (code == HttpStatus.SC_OK) {
1345                        HttpEntity e = res.getEntity();
1346                        int len = (int)e.getContentLength();
1347                        InputStream is = res.getEntity().getContent();
1348                        if (len != 0) {
1349                            int pingResult = parsePingResult(is, mContentResolver, pingErrorMap);
1350                            // If our ping completed (status = 1), and we weren't forced and we're
1351                            // not at the maximum, try increasing timeout by two minutes
1352                            if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
1353                                if (pingHeartbeat > mPingHighWaterMark) {
1354                                    mPingHighWaterMark = pingHeartbeat;
1355                                    userLog("Setting high water mark at: ", mPingHighWaterMark);
1356                                }
1357                                if ((pingHeartbeat < PING_MAX_HEARTBEAT) &&
1358                                        !mPingHeartbeatDropped) {
1359                                    pingHeartbeat += PING_HEARTBEAT_INCREMENT;
1360                                    if (pingHeartbeat > PING_MAX_HEARTBEAT) {
1361                                        pingHeartbeat = PING_MAX_HEARTBEAT;
1362                                    }
1363                                    userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
1364                                }
1365                            }
1366                        } else {
1367                            userLog("Ping returned empty result; throwing IOException");
1368                            throw new IOException();
1369                        }
1370                    } else if (isAuthError(code)) {
1371                        mExitStatus = EXIT_LOGIN_FAILURE;
1372                        userLog("Authorization error during Ping: ", code);
1373                        throw new IOException();
1374                    }
1375                } catch (IOException e) {
1376                    String message = e.getMessage();
1377                    // If we get the exception that is indicative of a NAT timeout and if we
1378                    // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
1379                    boolean hasMessage = message != null;
1380                    userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
1381                    if (mPostAborted || (hasMessage && message.contains("reset by peer"))) {
1382                        long pingLength = SystemClock.elapsedRealtime() - pingTime;
1383                        if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
1384                                (pingHeartbeat > mPingHighWaterMark)) {
1385                            pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
1386                            mPingHeartbeatDropped = true;
1387                            if (pingHeartbeat < PING_MIN_HEARTBEAT) {
1388                                pingHeartbeat = PING_MIN_HEARTBEAT;
1389                            }
1390                            userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
1391                        } else if (mPostAborted || (pingLength < 2000)) {
1392                            userLog("Abort or NAT type return < 2 seconds; throwing IOException");
1393                            throw e;
1394                        } else {
1395                            userLog("NAT type IOException > 2 seconds?");
1396                        }
1397                    } else {
1398                        throw e;
1399                    }
1400                }
1401            } else if (forcePing) {
1402                // In this case, there aren't any boxes that are pingable, but there are boxes
1403                // waiting (for IOExceptions)
1404                userLog("pingLoop waiting 60s for any pingable boxes");
1405                sleep(60*SECONDS, true);
1406            } else if (pushCount > 0) {
1407                // If we want to Ping, but can't just yet, wait a little bit
1408                // TODO Change sleep to wait and use notify from SyncManager when a sync ends
1409                sleep(2*SECONDS, false);
1410                pingWaitCount++;
1411                //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
1412            } else if (uninitCount > 0) {
1413                // In this case, we're doing an initial sync of at least one mailbox.  Since this
1414                // is typically a one-time case, I'm ok with trying again every 10 seconds until
1415                // we're in one of the other possible states.
1416                userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
1417                sleep(10*SECONDS, true);
1418            } else {
1419                // We've got nothing to do, so we'll check again in 30 minutes at which time
1420                // we'll update the folder list.  Let the device sleep in the meantime...
1421                userLog("pingLoop sleeping for 30m");
1422                sleep(30*MINUTES, true);
1423            }
1424        }
1425    }
1426
1427    void sleep(long ms, boolean runAsleep) {
1428        if (runAsleep) {
1429            SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS));
1430        }
1431        try {
1432            Thread.sleep(ms);
1433        } catch (InterruptedException e) {
1434            // Doesn't matter whether we stop early; it's the thought that counts
1435        } finally {
1436            if (runAsleep) {
1437                SyncManager.runAwake(mMailboxId);
1438            }
1439        }
1440    }
1441
1442    private int parsePingResult(InputStream is, ContentResolver cr,
1443            HashMap<String, Integer> errorMap)
1444        throws IOException, StaleFolderListException {
1445        PingParser pp = new PingParser(is, this);
1446        if (pp.parse()) {
1447            // True indicates some mailboxes need syncing...
1448            // syncList has the serverId's of the mailboxes...
1449            mBindArguments[0] = Long.toString(mAccount.mId);
1450            mPingChangeList = pp.getSyncList();
1451            for (String serverId: mPingChangeList) {
1452                mBindArguments[1] = serverId;
1453                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
1454                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
1455                try {
1456                    if (c.moveToFirst()) {
1457
1458                        /**
1459                         * Check the boxes reporting changes to see if there really were any...
1460                         * We do this because bugs in various Exchange servers can put us into a
1461                         * looping behavior by continually reporting changes in a mailbox, even when
1462                         * there aren't any.
1463                         *
1464                         * This behavior is seemingly random, and therefore we must code defensively
1465                         * by backing off of push behavior when it is detected.
1466                         *
1467                         * One known cause, on certain Exchange 2003 servers, is acknowledged by
1468                         * Microsoft, and the server hotfix for this case can be found at
1469                         * http://support.microsoft.com/kb/923282
1470                         */
1471
1472                        // Check the status of the last sync
1473                        String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
1474                        int type = SyncManager.getStatusType(status);
1475                        // This check should always be true...
1476                        if (type == SyncManager.SYNC_PING) {
1477                            int changeCount = SyncManager.getStatusChangeCount(status);
1478                            if (changeCount > 0) {
1479                                errorMap.remove(serverId);
1480                            } else if (changeCount == 0) {
1481                                // This means that a ping reported changes in error; we keep a count
1482                                // of consecutive errors of this kind
1483                                String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
1484                                Integer failures = errorMap.get(serverId);
1485                                if (failures == null) {
1486                                    userLog("Last ping reported changes in error for: ", name);
1487                                    errorMap.put(serverId, 1);
1488                                } else if (failures > MAX_PING_FAILURES) {
1489                                    // We'll back off of push for this box
1490                                    pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
1491                                    continue;
1492                                } else {
1493                                    userLog("Last ping reported changes in error for: ", name);
1494                                    errorMap.put(serverId, failures + 1);
1495                                }
1496                            }
1497                        }
1498
1499                        // If there were no problems with previous sync, we'll start another one
1500                        SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
1501                                SyncManager.SYNC_PING, null);
1502                    }
1503                } finally {
1504                    c.close();
1505                }
1506            }
1507        }
1508        return pp.getSyncStatus();
1509    }
1510
1511    private String getEmailFilter() {
1512        String filter = Eas.FILTER_1_WEEK;
1513        switch (mAccount.mSyncLookback) {
1514            case com.android.email.Account.SYNC_WINDOW_1_DAY: {
1515                filter = Eas.FILTER_1_DAY;
1516                break;
1517            }
1518            case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
1519                filter = Eas.FILTER_3_DAYS;
1520                break;
1521            }
1522            case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
1523                filter = Eas.FILTER_1_WEEK;
1524                break;
1525            }
1526            case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
1527                filter = Eas.FILTER_2_WEEKS;
1528                break;
1529            }
1530            case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
1531                filter = Eas.FILTER_1_MONTH;
1532                break;
1533            }
1534            case com.android.email.Account.SYNC_WINDOW_ALL: {
1535                filter = Eas.FILTER_ALL;
1536                break;
1537            }
1538        }
1539        return filter;
1540    }
1541
1542    /**
1543     * Common code to sync E+PIM data
1544     *
1545     * @param target, an EasMailbox, EasContacts, or EasCalendar object
1546     */
1547    public void sync(AbstractSyncAdapter target) throws IOException {
1548        Mailbox mailbox = target.mMailbox;
1549
1550        boolean moreAvailable = true;
1551        while (!mStop && moreAvailable) {
1552            // If we have no connectivity, just exit cleanly.  SyncManager will start us up again
1553            // when connectivity has returned
1554            if (!hasConnectivity()) {
1555                userLog("No connectivity in sync; finishing sync");
1556                mExitStatus = EXIT_DONE;
1557                return;
1558            }
1559
1560            // Now, handle various requests
1561            while (true) {
1562                Request req = null;
1563                synchronized (mRequests) {
1564                    if (mRequests.isEmpty()) {
1565                        break;
1566                    } else {
1567                        req = mRequests.get(0);
1568                    }
1569                }
1570
1571                // Our two request types are PartRequest (loading attachment) and
1572                // MeetingResponseRequest (respond to a meeting request)
1573                if (req instanceof PartRequest) {
1574                    getAttachment((PartRequest)req);
1575                } else if (req instanceof MeetingResponseRequest) {
1576                    sendMeetingResponse((MeetingResponseRequest)req);
1577                }
1578
1579                // If there's an exception handling the request, we'll throw it
1580                // Otherwise, we remove the request
1581                synchronized(mRequests) {
1582                    mRequests.remove(req);
1583                }
1584            }
1585
1586            Serializer s = new Serializer();
1587            String className = target.getCollectionName();
1588            String syncKey = target.getSyncKey();
1589            userLog("sync, sending ", className, " syncKey: ", syncKey);
1590            s.start(Tags.SYNC_SYNC)
1591                .start(Tags.SYNC_COLLECTIONS)
1592                .start(Tags.SYNC_COLLECTION)
1593                .data(Tags.SYNC_CLASS, className)
1594                .data(Tags.SYNC_SYNC_KEY, syncKey)
1595                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
1596                .tag(Tags.SYNC_DELETES_AS_MOVES);
1597
1598            // EAS doesn't like GetChanges if the syncKey is "0"; not documented
1599            if (!syncKey.equals("0")) {
1600                s.tag(Tags.SYNC_GET_CHANGES);
1601            }
1602            s.data(Tags.SYNC_WINDOW_SIZE,
1603                    className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
1604
1605            // Handle options
1606            s.start(Tags.SYNC_OPTIONS);
1607            // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
1608            if (className.equals("Email")) {
1609                s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
1610            } else if (className.equals("Calendar")) {
1611                // TODO Force one month for calendar until we can set this!
1612                s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH);
1613            }
1614            // Set the truncation amount for all classes
1615            if (mProtocolVersionDouble >= 12.0) {
1616                s.start(Tags.BASE_BODY_PREFERENCE)
1617                    // HTML for email; plain text for everything else
1618                    .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
1619                        : Eas.BODY_PREFERENCE_TEXT))
1620                    .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE)
1621                    .end();
1622            } else {
1623                s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
1624            }
1625            s.end();
1626
1627            // Send our changes up to the server
1628            target.sendLocalChanges(s);
1629
1630            s.end().end().end().done();
1631            HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
1632            int code = resp.getStatusLine().getStatusCode();
1633            if (code == HttpStatus.SC_OK) {
1634                InputStream is = resp.getEntity().getContent();
1635                if (is != null) {
1636                    moreAvailable = target.parse(is);
1637                    target.cleanup();
1638                } else {
1639                    userLog("Empty input stream in sync command response");
1640                }
1641            } else {
1642                userLog("Sync response error: ", code);
1643                if (isProvisionError(code)) {
1644                    if (!tryProvision()) {
1645                        mExitStatus = EXIT_SECURITY_FAILURE;
1646                        return;
1647                    }
1648                } else if (isAuthError(code)) {
1649                    mExitStatus = EXIT_LOGIN_FAILURE;
1650                } else {
1651                    mExitStatus = EXIT_IO_ERROR;
1652                }
1653                return;
1654            }
1655        }
1656        mExitStatus = EXIT_DONE;
1657    }
1658
1659    protected boolean setupService() {
1660        // Make sure account and mailbox are always the latest from the database
1661        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
1662        if (mAccount == null) return false;
1663        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
1664        if (mMailbox == null) return false;
1665        mThread = Thread.currentThread();
1666        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
1667        TAG = mThread.getName();
1668
1669        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
1670        if (ha == null) return false;
1671        mHostAddress = ha.mAddress;
1672        mUserName = ha.mLogin;
1673        mPassword = ha.mPassword;
1674
1675        // Set up our protocol version from the Account
1676        mProtocolVersion = mAccount.mProtocolVersion;
1677        // If it hasn't been set up, start with default version
1678        if (mProtocolVersion == null) {
1679            mProtocolVersion = DEFAULT_PROTOCOL_VERSION;
1680        }
1681        mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
1682        return true;
1683    }
1684
1685    /* (non-Javadoc)
1686     * @see java.lang.Runnable#run()
1687     */
1688    public void run() {
1689        if (!setupService()) return;
1690
1691        try {
1692            SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
1693        } catch (RemoteException e1) {
1694            // Don't care if this fails
1695        }
1696
1697        // Whether or not we're the account mailbox
1698        try {
1699            mDeviceId = SyncManager.getDeviceId();
1700            if ((mMailbox == null) || (mAccount == null)) {
1701                return;
1702            } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
1703                runAccountMailbox();
1704            } else {
1705                AbstractSyncAdapter target;
1706                if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
1707                    target = new ContactsSyncAdapter(mMailbox, this);
1708                } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
1709                    target = new CalendarSyncAdapter(mMailbox, this);
1710                } else {
1711                    target = new EmailSyncAdapter(mMailbox, this);
1712                }
1713                // We loop here because someone might have put a request in while we were syncing
1714                // and we've missed that opportunity...
1715                do {
1716                    if (mRequestTime != 0) {
1717                        userLog("Looping for user request...");
1718                        mRequestTime = 0;
1719                    }
1720                    sync(target);
1721                } while (mRequestTime != 0);
1722            }
1723        } catch (EasAuthenticationException e) {
1724            userLog("Caught authentication error");
1725            mExitStatus = EXIT_LOGIN_FAILURE;
1726        } catch (IOException e) {
1727            String message = e.getMessage();
1728            userLog("Caught IOException: ", (message == null) ? "No message" : message);
1729            mExitStatus = EXIT_IO_ERROR;
1730        } catch (Exception e) {
1731            userLog("Uncaught exception in EasSyncService", e);
1732        } finally {
1733            int status;
1734
1735            if (!mStop) {
1736                userLog("Sync finished");
1737                SyncManager.done(this);
1738                switch (mExitStatus) {
1739                    case EXIT_IO_ERROR:
1740                        status = EmailServiceStatus.CONNECTION_ERROR;
1741                        break;
1742                    case EXIT_DONE:
1743                        status = EmailServiceStatus.SUCCESS;
1744                        ContentValues cv = new ContentValues();
1745                        cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1746                        String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
1747                        cv.put(Mailbox.SYNC_STATUS, s);
1748                        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
1749                                mMailboxId), cv, null, null);
1750                        break;
1751                    case EXIT_LOGIN_FAILURE:
1752                        status = EmailServiceStatus.LOGIN_FAILED;
1753                        break;
1754                    case EXIT_SECURITY_FAILURE:
1755                        status = EmailServiceStatus.SECURITY_FAILURE;
1756                        break;
1757                    default:
1758                        status = EmailServiceStatus.REMOTE_EXCEPTION;
1759                        errorLog("Sync ended due to an exception.");
1760                        break;
1761                }
1762            } else {
1763                userLog("Stopped sync finished.");
1764                status = EmailServiceStatus.SUCCESS;
1765            }
1766
1767            try {
1768                SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
1769            } catch (RemoteException e1) {
1770                // Don't care if this fails
1771            }
1772
1773            // Make sure SyncManager knows about this
1774            SyncManager.kick("sync finished");
1775       }
1776    }
1777}
1778