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