EasSyncService.java revision 3ba50d433ee89b252cd1dd9fd3eee1f2baa3945b
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.emailcommon.mail.Address;
21import com.android.emailcommon.mail.MeetingInfo;
22import com.android.emailcommon.mail.MessagingException;
23import com.android.emailcommon.mail.PackedString;
24import com.android.emailcommon.provider.EmailContent.Account;
25import com.android.emailcommon.provider.EmailContent.AccountColumns;
26import com.android.emailcommon.provider.EmailContent.Attachment;
27import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
28import com.android.emailcommon.provider.EmailContent.HostAuth;
29import com.android.emailcommon.provider.EmailContent.Mailbox;
30import com.android.emailcommon.provider.EmailContent.MailboxColumns;
31import com.android.emailcommon.provider.EmailContent.Message;
32import com.android.emailcommon.provider.EmailContent.MessageColumns;
33import com.android.emailcommon.provider.EmailContent.SyncColumns;
34import com.android.emailcommon.provider.Policy;
35import com.android.emailcommon.service.EmailServiceConstants;
36import com.android.emailcommon.service.EmailServiceProxy;
37import com.android.emailcommon.service.EmailServiceStatus;
38import com.android.emailcommon.utility.AttachmentUtilities;
39import com.android.emailcommon.utility.Utility;
40import com.android.exchange.CommandStatusException.CommandStatus;
41import com.android.exchange.adapter.AbstractSyncAdapter;
42import com.android.exchange.adapter.AccountSyncAdapter;
43import com.android.exchange.adapter.CalendarSyncAdapter;
44import com.android.exchange.adapter.ContactsSyncAdapter;
45import com.android.exchange.adapter.EmailSyncAdapter;
46import com.android.exchange.adapter.FolderSyncParser;
47import com.android.exchange.adapter.GalParser;
48import com.android.exchange.adapter.MeetingResponseParser;
49import com.android.exchange.adapter.MoveItemsParser;
50import com.android.exchange.adapter.Parser.EasParserException;
51import com.android.exchange.adapter.Parser.EmptyStreamException;
52import com.android.exchange.adapter.PingParser;
53import com.android.exchange.adapter.ProvisionParser;
54import com.android.exchange.adapter.SearchParser;
55import com.android.exchange.adapter.Serializer;
56import com.android.exchange.adapter.Tags;
57import com.android.exchange.provider.GalResult;
58import com.android.exchange.provider.MailboxUtilities;
59import com.android.exchange.utility.CalendarUtilities;
60
61import org.apache.http.Header;
62import org.apache.http.HttpEntity;
63import org.apache.http.HttpResponse;
64import org.apache.http.HttpStatus;
65import org.apache.http.client.HttpClient;
66import org.apache.http.client.methods.HttpOptions;
67import org.apache.http.client.methods.HttpPost;
68import org.apache.http.client.methods.HttpRequestBase;
69import org.apache.http.conn.ClientConnectionManager;
70import org.apache.http.entity.ByteArrayEntity;
71import org.apache.http.entity.StringEntity;
72import org.apache.http.impl.client.DefaultHttpClient;
73import org.apache.http.params.BasicHttpParams;
74import org.apache.http.params.HttpConnectionParams;
75import org.apache.http.params.HttpParams;
76import org.xmlpull.v1.XmlPullParser;
77import org.xmlpull.v1.XmlPullParserException;
78import org.xmlpull.v1.XmlPullParserFactory;
79import org.xmlpull.v1.XmlSerializer;
80
81import android.content.ContentResolver;
82import android.content.ContentUris;
83import android.content.ContentValues;
84import android.content.Context;
85import android.content.Entity;
86import android.database.Cursor;
87import android.net.Uri;
88import android.os.Build;
89import android.os.Bundle;
90import android.os.RemoteException;
91import android.os.SystemClock;
92import android.provider.Calendar.Attendees;
93import android.provider.Calendar.Events;
94import android.text.TextUtils;
95import android.util.Base64;
96import android.util.Log;
97import android.util.Xml;
98
99import java.io.ByteArrayOutputStream;
100import java.io.FileNotFoundException;
101import java.io.IOException;
102import java.io.InputStream;
103import java.io.OutputStream;
104import java.lang.Thread.State;
105import java.net.URI;
106import java.security.cert.CertificateException;
107import java.util.ArrayList;
108import java.util.HashMap;
109import java.util.zip.GZIPInputStream;
110
111public class EasSyncService extends AbstractSyncService {
112    // DO NOT CHECK IN SET TO TRUE
113    public static final boolean DEBUG_GAL_SERVICE = false;
114
115    private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
116        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
117    private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
118        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
119        '=' + Mailbox.CHECK_INTERVAL_PING;
120    private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
121        MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
122        ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
123        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
124    private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
125        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
126        '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
127    static private final int CHUNK_SIZE = 16*1024;
128
129    static private final String PING_COMMAND = "Ping";
130    // Command timeout is the the time allowed for reading data from an open connection before an
131    // IOException is thrown.  After a small added allowance, our watchdog alarm goes off (allowing
132    // us to detect a silently dropped connection).  The allowance is defined below.
133    static private final int COMMAND_TIMEOUT = 30*SECONDS;
134    // Connection timeout is the time given to connect to the server before reporting an IOException
135    static private final int CONNECTION_TIMEOUT = 20*SECONDS;
136    // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
137    static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
138
139    // The amount of time the account mailbox will sleep if there are no pingable mailboxes
140    // This could happen if the sync time is set to "never"; we always want to check in from time
141    // to time, however, for folder list/policy changes
142    static private final int ACCOUNT_MAILBOX_SLEEP_TIME = 20*MINUTES;
143    static private final String ACCOUNT_MAILBOX_SLEEP_TEXT =
144        "Account mailbox sleeping for " + (ACCOUNT_MAILBOX_SLEEP_TIME / MINUTES) + "m";
145
146    static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
147        "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
148    static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
149    static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
150
151    static private final int INTERNAL_SERVER_ERROR_CODE = 500;
152
153    static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
154    static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
155
156    static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
157
158    /**
159     * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time.  There's
160     * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
161     * the ping exception out.  The maximum I use is 17 minutes, which is really an empirical
162     * choice; too long and we risk silent connection loss and loss of push for that period.  Too
163     * short and we lose efficiency/battery life.
164     *
165     * If we ever have to drop the ping timeout, we'll never increase it again.  There's no point
166     * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
167     * which will cause the sync service to be restarted at the starting heartbeat and going through
168     * the process again.
169     */
170    static private final int PING_MINUTES = 60; // in seconds
171    static private final int PING_FUDGE_LOW = 10;
172    static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
173    static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
174
175    // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
176    // forcing it to stop.  This number has been determined empirically.
177    static private final int MAX_LOOPING_COUNT = 100;
178
179    static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
180
181    // The amount of time we allow for a thread to release its post lock after receiving an alert
182    static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
183
184    // Fallbacks (in minutes) for ping loop failures
185    static private final int MAX_PING_FAILURES = 1;
186    static private final int PING_FALLBACK_INBOX = 5;
187    static private final int PING_FALLBACK_PIM = 25;
188
189    // MSFT's custom HTTP result code indicating the need to provision
190    static private final int HTTP_NEED_PROVISIONING = 449;
191
192    // The EAS protocol Provision status for "we implement all of the policies"
193    static private final String PROVISION_STATUS_OK = "1";
194    // The EAS protocol Provision status meaning "we partially implement the policies"
195    static private final String PROVISION_STATUS_PARTIAL = "2";
196
197    static /*package*/ final String DEVICE_TYPE = "Android";
198    static private final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
199        Eas.CLIENT_VERSION;
200
201    // The shortest search query we'll accept
202    // TODO Check with UX whether this is correct
203    static private final int MIN_QUERY_LENGTH = 3;
204    // The largest number of results we'll ask for per server request
205    static private final int MAX_SEARCH_RESULTS = 100;
206
207    // Reasonable default
208    public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
209    public Double mProtocolVersionDouble;
210    protected String mDeviceId = null;
211    /*package*/ String mAuthString = null;
212    /*package*/ String mCmdString = null;
213    public String mHostAddress;
214    public String mUserName;
215    public String mPassword;
216    private boolean mSsl = true;
217    private boolean mTrustSsl = false;
218    public ContentResolver mContentResolver;
219    private String[] mBindArguments = new String[2];
220    private ArrayList<String> mPingChangeList;
221    // The HttpPost in progress
222    private volatile HttpPost mPendingPost = null;
223    // Our heartbeat when we are waiting for ping boxes to be ready
224    /*package*/ int mPingForceHeartbeat = 2*PING_MINUTES;
225    // The minimum heartbeat we will send
226    /*package*/ int mPingMinHeartbeat = (5*PING_MINUTES)-PING_FUDGE_LOW;
227    // The maximum heartbeat we will send
228    /*package*/ int mPingMaxHeartbeat = (17*PING_MINUTES)-PING_FUDGE_LOW;
229    // The ping time (in seconds)
230    /*package*/ int mPingHeartbeat = PING_STARTING_HEARTBEAT;
231    // The longest successful ping heartbeat
232    private int mPingHighWaterMark = 0;
233    // Whether we've ever lowered the heartbeat
234    /*package*/ boolean mPingHeartbeatDropped = false;
235    // Whether a POST was aborted due to alarm (watchdog alarm)
236    private boolean mPostAborted = false;
237    // Whether a POST was aborted due to reset
238    private boolean mPostReset = false;
239    // Whether or not the sync service is valid (usable)
240    public boolean mIsValid = true;
241
242    public EasSyncService(Context _context, Mailbox _mailbox) {
243        super(_context, _mailbox);
244        mContentResolver = _context.getContentResolver();
245        if (mAccount == null) {
246            mIsValid = false;
247            return;
248        }
249        HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
250        if (ha == null) {
251            mIsValid = false;
252            return;
253        }
254        mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
255        mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
256    }
257
258    private EasSyncService(String prefix) {
259        super(prefix);
260    }
261
262    public EasSyncService() {
263        this("EAS Validation");
264    }
265
266    @Override
267    /**
268     * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
269     * socket timeout without having thrown an Exception
270     *
271     * @return true if the POST was successfully stopped; false if we've failed and interrupted
272     * the thread
273     */
274    public boolean alarm() {
275        HttpPost post;
276        if (mThread == null) return true;
277        String threadName = mThread.getName();
278
279        // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
280        // executePostWithTimeout (which executes the HttpPost) also uses this lock
281        synchronized(getSynchronizer()) {
282            // Get a reference to the current post lock
283            post = mPendingPost;
284            if (post != null) {
285                if (Eas.USER_LOG) {
286                    URI uri = post.getURI();
287                    if (uri != null) {
288                        String query = uri.getQuery();
289                        if (query == null) {
290                            query = "POST";
291                        }
292                        userLog(threadName, ": Alert, aborting ", query);
293                    } else {
294                        userLog(threadName, ": Alert, no URI?");
295                    }
296                }
297                // Abort the POST
298                mPostAborted = true;
299                post.abort();
300            } else {
301                // If there's no POST, we're done
302                userLog("Alert, no pending POST");
303                return true;
304            }
305        }
306
307        // Wait for the POST to finish
308        try {
309            Thread.sleep(POST_LOCK_TIMEOUT);
310        } catch (InterruptedException e) {
311        }
312
313        State s = mThread.getState();
314        if (Eas.USER_LOG) {
315            userLog(threadName + ": State = " + s.name());
316        }
317
318        synchronized (getSynchronizer()) {
319            // If the thread is still hanging around and the same post is pending, let's try to
320            // stop the thread with an interrupt.
321            if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
322                mStop = true;
323                mThread.interrupt();
324                userLog("Interrupting...");
325                // Let the caller know we had to interrupt the thread
326                return false;
327            }
328        }
329        // Let the caller know that the alarm was handled normally
330        return true;
331    }
332
333    @Override
334    public void reset() {
335        synchronized(getSynchronizer()) {
336            if (mPendingPost != null) {
337                URI uri = mPendingPost.getURI();
338                if (uri != null) {
339                    String query = uri.getQuery();
340                    if (query.startsWith("Cmd=Ping")) {
341                        userLog("Reset, aborting Ping");
342                        mPostReset = true;
343                        mPendingPost.abort();
344                    }
345                }
346            }
347        }
348    }
349
350    @Override
351    public void stop() {
352        mStop = true;
353        synchronized(getSynchronizer()) {
354            if (mPendingPost != null) {
355                mPendingPost.abort();
356            }
357        }
358    }
359
360    @Override
361    public void addRequest(Request request) {
362        // Don't allow duplicates of requests; just refuse them
363        if (mRequestQueue.contains(request)) return;
364        // Add the request
365        super.addRequest(request);
366    }
367
368    /**
369     * Determine whether an HTTP code represents an authentication error
370     * @param code the HTTP code returned by the server
371     * @return whether or not the code represents an authentication error
372     */
373    protected boolean isAuthError(int code) {
374        return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
375    }
376
377    /**
378     * Determine whether an HTTP code represents a provisioning error
379     * @param code the HTTP code returned by the server
380     * @return whether or not the code represents an provisioning error
381     */
382    protected boolean isProvisionError(int code) {
383        return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN);
384    }
385
386    private void setupProtocolVersion(EasSyncService service, Header versionHeader)
387            throws MessagingException {
388        // The string is a comma separated list of EAS versions in ascending order
389        // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
390        String supportedVersions = versionHeader.getValue();
391        userLog("Server supports versions: ", supportedVersions);
392        String[] supportedVersionsArray = supportedVersions.split(",");
393        String ourVersion = null;
394        // Find the most recent version we support
395        for (String version: supportedVersionsArray) {
396            if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
397                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
398                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) ||
399                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) ||
400                    version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) {
401                ourVersion = version;
402            }
403        }
404        // If we don't support any of the servers supported versions, throw an exception here
405        // This will cause validation to fail
406        if (ourVersion == null) {
407            Log.w(TAG, "No supported EAS versions: " + supportedVersions);
408            throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
409        } else {
410            service.mProtocolVersion = ourVersion;
411            service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
412            if (service.mAccount != null) {
413                service.mAccount.mProtocolVersion = ourVersion;
414            }
415        }
416    }
417
418    /**
419     * Create an EasSyncService for the specified account
420     *
421     * @param context the caller's context
422     * @param account the account
423     * @return the service, or null if the account is on hold or hasn't been initialized
424     */
425    private static EasSyncService setupServiceForAccount(Context context, Account account) {
426        // Just return null if we're on security hold
427        if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
428            return null;
429        }
430        // If there's no protocol version, we're not initialized
431        String protocolVersion = account.mProtocolVersion;
432        if (protocolVersion == null) {
433            return null;
434        }
435        EasSyncService svc = new EasSyncService("OutOfBand");
436        HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
437        svc.mProtocolVersion = protocolVersion;
438        svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
439        svc.mContext = context;
440        svc.mHostAddress = ha.mAddress;
441        svc.mUserName = ha.mLogin;
442        svc.mPassword = ha.mPassword;
443        svc.mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
444        svc.mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
445        try {
446            svc.mDeviceId = ExchangeService.getDeviceId(context);
447        } catch (IOException e) {
448            return null;
449        }
450        svc.mAccount = account;
451        return svc;
452    }
453
454    /**
455     * Encapsulate a response to an HTTP POST
456     */
457    public static class EasResponse {
458        private final HttpResponse mResponse;
459        private final HttpEntity mEntity;
460        private final int mLength;
461        private InputStream mInputStream;
462        private boolean mClosed;
463
464        public EasResponse(HttpResponse response) {
465            mResponse = response;
466            mEntity = mResponse.getEntity();
467            if (mEntity !=  null) {
468                mLength = (int)mEntity.getContentLength();
469            } else {
470                mLength = 0;
471            }
472        }
473
474        /**
475         * Return an appropriate input stream for the response, either a GZIPInputStream, for
476         * compressed data, or a generic InputStream otherwise
477         * @return the input stream for the response
478         */
479        public InputStream getInputStream() {
480            if (mInputStream != null || mClosed) {
481                throw new IllegalStateException("Can't reuse stream or get closed stream");
482            } else if (mEntity == null) {
483                throw new IllegalStateException("Can't get input stream without entity");
484            }
485            InputStream is = null;
486            try {
487                // Get the default input stream for the entity
488                is = mEntity.getContent();
489                Header ceHeader = mResponse.getFirstHeader("Content-Encoding");
490                if (ceHeader != null) {
491                    String encoding = ceHeader.getValue();
492                    // If we're gzip encoded, wrap appropriately
493                    if (encoding.toLowerCase().equals("gzip")) {
494                        is = new GZIPInputStream(is);
495                    }
496                }
497            } catch (IllegalStateException e1) {
498            } catch (IOException e1) {
499            }
500            mInputStream = is;
501            return is;
502        }
503
504        public boolean isEmpty() {
505            return mLength == 0;
506        }
507
508        public int getStatus() {
509            return mResponse.getStatusLine().getStatusCode();
510        }
511
512        public Header getHeader(String name) {
513            return mResponse.getFirstHeader(name);
514        }
515
516        public int getLength() {
517            return mLength;
518        }
519
520        public void close() {
521            if (mEntity != null && !mClosed) {
522                try {
523                    mEntity.consumeContent();
524                } catch (IOException e) {
525                    // No harm, no foul
526                }
527            }
528            mClosed = true;
529        }
530    }
531
532    public static int searchMessages(Context context, long accountId, long mailboxId,
533            boolean includeSubfolders, String query, int numResults, int firstResult,
534            long destMailboxId) {
535        // Sanity check for arguments
536        if (numResults < 0 || numResults > MAX_SEARCH_RESULTS || firstResult < 0) return 0;
537        // TODO Should this be checked in UI?  Are there guidelines for minimums?
538        if (query == null || query.length() < MIN_QUERY_LENGTH) return 0;
539
540        int res = 0;
541        Account account = Account.restoreAccountWithId(context, accountId);
542        if (account == null) return res;
543        EasSyncService svc = setupServiceForAccount(context, account);
544        if (svc == null) return res;
545        try {
546            Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
547            // Sanity check; account might have been deleted?
548            if (searchMailbox == null) return res;
549            svc.mMailbox = searchMailbox;
550            svc.mAccount = account;
551            Serializer s = new Serializer();
552            s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
553            s.data(Tags.SEARCH_NAME, "Mailbox");
554            s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND);
555            s.data(Tags.SYNC_CLASS, "Email");
556            s.data(Tags.SEARCH_FREE_TEXT, query);
557            s.end().end();              // SEARCH_AND, SEARCH_QUERY
558            s.start(Tags.SEARCH_OPTIONS);
559            if (firstResult == 0) {
560                s.tag(Tags.SEARCH_REBUILD_RESULTS);
561            }
562            if (includeSubfolders) {
563                s.tag(Tags.SEARCH_DEEP_TRAVERSAL);
564            }
565            // Range is sent in the form first-last (e.g. 0-9)
566            s.data(Tags.SEARCH_RANGE, firstResult + "-" + (firstResult + numResults - 1));
567            s.start(Tags.BASE_BODY_PREFERENCE);
568            s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
569            s.data(Tags.BASE_TRUNCATION_SIZE, "20000");
570            s.end();                    // BASE_BODY_PREFERENCE
571            s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH
572            EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
573            try {
574                int code = resp.getStatus();
575                if (code == HttpStatus.SC_OK) {
576                    InputStream is = resp.getInputStream();
577                    try {
578                        new SearchParser(is, svc, query).parse();
579                    } finally {
580                        is.close();
581                    }
582                } else {
583                    svc.userLog("Search returned " + code);
584                }
585            } finally {
586                resp.close();
587            }
588        } catch (IOException e) {
589            svc.userLog("Search exception " + e);
590        }
591        // TODO Capture and return the correct value
592        return res;
593    }
594
595    @Override
596    public Bundle validateAccount(String hostAddress, String userName, String password, int port,
597            boolean ssl, boolean trustCertificates, Context context) {
598        Bundle bundle = new Bundle();
599        int resultCode = MessagingException.NO_ERROR;
600        try {
601            userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
602            EasSyncService svc = new EasSyncService("%TestAccount%");
603            svc.mContext = context;
604            svc.mHostAddress = hostAddress;
605            svc.mUserName = userName;
606            svc.mPassword = password;
607            svc.mSsl = ssl;
608            svc.mTrustSsl = trustCertificates;
609            // We mustn't use the "real" device id or we'll screw up current accounts
610            // Any string will do, but we'll go for "validate"
611            svc.mDeviceId = "validate";
612            svc.mAccount = new Account();
613            svc.mAccount.mEmailAddress = userName;
614            EasResponse resp = svc.sendHttpClientOptions();
615            try {
616                int code = resp.getStatus();
617                userLog("Validation (OPTIONS) response: " + code);
618                if (code == HttpStatus.SC_OK) {
619                    // No exception means successful validation
620                    Header commands = resp.getHeader("MS-ASProtocolCommands");
621                    Header versions = resp.getHeader("ms-asprotocolversions");
622                    // Make sure we've got the right protocol version set up
623                    try {
624                        if (commands == null || versions == null) {
625                            userLog("OPTIONS response without commands or versions");
626                            // We'll treat this as a protocol exception
627                            throw new MessagingException(0);
628                        }
629                        setupProtocolVersion(svc, versions);
630                    } catch (MessagingException e) {
631                        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
632                                MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
633                        return bundle;
634                    }
635
636                    // Run second test here for provisioning failures using FolderSync
637                    userLog("Try folder sync");
638                    // Send "0" as the sync key for new accounts; otherwise, use the current key
639                    String syncKey = "0";
640                    Account existingAccount =
641                        Utility.findExistingAccount(context, -1L, hostAddress, userName);
642                    if (existingAccount != null && existingAccount.mSyncKey != null) {
643                        syncKey = existingAccount.mSyncKey;
644                    }
645                    Serializer s = new Serializer();
646                    s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
647                        .end().end().done();
648                    resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
649                    code = resp.getStatus();
650                    // We'll get one of the following responses if policies are required
651                    if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
652                        throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
653                    } else if (code == HttpStatus.SC_NOT_FOUND) {
654                        // We get a 404 from OWA addresses (which are NOT EAS addresses)
655                        resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
656                    } else if (code != HttpStatus.SC_OK) {
657                        // Fail generically with anything other than success
658                        userLog("Unexpected response for FolderSync: ", code);
659                        resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
660                    } else {
661                        // We need to parse the result to see if we've got a provisioning issue
662                        // (EAS 14.0 only)
663                        if (!resp.isEmpty()) {
664                            InputStream is = resp.getInputStream();
665                            // Create the parser with statusOnly set to true; we only care about
666                            // seeing if a CommandStatusException is thrown (indicating a
667                            // provisioning failure)
668                            new FolderSyncParser(is, new AccountSyncAdapter(svc), true).parse();
669                        }
670                        userLog("Validation successful");
671                    }
672                } else if (isAuthError(code)) {
673                    userLog("Authentication failed");
674                    resultCode = MessagingException.AUTHENTICATION_FAILED;
675                } else if (code == INTERNAL_SERVER_ERROR_CODE) {
676                    // For Exchange 2003, this could mean an authentication failure OR server error
677                    userLog("Internal server error");
678                    resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
679                } else {
680                    // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
681                    userLog("Validation failed, reporting I/O error: ", code);
682                    resultCode = MessagingException.IOERROR;
683                }
684            } catch (CommandStatusException e) {
685                int status = e.mStatus;
686                if (CommandStatus.isNeedsProvisioning(status)) {
687                    // Get the policies and see if we are able to support them
688                    ProvisionParser pp = svc.canProvision();
689                    if (pp != null) {
690                        // Set the proper result code and save the PolicySet in our Bundle
691                        resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
692                        bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
693                                pp.getPolicy());
694                    } else
695                        // If not, set the proper code (the account will not be created)
696                        resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
697                } else if (CommandStatus.isDeniedAccess(status)) {
698                    userLog("Denied access: ", CommandStatus.toString(status));
699                    resultCode = MessagingException.ACCESS_DENIED;
700                } else if (CommandStatus.isTransientError(status)) {
701                    userLog("Transient error: ", CommandStatus.toString(status));
702                    resultCode = MessagingException.IOERROR;
703                } else {
704                    userLog("Unexpected response: ", CommandStatus.toString(status));
705                    resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
706                }
707            } finally {
708                resp.close();
709           }
710        } catch (IOException e) {
711            Throwable cause = e.getCause();
712            if (cause != null && cause instanceof CertificateException) {
713                userLog("CertificateException caught: ", e.getMessage());
714                resultCode = MessagingException.GENERAL_SECURITY;
715            }
716            userLog("IOException caught: ", e.getMessage());
717            resultCode = MessagingException.IOERROR;
718        }
719        bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
720        return bundle;
721    }
722
723    /**
724     * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
725     * it can be reused
726     *
727     * @param resp the HttpResponse that indicates a redirect (451)
728     * @param post the HttpPost that was originally sent to the server
729     * @return the HttpPost, updated with the redirect location
730     */
731    private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
732        Header locHeader = resp.getFirstHeader("X-MS-Location");
733        if (locHeader != null) {
734            String loc = locHeader.getValue();
735            // If we've gotten one and it shows signs of looking like an address, we try
736            // sending our request there
737            if (loc != null && loc.startsWith("http")) {
738                post.setURI(URI.create(loc));
739                return post;
740            }
741        }
742        return null;
743    }
744
745    /**
746     * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
747     * return the HttpResponse.  If we get a 401 (unauthorized) error and we're using the
748     * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
749     *
750     * @param client the HttpClient to be used for the request
751     * @param post the HttpPost we're going to send
752     * @param canRetry whether we can retry using the bare name on an authentication failure (401)
753     * @return an HttpResponse from the original or redirect server
754     * @throws IOException on any IOException within the HttpClient code
755     * @throws MessagingException
756     */
757    private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
758            throws IOException, MessagingException {
759        userLog("Posting autodiscover to: " + post.getURI());
760        EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
761        int code = resp.getStatus();
762        // On a redirect, try the new location
763        if (code == AUTO_DISCOVER_REDIRECT_CODE) {
764            post = getRedirect(resp.mResponse, post);
765            if (post != null) {
766                userLog("Posting autodiscover to redirect: " + post.getURI());
767                return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
768            }
769        // 401 (Unauthorized) is for true auth errors when used in Autodiscover
770        } else if (code == HttpStatus.SC_UNAUTHORIZED) {
771            if (canRetry && mUserName.contains("@")) {
772                // Try again using the bare user name
773                int atSignIndex = mUserName.indexOf('@');
774                mUserName = mUserName.substring(0, atSignIndex);
775                cacheAuthAndCmdString();
776                userLog("401 received; trying username: ", mUserName);
777                // Recreate the basic authentication string and reset the header
778                post.removeHeaders("Authorization");
779                post.setHeader("Authorization", mAuthString);
780                return postAutodiscover(client, post, false);
781            }
782            throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
783        // 403 (and others) we'll just punt on
784        } else if (code != HttpStatus.SC_OK) {
785            // We'll try the next address if this doesn't work
786            userLog("Code: " + code + ", throwing IOException");
787            throw new IOException();
788        }
789        return resp;
790    }
791
792    /**
793     * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
794     * only an email address and the password
795     *
796     * @param userName the user's email address
797     * @param password the user's password
798     * @return a HostAuth ready to be saved in an Account or null (failure)
799     */
800    public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
801        XmlSerializer s = Xml.newSerializer();
802        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
803        HostAuth hostAuth = new HostAuth();
804        Bundle bundle = new Bundle();
805        bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
806                MessagingException.NO_ERROR);
807        try {
808            // Build the XML document that's sent to the autodiscover server(s)
809            s.setOutput(os, "UTF-8");
810            s.startDocument("UTF-8", false);
811            s.startTag(null, "Autodiscover");
812            s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
813            s.startTag(null, "Request");
814            s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
815            s.startTag(null, "AcceptableResponseSchema");
816            s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
817            s.endTag(null, "AcceptableResponseSchema");
818            s.endTag(null, "Request");
819            s.endTag(null, "Autodiscover");
820            s.endDocument();
821            String req = os.toString();
822
823            // Initialize the user name and password
824            mUserName = userName;
825            mPassword = password;
826            // Make sure the authentication string is recreated and cached
827            cacheAuthAndCmdString();
828
829            // Split out the domain name
830            int amp = userName.indexOf('@');
831            // The UI ensures that userName is a valid email address
832            if (amp < 0) {
833                throw new RemoteException();
834            }
835            String domain = userName.substring(amp + 1);
836
837            // There are up to four attempts here; the two URLs that we're supposed to try per the
838            // specification, and up to one redirect for each (handled in postAutodiscover)
839            // Note: The expectation is that, of these four attempts, only a single server will
840            // actually be identified as the autodiscover server.  For the identified server,
841            // we may also try a 2nd connection with a different format (bare name).
842
843            // Try the domain first and see if we can get a response
844            HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
845            setHeaders(post, false);
846            post.setHeader("Content-Type", "text/xml");
847            post.setEntity(new StringEntity(req));
848            HttpClient client = getHttpClient(COMMAND_TIMEOUT);
849            EasResponse resp;
850            try {
851                resp = postAutodiscover(client, post, true /*canRetry*/);
852            } catch (IOException e1) {
853                userLog("IOException in autodiscover; trying alternate address");
854                // We catch the IOException here because we have an alternate address to try
855                post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
856                // If we fail here, we're out of options, so we let the outer try catch the
857                // IOException and return null
858                resp = postAutodiscover(client, post, true /*canRetry*/);
859            }
860
861            try {
862                // Get the "final" code; if it's not 200, just return null
863                int code = resp.getStatus();
864                userLog("Code: " + code);
865                if (code != HttpStatus.SC_OK) return null;
866
867                InputStream is = resp.getInputStream();
868                // The response to Autodiscover is regular XML (not WBXML)
869                // If we ever get an error in this process, we'll just punt and return null
870                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
871                XmlPullParser parser = factory.newPullParser();
872                parser.setInput(is, "UTF-8");
873                int type = parser.getEventType();
874                if (type == XmlPullParser.START_DOCUMENT) {
875                    type = parser.next();
876                    if (type == XmlPullParser.START_TAG) {
877                        String name = parser.getName();
878                        if (name.equals("Autodiscover")) {
879                            hostAuth = new HostAuth();
880                            parseAutodiscover(parser, hostAuth);
881                            // On success, we'll have a server address and login
882                            if (hostAuth.mAddress != null) {
883                                // Fill in the rest of the HostAuth
884                                // We use the user name and password that were successful during
885                                // the autodiscover process
886                                hostAuth.mLogin = mUserName;
887                                hostAuth.mPassword = mPassword;
888                                hostAuth.mPort = 443;
889                                hostAuth.mProtocol = "eas";
890                                hostAuth.mFlags =
891                                    HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
892                                bundle.putParcelable(
893                                        EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
894                            } else {
895                                bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
896                                        MessagingException.UNSPECIFIED_EXCEPTION);
897                            }
898                        }
899                    }
900                }
901            } catch (XmlPullParserException e1) {
902                // This would indicate an I/O error of some sort
903                // We will simply return null and user can configure manually
904            } finally {
905               resp.close();
906            }
907        // There's no reason at all for exceptions to be thrown, and it's ok if so.
908        // We just won't do auto-discover; user can configure manually
909       } catch (IllegalArgumentException e) {
910             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
911                     MessagingException.UNSPECIFIED_EXCEPTION);
912       } catch (IllegalStateException e) {
913            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
914                    MessagingException.UNSPECIFIED_EXCEPTION);
915       } catch (IOException e) {
916            userLog("IOException in Autodiscover", e);
917            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
918                    MessagingException.IOERROR);
919        } catch (MessagingException e) {
920            bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
921                    MessagingException.AUTHENTICATION_FAILED);
922        }
923        return bundle;
924    }
925
926    void parseServer(XmlPullParser parser, HostAuth hostAuth)
927            throws XmlPullParserException, IOException {
928        boolean mobileSync = false;
929        while (true) {
930            int type = parser.next();
931            if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
932                break;
933            } else if (type == XmlPullParser.START_TAG) {
934                String name = parser.getName();
935                if (name.equals("Type")) {
936                    if (parser.nextText().equals("MobileSync")) {
937                        mobileSync = true;
938                    }
939                } else if (mobileSync && name.equals("Url")) {
940                    String url = parser.nextText().toLowerCase();
941                    // This will look like https://<server address>/Microsoft-Server-ActiveSync
942                    // We need to extract the <server address>
943                    if (url.startsWith("https://") &&
944                            url.endsWith("/microsoft-server-activesync")) {
945                        int lastSlash = url.lastIndexOf('/');
946                        hostAuth.mAddress = url.substring(8, lastSlash);
947                        userLog("Autodiscover, server: " + hostAuth.mAddress);
948                    }
949                }
950            }
951        }
952    }
953
954    void parseSettings(XmlPullParser parser, HostAuth hostAuth)
955            throws XmlPullParserException, IOException {
956        while (true) {
957            int type = parser.next();
958            if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
959                break;
960            } else if (type == XmlPullParser.START_TAG) {
961                String name = parser.getName();
962                if (name.equals("Server")) {
963                    parseServer(parser, hostAuth);
964                }
965            }
966        }
967    }
968
969    void parseAction(XmlPullParser parser, HostAuth hostAuth)
970            throws XmlPullParserException, IOException {
971        while (true) {
972            int type = parser.next();
973            if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
974                break;
975            } else if (type == XmlPullParser.START_TAG) {
976                String name = parser.getName();
977                if (name.equals("Error")) {
978                    // Should parse the error
979                } else if (name.equals("Redirect")) {
980                    Log.d(TAG, "Redirect: " + parser.nextText());
981                } else if (name.equals("Settings")) {
982                    parseSettings(parser, hostAuth);
983                }
984            }
985        }
986    }
987
988    void parseUser(XmlPullParser parser, HostAuth hostAuth)
989            throws XmlPullParserException, IOException {
990        while (true) {
991            int type = parser.next();
992            if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
993                break;
994            } else if (type == XmlPullParser.START_TAG) {
995                String name = parser.getName();
996                if (name.equals("EMailAddress")) {
997                    String addr = parser.nextText();
998                    userLog("Autodiscover, email: " + addr);
999                } else if (name.equals("DisplayName")) {
1000                    String dn = parser.nextText();
1001                    userLog("Autodiscover, user: " + dn);
1002                }
1003            }
1004        }
1005    }
1006
1007    void parseResponse(XmlPullParser parser, HostAuth hostAuth)
1008            throws XmlPullParserException, IOException {
1009        while (true) {
1010            int type = parser.next();
1011            if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
1012                break;
1013            } else if (type == XmlPullParser.START_TAG) {
1014                String name = parser.getName();
1015                if (name.equals("User")) {
1016                    parseUser(parser, hostAuth);
1017                } else if (name.equals("Action")) {
1018                    parseAction(parser, hostAuth);
1019                }
1020            }
1021        }
1022    }
1023
1024    void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
1025            throws XmlPullParserException, IOException {
1026        while (true) {
1027            int type = parser.nextTag();
1028            if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
1029                break;
1030            } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
1031                parseResponse(parser, hostAuth);
1032            }
1033        }
1034    }
1035
1036    /**
1037     * Contact the GAL and obtain a list of matching accounts
1038     * @param context caller's context
1039     * @param accountId the account Id to search
1040     * @param filter the characters entered so far
1041     * @return a result record or null for no data
1042     *
1043     * TODO: shorter timeout for interactive lookup
1044     * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
1045     * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
1046     */
1047    static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
1048        Account acct = Account.restoreAccountWithId(context, accountId);
1049        if (acct != null) {
1050            EasSyncService svc = setupServiceForAccount(context, acct);
1051            if (svc == null) return null;
1052            try {
1053                Serializer s = new Serializer();
1054                s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
1055                s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
1056                s.start(Tags.SEARCH_OPTIONS);
1057                s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
1058                s.end().end().end().done();
1059                EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
1060                try {
1061                    int code = resp.getStatus();
1062                    if (code == HttpStatus.SC_OK) {
1063                        InputStream is = resp.getInputStream();
1064                        try {
1065                            GalParser gp = new GalParser(is, svc);
1066                            if (gp.parse()) {
1067                                return gp.getGalResult();
1068                            }
1069                        } finally {
1070                            is.close();
1071                        }
1072                    } else {
1073                        svc.userLog("GAL lookup returned " + code);
1074                    }
1075                } finally {
1076                    resp.close();
1077                }
1078            } catch (IOException e) {
1079                // GAL is non-critical; we'll just go on
1080                svc.userLog("GAL lookup exception " + e);
1081            }
1082        }
1083        return null;
1084    }
1085
1086    private void doStatusCallback(long messageId, long attachmentId, int status) {
1087        try {
1088            ExchangeService.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
1089        } catch (RemoteException e) {
1090            // No danger if the client is no longer around
1091        }
1092    }
1093
1094    private void doProgressCallback(long messageId, long attachmentId, int progress) {
1095        try {
1096            ExchangeService.callback().loadAttachmentStatus(messageId, attachmentId,
1097                    EmailServiceStatus.IN_PROGRESS, progress);
1098        } catch (RemoteException e) {
1099            // No danger if the client is no longer around
1100        }
1101    }
1102
1103    /**
1104     * Loads an attachment, based on the PartRequest passed in.  The PartRequest is basically our
1105     * wrapper for Attachment
1106     * @param req the part (attachment) to be retrieved
1107     * @throws IOException
1108     */
1109    protected void loadAttachment(PartRequest req) throws IOException {
1110        Attachment att = req.mAttachment;
1111        Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
1112        if (msg == null) {
1113            doStatusCallback(att.mMessageKey, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
1114            return;
1115        } else {
1116            doProgressCallback(msg.mId, att.mId, 0);
1117        }
1118
1119        String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
1120        EasResponse resp = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
1121
1122        try {
1123            int status = resp.getStatus();
1124            if (status == HttpStatus.SC_OK) {
1125                int len = (int)resp.getLength();
1126                InputStream is = resp.getInputStream();
1127                OutputStream os = null;
1128                Uri attachmentUri = AttachmentUtilities.getAttachmentUri(att.mAccountKey, att.mId);
1129                String fileName = null;
1130                try {
1131                    os = mContentResolver.openOutputStream(attachmentUri);
1132                    userLog("Attachment filename retrieved as: " + fileName);
1133                } catch (FileNotFoundException e) {
1134                    userLog("Can't get attachment; write file not found?");
1135                }
1136                if (os != null) {
1137                    // len > 0 means that Content-Length was set in the headers
1138                    // len < 0 means "chunked" transfer-encoding
1139                    if (len != 0) {
1140                        try {
1141                            mPendingRequest = req;
1142                            byte[] bytes = new byte[CHUNK_SIZE];
1143                            int length = len;
1144                            // Loop terminates 1) when EOF is reached or 2) IOException occurs
1145                            // One of these is guaranteed to occur
1146                            int totalRead = 0;
1147                            userLog("Attachment content-length: ", len);
1148                            while (true) {
1149                                int read = is.read(bytes, 0, CHUNK_SIZE);
1150
1151                                // read < 0 means that EOF was reached
1152                                if (read < 0) {
1153                                    userLog("Attachment load reached EOF, totalRead: ", totalRead);
1154                                    break;
1155                                }
1156
1157                                // Keep track of how much we've read for progress callback
1158                                totalRead += read;
1159
1160                                // Write these bytes out
1161                                os.write(bytes, 0, read);
1162
1163                                // We can't report percentages if this is chunked; the
1164                                // length of incoming data is unknown
1165                                if (length > 0) {
1166                                    // Belt and suspenders check to prevent runaway reading
1167                                    if (totalRead > length) {
1168                                        errorLog("totalRead is greater than attachment length?");
1169                                        break;
1170                                    }
1171                                    int pct = (totalRead * 100) / length;
1172                                    doProgressCallback(msg.mId, att.mId, pct);
1173                                }
1174                            }
1175                        } finally {
1176                            mPendingRequest = null;
1177                        }
1178                    }
1179                    os.flush();
1180                    os.close();
1181
1182                    // EmailProvider will throw an exception if update an unsaved attachment
1183                    if (att.isSaved()) {
1184                        ContentValues cv = new ContentValues();
1185                        cv.put(AttachmentColumns.CONTENT_URI, attachmentUri.toString());
1186                        att.update(mContext, cv);
1187                        doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
1188                    }
1189                }
1190            } else {
1191                doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
1192            }
1193        } finally {
1194            resp.close();
1195        }
1196    }
1197
1198    /**
1199     * Send an email responding to a Message that has been marked as a meeting request.  The message
1200     * will consist a little bit of event information and an iCalendar attachment
1201     * @param msg the meeting request email
1202     */
1203    private void sendMeetingResponseMail(Message msg, int response) {
1204        // Get the meeting information; we'd better have some...
1205        if (msg.mMeetingInfo == null) return;
1206        PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
1207
1208        // This will come as "First Last" <box@server.blah>, so we use Address to
1209        // parse it into parts; we only need the email address part for the ics file
1210        Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
1211        // It shouldn't be possible, but handle it anyway
1212        if (addrs.length != 1) return;
1213        String organizerEmail = addrs[0].getAddress();
1214
1215        String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
1216        String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
1217        String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
1218
1219        // What we're doing here is to create an Entity that looks like an Event as it would be
1220        // stored by CalendarProvider
1221        ContentValues entityValues = new ContentValues();
1222        Entity entity = new Entity(entityValues);
1223
1224        // Fill in times, location, title, and organizer
1225        entityValues.put("DTSTAMP",
1226                CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
1227        entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
1228        entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
1229        entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
1230        entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
1231        entityValues.put(Events.ORGANIZER, organizerEmail);
1232
1233        // Add ourselves as an attendee, using our account email address
1234        ContentValues attendeeValues = new ContentValues();
1235        attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1236                Attendees.RELATIONSHIP_ATTENDEE);
1237        attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
1238        entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
1239
1240        // Add the organizer
1241        ContentValues organizerValues = new ContentValues();
1242        organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
1243                Attendees.RELATIONSHIP_ORGANIZER);
1244        organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
1245        entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
1246
1247        // Create a message from the Entity we've built.  The message will have fields like
1248        // to, subject, date, and text filled in.  There will also be an "inline" attachment
1249        // which is in iCalendar format
1250        int flag;
1251        switch(response) {
1252            case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
1253                flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
1254                break;
1255            case EmailServiceConstants.MEETING_REQUEST_DECLINED:
1256                flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
1257                break;
1258            case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
1259            default:
1260                flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
1261                break;
1262        }
1263        Message outgoingMsg =
1264            CalendarUtilities.createMessageForEntity(mContext, entity, flag,
1265                    meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
1266        // Assuming we got a message back (we might not if the event has been deleted), send it
1267        if (outgoingMsg != null) {
1268            EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
1269        }
1270    }
1271
1272    /**
1273     * Responds to a move request.  The MessageMoveRequest is basically our
1274     * wrapper for the MoveItems service call
1275     * @param req the request (message id and "to" mailbox id)
1276     * @throws IOException
1277     */
1278    protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
1279        // Retrieve the message and mailbox; punt if either are null
1280        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
1281        if (msg == null) return;
1282        Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
1283                msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
1284        Mailbox srcMailbox = null;
1285        try {
1286            if (!c.moveToNext()) return;
1287            srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
1288        } finally {
1289            c.close();
1290        }
1291        if (srcMailbox == null) return;
1292        Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
1293        if (dstMailbox == null) return;
1294        Serializer s = new Serializer();
1295        s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
1296        s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
1297        s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
1298        s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
1299        s.end().end().done();
1300        EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
1301        try {
1302            int status = resp.getStatus();
1303            if (status == HttpStatus.SC_OK) {
1304                if (!resp.isEmpty()) {
1305                    InputStream is = resp.getInputStream();
1306                    MoveItemsParser p = new MoveItemsParser(is, this);
1307                    p.parse();
1308                    int statusCode = p.getStatusCode();
1309                    ContentValues cv = new ContentValues();
1310                    if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
1311                        // Restore the old mailbox id
1312                        cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
1313                        mContentResolver.update(
1314                                ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
1315                                cv, null, null);
1316                    } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
1317                        // Update with the new server id
1318                        cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
1319                        cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
1320                        mContentResolver.update(
1321                                ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
1322                                cv, null, null);
1323                    }
1324                    if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
1325                            || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
1326                        // If we revert or succeed, we no longer need the update information
1327                        // OR the now-duplicate email (the new copy will be synced down)
1328                        mContentResolver.delete(ContentUris.withAppendedId(
1329                                Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
1330                    } else {
1331                        // In this case, we're retrying, so do nothing.  The request will be
1332                        // handled next sync
1333                    }
1334                }
1335            } else if (isAuthError(status)) {
1336                throw new EasAuthenticationException();
1337            } else {
1338                userLog("Move items request failed, code: " + status);
1339                throw new IOException();
1340            }
1341        } finally {
1342            resp.close();
1343        }
1344    }
1345
1346    /**
1347     * Responds to a meeting request.  The MeetingResponseRequest is basically our
1348     * wrapper for the meetingResponse service call
1349     * @param req the request (message id and response code)
1350     * @throws IOException
1351     */
1352    protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
1353        // Retrieve the message and mailbox; punt if either are null
1354        Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
1355        if (msg == null) return;
1356        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
1357        if (mailbox == null) return;
1358        Serializer s = new Serializer();
1359        s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
1360        s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
1361        s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId);
1362        s.data(Tags.MREQ_REQ_ID, msg.mServerId);
1363        s.end().end().done();
1364        EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
1365        try {
1366            int status = resp.getStatus();
1367            if (status == HttpStatus.SC_OK) {
1368                if (!resp.isEmpty()) {
1369                    InputStream is = resp.getInputStream();
1370                    new MeetingResponseParser(is, this).parse();
1371                    String meetingInfo = msg.mMeetingInfo;
1372                    if (meetingInfo != null) {
1373                        String responseRequested = new PackedString(meetingInfo).get(
1374                                MeetingInfo.MEETING_RESPONSE_REQUESTED);
1375                        // If there's no tag, or a non-zero tag, we send the response mail
1376                        if ("0".equals(responseRequested)) {
1377                            return;
1378                        }
1379                    }
1380                    sendMeetingResponseMail(msg, req.mResponse);
1381                }
1382            } else if (isAuthError(status)) {
1383                throw new EasAuthenticationException();
1384            } else {
1385                userLog("Meeting response request failed, code: " + status);
1386                throw new IOException();
1387            }
1388        } finally {
1389            resp.close();
1390       }
1391    }
1392
1393    /**
1394     * Using mUserName and mPassword, create and cache mAuthString and mCacheString, which are used
1395     * in all HttpPost commands.  This should be called if these strings are null, or if mUserName
1396     * and/or mPassword are changed
1397     */
1398    private void cacheAuthAndCmdString() {
1399        String safeUserName = Uri.encode(mUserName);
1400        String cs = mUserName + ':' + mPassword;
1401        mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
1402        mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
1403            "&DeviceType=" + DEVICE_TYPE;
1404    }
1405
1406    /*package*/ String makeUriString(String cmd, String extra) throws IOException {
1407        // Cache the authentication string and the command string
1408        if (mAuthString == null || mCmdString == null) {
1409            cacheAuthAndCmdString();
1410        }
1411        String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
1412            "/Microsoft-Server-ActiveSync";
1413        if (cmd != null) {
1414            us += "?Cmd=" + cmd + mCmdString;
1415        }
1416        if (extra != null) {
1417            us += extra;
1418        }
1419        return us;
1420    }
1421
1422    /**
1423     * Set standard HTTP headers, using a policy key if required
1424     * @param method the method we are going to send
1425     * @param usePolicyKey whether or not a policy key should be sent in the headers
1426     */
1427    /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
1428        method.setHeader("Authorization", mAuthString);
1429        method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
1430        method.setHeader("Connection", "keep-alive");
1431        method.setHeader("User-Agent", USER_AGENT);
1432        method.setHeader("Accept-Encoding", "gzip");
1433        if (usePolicyKey) {
1434            // If there's an account in existence, use its key; otherwise (we're creating the
1435            // account), send "0".  The server will respond with code 449 if there are policies
1436            // to be enforced
1437            String key = "0";
1438            if (mAccount != null) {
1439                String accountKey = mAccount.mSecuritySyncKey;
1440                if (!TextUtils.isEmpty(accountKey)) {
1441                    key = accountKey;
1442                }
1443            }
1444            method.setHeader("X-MS-PolicyKey", key);
1445        }
1446    }
1447
1448    private ClientConnectionManager getClientConnectionManager() {
1449        return ExchangeService.getClientConnectionManager();
1450    }
1451
1452    private HttpClient getHttpClient(int timeout) {
1453        HttpParams params = new BasicHttpParams();
1454        HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
1455        HttpConnectionParams.setSoTimeout(params, timeout);
1456        HttpConnectionParams.setSocketBufferSize(params, 8192);
1457        HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
1458        return client;
1459    }
1460
1461    protected EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
1462        return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
1463    }
1464
1465    protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
1466        return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
1467    }
1468
1469    protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
1470       Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
1471       if (Eas.USER_LOG) {
1472           userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
1473       }
1474       return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
1475    }
1476
1477    /**
1478     * Convenience method for executePostWithTimeout for use other than with the Ping command
1479     */
1480    protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
1481            throws IOException {
1482        return executePostWithTimeout(client, method, timeout, false);
1483    }
1484
1485    /**
1486     * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
1487     * @param client the HttpClient
1488     * @param method the HttpPost
1489     * @param timeout the timeout before failure, in ms
1490     * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
1491     * @return the HttpResponse
1492     * @throws IOException
1493     */
1494    protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout,
1495            boolean isPingCommand) throws IOException {
1496        synchronized(getSynchronizer()) {
1497            mPendingPost = method;
1498            long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
1499            if (isPingCommand) {
1500                ExchangeService.runAsleep(mMailboxId, alarmTime);
1501            } else {
1502                ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
1503            }
1504        }
1505        try {
1506            return new EasResponse(client.execute(method));
1507        } finally {
1508            synchronized(getSynchronizer()) {
1509                if (isPingCommand) {
1510                    ExchangeService.runAwake(mMailboxId);
1511                } else {
1512                    ExchangeService.clearWatchdogAlarm(mMailboxId);
1513                }
1514                mPendingPost = null;
1515            }
1516        }
1517    }
1518
1519    protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
1520            throws IOException {
1521        HttpClient client = getHttpClient(timeout);
1522        boolean isPingCommand = cmd.equals(PING_COMMAND);
1523
1524        // Split the mail sending commands
1525        String extra = null;
1526        boolean msg = false;
1527        if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
1528            int cmdLength = cmd.indexOf('&');
1529            extra = cmd.substring(cmdLength);
1530            cmd = cmd.substring(0, cmdLength);
1531            msg = true;
1532        } else if (cmd.startsWith("SendMail&")) {
1533            msg = true;
1534        }
1535
1536        String us = makeUriString(cmd, extra);
1537        HttpPost method = new HttpPost(URI.create(us));
1538        // Send the proper Content-Type header
1539        // If entity is null (e.g. for attachments), don't set this header
1540        if (msg) {
1541            method.setHeader("Content-Type", "message/rfc822");
1542        } else if (entity != null) {
1543            method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
1544        }
1545        setHeaders(method, !cmd.equals(PING_COMMAND));
1546        method.setEntity(entity);
1547        return executePostWithTimeout(client, method, timeout, isPingCommand);
1548    }
1549
1550    protected EasResponse sendHttpClientOptions() throws IOException {
1551        HttpClient client = getHttpClient(COMMAND_TIMEOUT);
1552        String us = makeUriString("OPTIONS", null);
1553        HttpOptions method = new HttpOptions(URI.create(us));
1554        setHeaders(method, false);
1555        return new EasResponse(client.execute(method));
1556    }
1557
1558    String getTargetCollectionClassFromCursor(Cursor c) {
1559        int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
1560        if (type == Mailbox.TYPE_CONTACTS) {
1561            return "Contacts";
1562        } else if (type == Mailbox.TYPE_CALENDAR) {
1563            return "Calendar";
1564        } else {
1565            return "Email";
1566        }
1567    }
1568
1569    /**
1570     * Negotiate provisioning with the server.  First, get policies form the server and see if
1571     * the policies are supported by the device.  Then, write the policies to the account and
1572     * tell SecurityPolicy that we have policies in effect.  Finally, see if those policies are
1573     * active; if so, acknowledge the policies to the server and get a final policy key that we
1574     * use in future EAS commands and write this key to the account.
1575     * @return whether or not provisioning has been successful
1576     * @throws IOException
1577     */
1578    private boolean tryProvision() throws IOException {
1579        // First, see if provisioning is even possible, i.e. do we support the policies required
1580        // by the server
1581        ProvisionParser pp = canProvision();
1582        if (pp != null) {
1583            // Get the policies from ProvisionParser
1584            Policy policy = pp.getPolicy();
1585            // Update the account with a null policyKey (the key we've gotten is
1586            // temporary and cannot be used for syncing)
1587            Policy.clearAccountPolicy(mContext, mAccount);
1588            // Make sure that SecurityPolicy is up-to-date
1589            SecurityPolicyDelegate.policiesUpdated(mContext, mAccount.mId);
1590            if (pp.getRemoteWipe()) {
1591                // We've gotten a remote wipe command
1592                ExchangeService.alwaysLog("!!! Remote wipe request received");
1593                // Start by setting the account to security hold
1594                SecurityPolicyDelegate.setAccountHoldFlag(mContext, mAccount, true);
1595                // Force a stop to any running syncs for this account (except this one)
1596                ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
1597
1598                // If we're not the admin, we can't do the wipe, so just return
1599                if (!SecurityPolicyDelegate.isActiveAdmin(mContext)) {
1600                    ExchangeService.alwaysLog("!!! Not device admin; can't wipe");
1601                    return false;
1602                }
1603
1604                // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
1605                // we wipe the device regardless of any errors in acknowledgment
1606                try {
1607                    ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
1608                    acknowledgeRemoteWipe(pp.getSecuritySyncKey());
1609                } catch (Exception e) {
1610                    // Because remote wipe is such a high priority task, we don't want to
1611                    // circumvent it if there's an exception in acknowledgment
1612                }
1613                // Then, tell SecurityPolicy to wipe the device
1614                ExchangeService.alwaysLog("!!! Executing remote wipe");
1615                SecurityPolicyDelegate.remoteWipe(mContext);
1616                return false;
1617            } else if (SecurityPolicyDelegate.isActive(mContext, policy)) {
1618                // See if the required policies are in force; if they are, acknowledge the policies
1619                // to the server and get the final policy key
1620                String securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
1621                        PROVISION_STATUS_OK);
1622                if (securitySyncKey != null) {
1623                    // Write the final policy key to the Account and say we've been successful
1624                    policy.setAccountPolicy(mContext, mAccount, securitySyncKey);
1625                    // Release any mailboxes that might be in a security hold
1626                    ExchangeService.releaseSecurityHold(mAccount);
1627                    return true;
1628                }
1629            } else {
1630                // Notify that we are blocked because of policies
1631                SecurityPolicyDelegate.policiesRequired(mContext, mAccount.mId);
1632            }
1633        }
1634        return false;
1635    }
1636
1637    private String getPolicyType() {
1638        return (mProtocolVersionDouble >=
1639            Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
1640    }
1641
1642    /**
1643     * Obtain a set of policies from the server and determine whether those policies are supported
1644     * by the device.
1645     * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
1646     * @throws IOException
1647     */
1648    private ProvisionParser canProvision() throws IOException {
1649        Serializer s = new Serializer();
1650        s.start(Tags.PROVISION_PROVISION);
1651        if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
1652            // Send settings information in 14.0 and greater
1653            s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
1654            s.data(Tags.SETTINGS_MODEL, Build.MODEL);
1655            //s.data(Tags.SETTINGS_IMEI, "");
1656            //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
1657            s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
1658            //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
1659            //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
1660            //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
1661            s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
1662            s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
1663        }
1664        s.start(Tags.PROVISION_POLICIES);
1665        s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()).end();
1666        s.end();  // PROVISION_POLICIES
1667        s.end().done(); // PROVISION_PROVISION
1668        EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
1669        try {
1670            int code = resp.getStatus();
1671            if (code == HttpStatus.SC_OK) {
1672                InputStream is = resp.getInputStream();
1673                ProvisionParser pp = new ProvisionParser(is, this);
1674                if (pp.parse()) {
1675                    // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
1676                    // policies.  If others are required, hasSupportablePolicySet will be false
1677                    if (!pp.hasSupportablePolicySet())  {
1678                        // Try to acknowledge using the "partial" status (i.e. we can partially
1679                        // accommodate the required policies).  The server will agree to this if the
1680                        // "allow non-provisionable devices" setting is enabled on the server
1681                        String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
1682                                PROVISION_STATUS_PARTIAL);
1683                        // Return either the parser (success) or null (failure)
1684                        if (policyKey != null) {
1685                            pp.clearUnsupportedPolicies();
1686                        }
1687                    }
1688                    return pp;
1689                }
1690            }
1691        } finally {
1692            resp.close();
1693        }
1694        // On failures, simply return null
1695        return null;
1696    }
1697
1698    /**
1699     * Acknowledge that we support the policies provided by the server, and that these policies
1700     * are in force.
1701     * @param tempKey the initial (temporary) policy key sent by the server
1702     * @return the final policy key, which can be used for syncing
1703     * @throws IOException
1704     */
1705    private void acknowledgeRemoteWipe(String tempKey) throws IOException {
1706        acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
1707    }
1708
1709    private String acknowledgeProvision(String tempKey, String result) throws IOException {
1710        return acknowledgeProvisionImpl(tempKey, result, false);
1711    }
1712
1713    private String acknowledgeProvisionImpl(String tempKey, String status,
1714            boolean remoteWipe) throws IOException {
1715        Serializer s = new Serializer();
1716        s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
1717        s.start(Tags.PROVISION_POLICY);
1718
1719        // Use the proper policy type, depending on EAS version
1720        s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
1721
1722        s.data(Tags.PROVISION_POLICY_KEY, tempKey);
1723        s.data(Tags.PROVISION_STATUS, status);
1724        s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
1725        if (remoteWipe) {
1726            s.start(Tags.PROVISION_REMOTE_WIPE);
1727            s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
1728            s.end();
1729        }
1730        s.end().done(); // PROVISION_PROVISION
1731        EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
1732        try {
1733            int code = resp.getStatus();
1734            if (code == HttpStatus.SC_OK) {
1735                InputStream is = resp.getInputStream();
1736                ProvisionParser pp = new ProvisionParser(is, this);
1737                if (pp.parse()) {
1738                    // Return the final policy key from the ProvisionParser
1739                    return pp.getSecuritySyncKey();
1740                }
1741            }
1742        } finally {
1743            resp.close();
1744        }
1745        // On failures, return null
1746        return null;
1747    }
1748
1749    /**
1750     * Translate exit status code to service status code (used in callbacks)
1751     * @param exitStatus the service's exit status
1752     * @return the corresponding service status
1753     */
1754    private int exitStatusToServiceStatus(int exitStatus) {
1755        switch(exitStatus) {
1756            case EXIT_SECURITY_FAILURE:
1757                return EmailServiceStatus.SECURITY_FAILURE;
1758            case EXIT_LOGIN_FAILURE:
1759                return EmailServiceStatus.LOGIN_FAILED;
1760            default:
1761                return EmailServiceStatus.SUCCESS;
1762        }
1763    }
1764
1765    /**
1766     * Performs FolderSync
1767     *
1768     * @throws IOException
1769     * @throws EasParserException
1770     */
1771    public void runAccountMailbox() throws IOException, EasParserException {
1772        // Check that the account's mailboxes are consistent
1773        MailboxUtilities.checkMailboxConsistency(mContext, mAccount.mId);
1774        // Initialize exit status to success
1775        mExitStatus = EXIT_DONE;
1776        try {
1777            try {
1778                ExchangeService.callback()
1779                    .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
1780            } catch (RemoteException e1) {
1781                // Don't care if this fails
1782            }
1783
1784            // Don't run if we're being held
1785            if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
1786                mExitStatus = EXIT_SECURITY_FAILURE;
1787                return;
1788            }
1789
1790            if (mAccount.mSyncKey == null) {
1791                mAccount.mSyncKey = "0";
1792                userLog("Account syncKey INIT to 0");
1793                ContentValues cv = new ContentValues();
1794                cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
1795                mAccount.update(mContext, cv);
1796            }
1797
1798            boolean firstSync = mAccount.mSyncKey.equals("0");
1799            if (firstSync) {
1800                userLog("Initial FolderSync");
1801            }
1802
1803            // When we first start up, change all mailboxes to push.
1804            ContentValues cv = new ContentValues();
1805            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1806            if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1807                    WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
1808                    new String[] {Long.toString(mAccount.mId)}) > 0) {
1809                ExchangeService.kick("change ping boxes to push");
1810            }
1811
1812            // Determine our protocol version, if we haven't already and save it in the Account
1813            // Also re-check protocol version at least once a day (in case of upgrade)
1814            if (mAccount.mProtocolVersion == null ||
1815                    ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) {
1816                userLog("Determine EAS protocol version");
1817                EasResponse resp = sendHttpClientOptions();
1818                try {
1819                    int code = resp.getStatus();
1820                    userLog("OPTIONS response: ", code);
1821                    if (code == HttpStatus.SC_OK) {
1822                        Header header = resp.getHeader("MS-ASProtocolCommands");
1823                        userLog(header.getValue());
1824                        header = resp.getHeader("ms-asprotocolversions");
1825                        try {
1826                            setupProtocolVersion(this, header);
1827                        } catch (MessagingException e) {
1828                            // Since we've already validated, this can't really happen
1829                            // But if it does, we'll rethrow this...
1830                            throw new IOException();
1831                        }
1832                        // Save the protocol version
1833                        cv.clear();
1834                        // Save the protocol version in the account; if we're using 12.0 or greater,
1835                        // set the flag for support of SmartForward
1836                        cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
1837                        if (mProtocolVersionDouble >= 12.0) {
1838                            cv.put(Account.FLAGS,
1839                                    mAccount.mFlags | Account.FLAGS_SUPPORTS_SMART_FORWARD);
1840                        }
1841                        mAccount.update(mContext, cv);
1842                        cv.clear();
1843                        // Save the sync time of the account mailbox to current time
1844                        cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
1845                        mMailbox.update(mContext, cv);
1846                     } else {
1847                        errorLog("OPTIONS command failed; throwing IOException");
1848                        throw new IOException();
1849                    }
1850                } finally {
1851                    resp.close();
1852                }
1853            }
1854
1855            // Change all pushable boxes to push when we start the account mailbox
1856            if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
1857                cv.clear();
1858                cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
1859                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1860                        ExchangeService.WHERE_IN_ACCOUNT_AND_PUSHABLE,
1861                        new String[] {Long.toString(mAccount.mId)}) > 0) {
1862                    userLog("Push account; set pushable boxes to push...");
1863                }
1864            }
1865
1866            while (!mStop) {
1867                userLog("Sending Account syncKey: ", mAccount.mSyncKey);
1868                Serializer s = new Serializer();
1869                s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
1870                    .text(mAccount.mSyncKey).end().end().done();
1871                EasResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
1872                try {
1873                    if (mStop) break;
1874                    int code = resp.getStatus();
1875                    if (code == HttpStatus.SC_OK) {
1876                        if (!resp.isEmpty()) {
1877                            InputStream is = resp.getInputStream();
1878                            // Returns true if we need to sync again
1879                            if (new FolderSyncParser(is, new AccountSyncAdapter(this)).parse()) {
1880                                continue;
1881                            }
1882                        }
1883                    } else if (isProvisionError(code)) {
1884                        throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
1885                    } else if (isAuthError(code)) {
1886                        mExitStatus = EXIT_LOGIN_FAILURE;
1887                        return;
1888                    } else {
1889                        userLog("FolderSync response error: ", code);
1890                    }
1891                } finally {
1892                    resp.close();
1893                }
1894
1895                // Change all push/hold boxes to push
1896                cv.clear();
1897                cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
1898                if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
1899                        WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
1900                        new String[] {Long.toString(mAccount.mId)}) > 0) {
1901                    userLog("Set push/hold boxes to push...");
1902                }
1903
1904                try {
1905                    ExchangeService.callback()
1906                        .syncMailboxListStatus(mAccount.mId, exitStatusToServiceStatus(mExitStatus),
1907                                0);
1908                } catch (RemoteException e1) {
1909                    // Don't care if this fails
1910                }
1911
1912                // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's
1913                // active; otherwise, clear out the key/flag.  This should cause a provisioning
1914                // error on the next POST, and start the security sequence over again
1915                String key = mAccount.mSecuritySyncKey;
1916                if (!TextUtils.isEmpty(key)) {
1917                    Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
1918                    if (!SecurityPolicyDelegate.isActive(mContext, policy)) {
1919                        cv.clear();
1920                        cv.put(AccountColumns.SECURITY_FLAGS, 0);
1921                        cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
1922                        long accountId = mAccount.mId;
1923                        mContentResolver.update(ContentUris.withAppendedId(
1924                                Account.CONTENT_URI, accountId), cv, null, null);
1925                        SecurityPolicyDelegate.policiesRequired(mContext, accountId);
1926                    }
1927                }
1928
1929                // Wait for push notifications.
1930                String threadName = Thread.currentThread().getName();
1931                try {
1932                    runPingLoop();
1933                } catch (StaleFolderListException e) {
1934                    // We break out if we get told about a stale folder list
1935                    userLog("Ping interrupted; folder list requires sync...");
1936                } catch (IllegalHeartbeatException e) {
1937                    // If we're sending an illegal heartbeat, reset either the min or the max to
1938                    // that heartbeat
1939                    resetHeartbeats(e.mLegalHeartbeat);
1940                } finally {
1941                    Thread.currentThread().setName(threadName);
1942                }
1943            }
1944        } catch (CommandStatusException e) {
1945            // If the sync error is a provisioning failure (perhaps policies changed),
1946            // let's try the provisioning procedure
1947            // Provisioning must only be attempted for the account mailbox - trying to
1948            // provision any other mailbox may result in race conditions and the
1949            // creation of multiple policy keys.
1950            int status = e.mStatus;
1951            if (CommandStatus.isNeedsProvisioning(status)) {
1952                if (!tryProvision()) {
1953                    // Set the appropriate failure status
1954                    mExitStatus = EXIT_SECURITY_FAILURE;
1955                    return;
1956                }
1957            } else if (CommandStatus.isDeniedAccess(status)) {
1958                mExitStatus = EXIT_ACCESS_DENIED;
1959                return;
1960            } else {
1961                userLog("Unexpected status: " + CommandStatus.toString(status));
1962                mExitStatus = EXIT_EXCEPTION;
1963            }
1964        } catch (IOException e) {
1965            // We catch this here to send the folder sync status callback
1966            // A folder sync failed callback will get sent from run()
1967            try {
1968                if (!mStop) {
1969                    // NOTE: The correct status is CONNECTION_ERROR, but the UI displays this, and
1970                    // it's not really appropriate for EAS as this is not unexpected for a ping and
1971                    // connection errors are retried in any case
1972                    ExchangeService.callback()
1973                        .syncMailboxListStatus(mAccount.mId,
1974                                EmailServiceStatus.SUCCESS, 0);
1975                }
1976            } catch (RemoteException e1) {
1977                // Don't care if this fails
1978            }
1979            throw e;
1980        }
1981    }
1982
1983    /**
1984     * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal
1985     * @param legalHeartbeat a known legal heartbeat (from the EAS server)
1986     */
1987    /*package*/ void resetHeartbeats(int legalHeartbeat) {
1988        userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat);
1989        // We are here because the current heartbeat (mPingHeartbeat) is invalid.  Depending on
1990        // whether the argument is above or below the current heartbeat, we can infer the need to
1991        // change either the minimum or maximum heartbeat
1992        if (legalHeartbeat > mPingHeartbeat) {
1993            // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was
1994            // too low.  We respond by raising either or both of the minimum heartbeat or the
1995            // force heartbeat to the argument value
1996            if (mPingMinHeartbeat < legalHeartbeat) {
1997                mPingMinHeartbeat = legalHeartbeat;
1998            }
1999            if (mPingForceHeartbeat < legalHeartbeat) {
2000                mPingForceHeartbeat = legalHeartbeat;
2001            }
2002            // If our minimum is now greater than the max, bring them together
2003            if (mPingMinHeartbeat > mPingMaxHeartbeat) {
2004                mPingMaxHeartbeat = legalHeartbeat;
2005            }
2006        } else if (legalHeartbeat < mPingHeartbeat) {
2007            // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was
2008            // too high.  We respond by lowering the maximum to the argument value
2009            mPingMaxHeartbeat = legalHeartbeat;
2010            // If our maximum is now less than the minimum, bring them together
2011            if (mPingMaxHeartbeat < mPingMinHeartbeat) {
2012                mPingMinHeartbeat = legalHeartbeat;
2013            }
2014        }
2015        // Set current heartbeat to the legal heartbeat
2016        mPingHeartbeat = legalHeartbeat;
2017        // Allow the heartbeat logic to run
2018        mPingHeartbeatDropped = false;
2019    }
2020
2021    private void pushFallback(long mailboxId) {
2022        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
2023        if (mailbox == null) {
2024            return;
2025        }
2026        ContentValues cv = new ContentValues();
2027        int mins = PING_FALLBACK_PIM;
2028        if (mailbox.mType == Mailbox.TYPE_INBOX) {
2029            mins = PING_FALLBACK_INBOX;
2030        }
2031        cv.put(Mailbox.SYNC_INTERVAL, mins);
2032        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
2033                cv, null, null);
2034        errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
2035        ExchangeService.kick("push fallback");
2036    }
2037
2038    /**
2039     * Simplistic attempt to determine a NAT timeout, based on experience with various carriers
2040     * and networks.  The string "reset by peer" is very common in these situations, so we look for
2041     * that specifically.  We may add additional tests here as more is learned.
2042     * @param message
2043     * @return whether this message is likely associated with a NAT failure
2044     */
2045    private boolean isLikelyNatFailure(String message) {
2046        if (message == null) return false;
2047        if (message.contains("reset by peer")) {
2048            return true;
2049        }
2050        return false;
2051    }
2052
2053    private void runPingLoop() throws IOException, StaleFolderListException,
2054            IllegalHeartbeatException, CommandStatusException {
2055        int pingHeartbeat = mPingHeartbeat;
2056        userLog("runPingLoop");
2057        // Do push for all sync services here
2058        long endTime = System.currentTimeMillis() + (30*MINUTES);
2059        HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
2060        ArrayList<String> readyMailboxes = new ArrayList<String>();
2061        ArrayList<String> notReadyMailboxes = new ArrayList<String>();
2062        int pingWaitCount = 0;
2063        long inboxId = -1;
2064
2065        while ((System.currentTimeMillis() < endTime) && !mStop) {
2066            // Count of pushable mailboxes
2067            int pushCount = 0;
2068            // Count of mailboxes that can be pushed right now
2069            int canPushCount = 0;
2070            // Count of uninitialized boxes
2071            int uninitCount = 0;
2072
2073            Serializer s = new Serializer();
2074            Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
2075                    MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
2076                    AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
2077            notReadyMailboxes.clear();
2078            readyMailboxes.clear();
2079            // Look for an inbox, and remember its id
2080            if (inboxId == -1) {
2081                inboxId = Mailbox.findMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
2082            }
2083            try {
2084                // Loop through our pushed boxes seeing what is available to push
2085                while (c.moveToNext()) {
2086                    pushCount++;
2087                    // Two requirements for push:
2088                    // 1) ExchangeService tells us the mailbox is syncable (not running/not stopped)
2089                    // 2) The syncKey isn't "0" (i.e. it's synced at least once)
2090                    long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
2091                    int pingStatus = ExchangeService.pingStatus(mailboxId);
2092                    String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
2093                    if (pingStatus == ExchangeService.PING_STATUS_OK) {
2094                        String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
2095                        if ((syncKey == null) || syncKey.equals("0")) {
2096                            // We can't push until the initial sync is done
2097                            pushCount--;
2098                            uninitCount++;
2099                            continue;
2100                        }
2101
2102                        if (canPushCount++ == 0) {
2103                            // Initialize the Ping command
2104                            s.start(Tags.PING_PING)
2105                                .data(Tags.PING_HEARTBEAT_INTERVAL,
2106                                        Integer.toString(pingHeartbeat))
2107                                .start(Tags.PING_FOLDERS);
2108                        }
2109
2110                        String folderClass = getTargetCollectionClassFromCursor(c);
2111                        s.start(Tags.PING_FOLDER)
2112                            .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
2113                            .data(Tags.PING_CLASS, folderClass)
2114                            .end();
2115                        readyMailboxes.add(mailboxName);
2116                    } else if ((pingStatus == ExchangeService.PING_STATUS_RUNNING) ||
2117                            (pingStatus == ExchangeService.PING_STATUS_WAITING)) {
2118                        notReadyMailboxes.add(mailboxName);
2119                    } else if (pingStatus == ExchangeService.PING_STATUS_UNABLE) {
2120                        pushCount--;
2121                        userLog(mailboxName, " in error state; ignore");
2122                        continue;
2123                    }
2124                }
2125            } finally {
2126                c.close();
2127            }
2128
2129            if (Eas.USER_LOG) {
2130                if (!notReadyMailboxes.isEmpty()) {
2131                    userLog("Ping not ready for: " + notReadyMailboxes);
2132                }
2133                if (!readyMailboxes.isEmpty()) {
2134                    userLog("Ping ready for: " + readyMailboxes);
2135                }
2136            }
2137
2138            // If we've waited 10 seconds or more, just ping with whatever boxes are ready
2139            // But use a shorter than normal heartbeat
2140            boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
2141
2142            if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
2143                // If all pingable boxes are ready for push, send Ping to the server
2144                s.end().end().done();
2145                pingWaitCount = 0;
2146                mPostReset = false;
2147                mPostAborted = false;
2148
2149                // If we've been stopped, this is a good time to return
2150                if (mStop) return;
2151
2152                long pingTime = SystemClock.elapsedRealtime();
2153                try {
2154                    // Send the ping, wrapped by appropriate timeout/alarm
2155                    if (forcePing) {
2156                        userLog("Forcing ping after waiting for all boxes to be ready");
2157                    }
2158                    EasResponse resp =
2159                        sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
2160
2161                    try {
2162                        int code = resp.getStatus();
2163                        userLog("Ping response: ", code);
2164
2165                        // Return immediately if we've been asked to stop during the ping
2166                        if (mStop) {
2167                            userLog("Stopping pingLoop");
2168                            return;
2169                        }
2170
2171                        if (code == HttpStatus.SC_OK) {
2172                            // Make sure to clear out any pending sync errors
2173                            ExchangeService.removeFromSyncErrorMap(mMailboxId);
2174                            if (!resp.isEmpty()) {
2175                                InputStream is = resp.getInputStream();
2176                                int pingResult = parsePingResult(is, mContentResolver,
2177                                        pingErrorMap);
2178                                // If our ping completed (status = 1), and wasn't forced and we're
2179                                // not at the maximum, try increasing timeout by two minutes
2180                                if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
2181                                    if (pingHeartbeat > mPingHighWaterMark) {
2182                                        mPingHighWaterMark = pingHeartbeat;
2183                                        userLog("Setting high water mark at: ", mPingHighWaterMark);
2184                                    }
2185                                    if ((pingHeartbeat < mPingMaxHeartbeat) &&
2186                                            !mPingHeartbeatDropped) {
2187                                        pingHeartbeat += PING_HEARTBEAT_INCREMENT;
2188                                        if (pingHeartbeat > mPingMaxHeartbeat) {
2189                                            pingHeartbeat = mPingMaxHeartbeat;
2190                                        }
2191                                        userLog("Increase ping heartbeat to ", pingHeartbeat, "s");
2192                                    }
2193                                }
2194                            } else {
2195                                userLog("Ping returned empty result; throwing IOException");
2196                                throw new IOException();
2197                            }
2198                        } else if (isAuthError(code)) {
2199                            mExitStatus = EXIT_LOGIN_FAILURE;
2200                            userLog("Authorization error during Ping: ", code);
2201                            throw new IOException();
2202                        }
2203                    } finally {
2204                        resp.close();
2205                    }
2206                } catch (IOException e) {
2207                    String message = e.getMessage();
2208                    // If we get the exception that is indicative of a NAT timeout and if we
2209                    // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
2210                    boolean hasMessage = message != null;
2211                    userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
2212                    if (mPostReset) {
2213                        // Nothing to do in this case; this is ExchangeService telling us to try
2214                        // another ping.
2215                    } else if (mPostAborted || isLikelyNatFailure(message)) {
2216                        long pingLength = SystemClock.elapsedRealtime() - pingTime;
2217                        if ((pingHeartbeat > mPingMinHeartbeat) &&
2218                                (pingHeartbeat > mPingHighWaterMark)) {
2219                            pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
2220                            mPingHeartbeatDropped = true;
2221                            if (pingHeartbeat < mPingMinHeartbeat) {
2222                                pingHeartbeat = mPingMinHeartbeat;
2223                            }
2224                            userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
2225                        } else if (mPostAborted) {
2226                            // There's no point in throwing here; this can happen in two cases
2227                            // 1) An alarm, which indicates minutes without activity; no sense
2228                            //    backing off
2229                            // 2) ExchangeService abort, due to sync of mailbox.  Again, we want to
2230                            //    keep on trying to ping
2231                            userLog("Ping aborted; retry");
2232                        } else if (pingLength < 2000) {
2233                            userLog("Abort or NAT type return < 2 seconds; throwing IOException");
2234                            throw e;
2235                        } else {
2236                            userLog("NAT type IOException");
2237                        }
2238                    } else if (hasMessage && message.contains("roken pipe")) {
2239                        // The "broken pipe" error (uppercase or lowercase "b") seems to be an
2240                        // internal error, so let's not throw an exception (which leads to delays)
2241                        // but rather simply run through the loop again
2242                    } else {
2243                        throw e;
2244                    }
2245                }
2246            } else if (forcePing) {
2247                // In this case, there aren't any boxes that are pingable, but there are boxes
2248                // waiting (for IOExceptions)
2249                userLog("pingLoop waiting 60s for any pingable boxes");
2250                sleep(60*SECONDS, true);
2251            } else if (pushCount > 0) {
2252                // If we want to Ping, but can't just yet, wait a little bit
2253                // TODO Change sleep to wait and use notify from ExchangeService when a sync ends
2254                sleep(2*SECONDS, false);
2255                pingWaitCount++;
2256                //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
2257            } else if (uninitCount > 0) {
2258                // In this case, we're doing an initial sync of at least one mailbox.  Since this
2259                // is typically a one-time case, I'm ok with trying again every 10 seconds until
2260                // we're in one of the other possible states.
2261                userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
2262                sleep(10*SECONDS, true);
2263            } else if (inboxId == -1) {
2264                // In this case, we're still syncing mailboxes, so sleep for only a short time
2265                sleep(45*SECONDS, true);
2266            } else {
2267                // We've got nothing to do, so we'll check again in 20 minutes at which time
2268                // we'll update the folder list, check for policy changes and/or remote wipe, etc.
2269                // Let the device sleep in the meantime...
2270                userLog(ACCOUNT_MAILBOX_SLEEP_TEXT);
2271                sleep(ACCOUNT_MAILBOX_SLEEP_TIME, true);
2272            }
2273        }
2274
2275        // Save away the current heartbeat
2276        mPingHeartbeat = pingHeartbeat;
2277    }
2278
2279    private void sleep(long ms, boolean runAsleep) {
2280        if (runAsleep) {
2281            ExchangeService.runAsleep(mMailboxId, ms+(5*SECONDS));
2282        }
2283        try {
2284            Thread.sleep(ms);
2285        } catch (InterruptedException e) {
2286            // Doesn't matter whether we stop early; it's the thought that counts
2287        } finally {
2288            if (runAsleep) {
2289                ExchangeService.runAwake(mMailboxId);
2290            }
2291        }
2292    }
2293
2294    private int parsePingResult(InputStream is, ContentResolver cr,
2295            HashMap<String, Integer> errorMap)
2296            throws IOException, StaleFolderListException, IllegalHeartbeatException,
2297                CommandStatusException {
2298        PingParser pp = new PingParser(is, this);
2299        if (pp.parse()) {
2300            // True indicates some mailboxes need syncing...
2301            // syncList has the serverId's of the mailboxes...
2302            mBindArguments[0] = Long.toString(mAccount.mId);
2303            mPingChangeList = pp.getSyncList();
2304            for (String serverId: mPingChangeList) {
2305                mBindArguments[1] = serverId;
2306                Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
2307                        WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
2308                try {
2309                    if (c.moveToFirst()) {
2310
2311                        /**
2312                         * Check the boxes reporting changes to see if there really were any...
2313                         * We do this because bugs in various Exchange servers can put us into a
2314                         * looping behavior by continually reporting changes in a mailbox, even when
2315                         * there aren't any.
2316                         *
2317                         * This behavior is seemingly random, and therefore we must code defensively
2318                         * by backing off of push behavior when it is detected.
2319                         *
2320                         * One known cause, on certain Exchange 2003 servers, is acknowledged by
2321                         * Microsoft, and the server hotfix for this case can be found at
2322                         * http://support.microsoft.com/kb/923282
2323                         */
2324
2325                        // Check the status of the last sync
2326                        String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
2327                        int type = ExchangeService.getStatusType(status);
2328                        // This check should always be true...
2329                        if (type == ExchangeService.SYNC_PING) {
2330                            int changeCount = ExchangeService.getStatusChangeCount(status);
2331                            if (changeCount > 0) {
2332                                errorMap.remove(serverId);
2333                            } else if (changeCount == 0) {
2334                                // This means that a ping reported changes in error; we keep a count
2335                                // of consecutive errors of this kind
2336                                String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
2337                                Integer failures = errorMap.get(serverId);
2338                                if (failures == null) {
2339                                    userLog("Last ping reported changes in error for: ", name);
2340                                    errorMap.put(serverId, 1);
2341                                } else if (failures > MAX_PING_FAILURES) {
2342                                    // We'll back off of push for this box
2343                                    pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
2344                                    continue;
2345                                } else {
2346                                    userLog("Last ping reported changes in error for: ", name);
2347                                    errorMap.put(serverId, failures + 1);
2348                                }
2349                            }
2350                        }
2351
2352                        // If there were no problems with previous sync, we'll start another one
2353                        ExchangeService.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
2354                                ExchangeService.SYNC_PING, null);
2355                    }
2356                } finally {
2357                    c.close();
2358                }
2359            }
2360        }
2361        return pp.getSyncStatus();
2362    }
2363
2364    /**
2365     * Common code to sync E+PIM data
2366     *
2367     * @param target an EasMailbox, EasContacts, or EasCalendar object
2368     */
2369    public void sync(AbstractSyncAdapter target) throws IOException {
2370        Mailbox mailbox = target.mMailbox;
2371
2372        boolean moreAvailable = true;
2373        int loopingCount = 0;
2374        while (!mStop && (moreAvailable || hasPendingRequests())) {
2375            // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
2376            // when connectivity has returned
2377            if (!hasConnectivity()) {
2378                userLog("No connectivity in sync; finishing sync");
2379                mExitStatus = EXIT_DONE;
2380                return;
2381            }
2382
2383            // Every time through the loop we check to see if we're still syncable
2384            if (!target.isSyncable()) {
2385                mExitStatus = EXIT_DONE;
2386                return;
2387            }
2388
2389            // Now, handle various requests
2390            while (true) {
2391                Request req = null;
2392
2393                if (mRequestQueue.isEmpty()) {
2394                    break;
2395                } else {
2396                    req = mRequestQueue.peek();
2397                }
2398
2399                // Our two request types are PartRequest (loading attachment) and
2400                // MeetingResponseRequest (respond to a meeting request)
2401                if (req instanceof PartRequest) {
2402                    loadAttachment((PartRequest)req);
2403                } else if (req instanceof MeetingResponseRequest) {
2404                    sendMeetingResponse((MeetingResponseRequest)req);
2405                } else if (req instanceof MessageMoveRequest) {
2406                    messageMoveRequest((MessageMoveRequest)req);
2407                }
2408
2409                // If there's an exception handling the request, we'll throw it
2410                // Otherwise, we remove the request
2411                mRequestQueue.remove();
2412            }
2413
2414            // Don't sync if we've got nothing to do
2415            if (!moreAvailable) {
2416                continue;
2417            }
2418
2419            Serializer s = new Serializer();
2420
2421            String className = target.getCollectionName();
2422            String syncKey = target.getSyncKey();
2423            userLog("sync, sending ", className, " syncKey: ", syncKey);
2424            s.start(Tags.SYNC_SYNC)
2425                .start(Tags.SYNC_COLLECTIONS)
2426                .start(Tags.SYNC_COLLECTION);
2427            // The "Class" element is removed in EAS 12.1 and later versions
2428            if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
2429                s.data(Tags.SYNC_CLASS, className);
2430            }
2431            s.data(Tags.SYNC_SYNC_KEY, syncKey)
2432                .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
2433
2434            // Start with the default timeout
2435            int timeout = COMMAND_TIMEOUT;
2436            if (!syncKey.equals("0")) {
2437                // EAS doesn't allow GetChanges in an initial sync; sending other options
2438                // appears to cause the server to delay its response in some cases, and this delay
2439                // can be long enough to result in an IOException and total failure to sync.
2440                // Therefore, we don't send any options with the initial sync.
2441                // Set the truncation amount, body preference, lookback, etc.
2442                target.sendSyncOptions(mProtocolVersionDouble, s);
2443            } else {
2444                // Use enormous timeout for initial sync, which empirically can take a while longer
2445                timeout = 120*SECONDS;
2446            }
2447            // Send our changes up to the server
2448            target.sendLocalChanges(s);
2449
2450            s.end().end().end().done();
2451            EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
2452                    timeout);
2453            try {
2454                int code = resp.getStatus();
2455                if (code == HttpStatus.SC_OK) {
2456                    // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
2457                    // no changes in the mailbox; handle that case here
2458                    // There are two cases here; if we get back a compressed stream (GZIP), we won't
2459                    // know until we try to parse it (and generate an EmptyStreamException). If we
2460                    // get uncompressed data, the response will be empty (i.e. have zero length)
2461                    boolean emptyStream = false;
2462                    if (!resp.isEmpty()) {
2463                        InputStream is = resp.getInputStream();
2464                        try {
2465                            moreAvailable = target.parse(is);
2466                            if (target.isLooping()) {
2467                                loopingCount++;
2468                                userLog("** Looping: " + loopingCount);
2469                                // After the maximum number of loops, we'll set moreAvailable to
2470                                // false and allow the sync loop to terminate
2471                                if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
2472                                    userLog("** Looping force stopped");
2473                                    moreAvailable = false;
2474                                }
2475                            } else {
2476                                loopingCount = 0;
2477                            }
2478                            target.cleanup();
2479                        } catch (EmptyStreamException e) {
2480                            userLog("Empty stream detected in GZIP response");
2481                            emptyStream = true;
2482                        } catch (CommandStatusException e) {
2483                            // TODO 14.1
2484                            int status = e.mStatus;
2485                            if (CommandStatus.isNeedsProvisioning(status)) {
2486                                mExitStatus = EXIT_SECURITY_FAILURE;
2487                            } else if (CommandStatus.isDeniedAccess(status)) {
2488                                mExitStatus = EXIT_ACCESS_DENIED;
2489                            } else if (CommandStatus.isTransientError(status)) {
2490                                mExitStatus = EXIT_IO_ERROR;
2491                            } else {
2492                                mExitStatus = EXIT_EXCEPTION;
2493                            }
2494                            return;
2495                        }
2496                    } else {
2497                        emptyStream = true;
2498                    }
2499
2500                    if (emptyStream) {
2501                        // If this happens, exit cleanly, and change the interval from push to ping
2502                        // if necessary
2503                        userLog("Empty sync response; finishing");
2504                        if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
2505                            userLog("Changing mailbox from push to ping");
2506                            ContentValues cv = new ContentValues();
2507                            cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
2508                            mContentResolver.update(
2509                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
2510                                    cv, null, null);
2511                        }
2512                        if (mRequestQueue.isEmpty()) {
2513                            mExitStatus = EXIT_DONE;
2514                            return;
2515                        } else {
2516                            continue;
2517                        }
2518                    }
2519                } else {
2520                    userLog("Sync response error: ", code);
2521                    if (isProvisionError(code)) {
2522                        mExitStatus = EXIT_SECURITY_FAILURE;
2523                    } else if (isAuthError(code)) {
2524                        mExitStatus = EXIT_LOGIN_FAILURE;
2525                    } else {
2526                        mExitStatus = EXIT_IO_ERROR;
2527                    }
2528                    return;
2529                }
2530            } finally {
2531                resp.close();
2532            }
2533        }
2534        mExitStatus = EXIT_DONE;
2535    }
2536
2537    protected boolean setupService() {
2538        synchronized(getSynchronizer()) {
2539            mThread = Thread.currentThread();
2540            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
2541            TAG = mThread.getName();
2542        }
2543        // Make sure account and mailbox are always the latest from the database
2544        mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
2545        if (mAccount == null) return false;
2546        mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
2547        if (mMailbox == null) return false;
2548        HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
2549        if (ha == null) return false;
2550        mHostAddress = ha.mAddress;
2551        mUserName = ha.mLogin;
2552        mPassword = ha.mPassword;
2553
2554        // Set up our protocol version from the Account
2555        mProtocolVersion = mAccount.mProtocolVersion;
2556        // If it hasn't been set up, start with default version
2557        if (mProtocolVersion == null) {
2558            mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
2559        }
2560        mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
2561        return true;
2562    }
2563
2564    /* (non-Javadoc)
2565     * @see java.lang.Runnable#run()
2566     */
2567    public void run() {
2568        // Make sure account and mailbox are still valid
2569        if (!setupService()) return;
2570        // If we've been stopped, we're done
2571        if (mStop) return;
2572        if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START) {
2573            try {
2574                ExchangeService.callback().syncMailboxStatus(mMailboxId,
2575                        EmailServiceStatus.IN_PROGRESS, 0);
2576            } catch (RemoteException e1) {
2577                // Don't care if this fails
2578            }
2579        }
2580
2581        // Whether or not we're the account mailbox
2582        try {
2583            mDeviceId = ExchangeService.getDeviceId(mContext);
2584            if ((mMailbox == null) || (mAccount == null)) {
2585                return;
2586            } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
2587                runAccountMailbox();
2588            } else {
2589                AbstractSyncAdapter target;
2590                if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
2591                    target = new ContactsSyncAdapter( this);
2592                } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
2593                    target = new CalendarSyncAdapter(this);
2594                } else {
2595                    target = new EmailSyncAdapter(this);
2596                }
2597                // We loop here because someone might have put a request in while we were syncing
2598                // and we've missed that opportunity...
2599                do {
2600                    if (mRequestTime != 0) {
2601                        userLog("Looping for user request...");
2602                        mRequestTime = 0;
2603                    }
2604                    sync(target);
2605                } while (mRequestTime != 0);
2606            }
2607        } catch (EasAuthenticationException e) {
2608            userLog("Caught authentication error");
2609            mExitStatus = EXIT_LOGIN_FAILURE;
2610        } catch (IOException e) {
2611            String message = e.getMessage();
2612            userLog("Caught IOException: ", (message == null) ? "No message" : message);
2613            mExitStatus = EXIT_IO_ERROR;
2614        } catch (Exception e) {
2615            userLog("Uncaught exception in EasSyncService", e);
2616        } finally {
2617            int status;
2618
2619            if (!mStop) {
2620                userLog("Sync finished");
2621                ExchangeService.done(this);
2622                switch (mExitStatus) {
2623                    case EXIT_IO_ERROR:
2624                        status = EmailServiceStatus.CONNECTION_ERROR;
2625                        break;
2626                    case EXIT_DONE:
2627                        status = EmailServiceStatus.SUCCESS;
2628                        ContentValues cv = new ContentValues();
2629                        cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
2630                        String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
2631                        cv.put(Mailbox.SYNC_STATUS, s);
2632                        mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
2633                                mMailboxId), cv, null, null);
2634                        break;
2635                    case EXIT_LOGIN_FAILURE:
2636                        status = EmailServiceStatus.LOGIN_FAILED;
2637                        break;
2638                    case EXIT_SECURITY_FAILURE:
2639                        status = EmailServiceStatus.SECURITY_FAILURE;
2640                        // Ask for a new folder list.  This should wake up the account mailbox; a
2641                        // security error in account mailbox should start the provisioning process
2642                        ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
2643                        break;
2644                    case EXIT_ACCESS_DENIED:
2645                        status = EmailServiceStatus.ACCESS_DENIED;
2646                        break;
2647                    default:
2648                        status = EmailServiceStatus.REMOTE_EXCEPTION;
2649                        errorLog("Sync ended due to an exception.");
2650                        break;
2651                }
2652            } else {
2653                userLog("Stopped sync finished.");
2654                status = EmailServiceStatus.SUCCESS;
2655            }
2656
2657            // Send a callback if this run was initiated by a service call
2658            if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START) {
2659                try {
2660                    // Unless the user specifically asked for a sync, we really don't want to report
2661                    // connection issues, as they are likely to be transient.  In this case, we
2662                    // simply report success, so that the progress indicator terminates without
2663                    // putting up an error banner
2664                    if (mSyncReason != ExchangeService.SYNC_UI_REQUEST &&
2665                            status == EmailServiceStatus.CONNECTION_ERROR) {
2666                        status = EmailServiceStatus.SUCCESS;
2667                    }
2668                    ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0);
2669                } catch (RemoteException e1) {
2670                    // Don't care if this fails
2671                }
2672            }
2673
2674            // Make sure ExchangeService knows about this
2675            ExchangeService.kick("sync finished");
2676       }
2677    }
2678}
2679