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