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