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