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