EasSyncService.java revision 5bb9fd2d7c145286d22adbe6e6e1b11d9ee5daac
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 = 20*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 // If we're not the admin, we can't do the wipe, so just return 1286 if (!sp.isActiveAdmin()) return false; 1287 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that 1288 // we wipe the device regardless of any errors in acknowledgment 1289 try { 1290 acknowledgeRemoteWipe(pp.getPolicyKey()); 1291 } catch (Exception e) { 1292 // Because remote wipe is such a high priority task, we don't want to 1293 // circumvent it if there's an exception in acknowledgment 1294 } 1295 // Then, tell SecurityPolicy to wipe the device 1296 sp.remoteWipe(); 1297 return false; 1298 } else if (sp.isActive(ps)) { 1299 // See if the required policies are in force; if they are, acknowledge the policies 1300 // to the server and get the final policy key 1301 String policyKey = acknowledgeProvision(pp.getPolicyKey(), PROVISION_STATUS_OK); 1302 if (policyKey != null) { 1303 // Write the final policy key to the Account and say we've been successful 1304 ps.writeAccount(mAccount, policyKey, true, mContext); 1305 // Release any mailboxes that might be in a security hold 1306 SyncManager.releaseSecurityHold(mAccount); 1307 return true; 1308 } 1309 } else { 1310 // Notify that we are blocked because of policies 1311 sp.policiesRequired(mAccount.mId); 1312 } 1313 } 1314 return false; 1315 } 1316 1317 private String getPolicyType() { 1318 return (mProtocolVersionDouble >= 1319 Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; 1320 } 1321 1322 /** 1323 * Obtain a set of policies from the server and determine whether those policies are supported 1324 * by the device. 1325 * @return the ProvisionParser (holds policies and key) if we receive policies and they are 1326 * supported by the device; null otherwise 1327 * @throws IOException 1328 */ 1329 private ProvisionParser canProvision() throws IOException { 1330 Serializer s = new Serializer(); 1331 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1332 s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()) 1333 .end().end().end().done(); 1334 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1335 int code = resp.getStatusLine().getStatusCode(); 1336 if (code == HttpStatus.SC_OK) { 1337 InputStream is = resp.getEntity().getContent(); 1338 ProvisionParser pp = new ProvisionParser(is, this); 1339 if (pp.parse()) { 1340 // The PolicySet in the ProvisionParser will have the requirements for all KNOWN 1341 // policies. If others are required, hasSupportablePolicySet will be false 1342 if (pp.hasSupportablePolicySet()) { 1343 // If the policies are supportable (in this context, meaning that there are no 1344 // completely unimplemented policies required), just return the parser itself 1345 return pp; 1346 } else { 1347 // Try to acknowledge using the "partial" status (i.e. we can partially 1348 // accommodate the required policies). The server will agree to this if the 1349 // "allow non-provisionable devices" setting is enabled on the server 1350 String policyKey = acknowledgeProvision(pp.getPolicyKey(), 1351 PROVISION_STATUS_PARTIAL); 1352 // Return either the parser (success) or null (failure) 1353 return (policyKey != null) ? pp : null; 1354 } 1355 } 1356 } 1357 // On failures, simply return null 1358 return null; 1359 } 1360 1361 /** 1362 * Acknowledge that we support the policies provided by the server, and that these policies 1363 * are in force. 1364 * @param tempKey the initial (temporary) policy key sent by the server 1365 * @return the final policy key, which can be used for syncing 1366 * @throws IOException 1367 */ 1368 private void acknowledgeRemoteWipe(String tempKey) throws IOException { 1369 acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true); 1370 } 1371 1372 private String acknowledgeProvision(String tempKey, String result) throws IOException { 1373 return acknowledgeProvisionImpl(tempKey, result, false); 1374 } 1375 1376 private String acknowledgeProvisionImpl(String tempKey, String status, 1377 boolean remoteWipe) throws IOException { 1378 Serializer s = new Serializer(); 1379 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1380 s.start(Tags.PROVISION_POLICY); 1381 1382 // Use the proper policy type, depending on EAS version 1383 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType()); 1384 1385 s.data(Tags.PROVISION_POLICY_KEY, tempKey); 1386 s.data(Tags.PROVISION_STATUS, status); 1387 s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES 1388 if (remoteWipe) { 1389 s.start(Tags.PROVISION_REMOTE_WIPE); 1390 s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK); 1391 s.end(); 1392 } 1393 s.end().done(); // PROVISION_PROVISION 1394 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1395 int code = resp.getStatusLine().getStatusCode(); 1396 if (code == HttpStatus.SC_OK) { 1397 InputStream is = resp.getEntity().getContent(); 1398 ProvisionParser pp = new ProvisionParser(is, this); 1399 if (pp.parse()) { 1400 // Return the final policy key from the ProvisionParser 1401 return pp.getPolicyKey(); 1402 } 1403 } 1404 // On failures, return null 1405 return null; 1406 } 1407 1408 /** 1409 * Performs FolderSync 1410 * 1411 * @throws IOException 1412 * @throws EasParserException 1413 */ 1414 public void runAccountMailbox() throws IOException, EasParserException { 1415 // Initialize exit status to success 1416 mExitStatus = EmailServiceStatus.SUCCESS; 1417 try { 1418 try { 1419 SyncManager.callback() 1420 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 1421 } catch (RemoteException e1) { 1422 // Don't care if this fails 1423 } 1424 1425 if (mAccount.mSyncKey == null) { 1426 mAccount.mSyncKey = "0"; 1427 userLog("Account syncKey INIT to 0"); 1428 ContentValues cv = new ContentValues(); 1429 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 1430 mAccount.update(mContext, cv); 1431 } 1432 1433 boolean firstSync = mAccount.mSyncKey.equals("0"); 1434 if (firstSync) { 1435 userLog("Initial FolderSync"); 1436 } 1437 1438 // When we first start up, change all mailboxes to push. 1439 ContentValues cv = new ContentValues(); 1440 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1441 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1442 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 1443 new String[] {Long.toString(mAccount.mId)}) > 0) { 1444 SyncManager.kick("change ping boxes to push"); 1445 } 1446 1447 // Determine our protocol version, if we haven't already and save it in the Account 1448 // Also re-check protocol version at least once a day (in case of upgrade) 1449 if (mAccount.mProtocolVersion == null || 1450 ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) { 1451 userLog("Determine EAS protocol version"); 1452 HttpResponse resp = sendHttpClientOptions(); 1453 int code = resp.getStatusLine().getStatusCode(); 1454 userLog("OPTIONS response: ", code); 1455 if (code == HttpStatus.SC_OK) { 1456 Header header = resp.getFirstHeader("MS-ASProtocolCommands"); 1457 userLog(header.getValue()); 1458 header = resp.getFirstHeader("ms-asprotocolversions"); 1459 try { 1460 setupProtocolVersion(this, header); 1461 } catch (MessagingException e) { 1462 // Since we've already validated, this can't really happen 1463 // But if it does, we'll rethrow this... 1464 throw new IOException(); 1465 } 1466 // Save the protocol version 1467 cv.clear(); 1468 // Save the protocol version in the account 1469 cv.put(Account.PROTOCOL_VERSION, mProtocolVersion); 1470 mAccount.update(mContext, cv); 1471 cv.clear(); 1472 // Save the sync time of the account mailbox to current time 1473 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1474 mMailbox.update(mContext, cv); 1475 } else { 1476 errorLog("OPTIONS command failed; throwing IOException"); 1477 throw new IOException(); 1478 } 1479 } 1480 1481 // Change all pushable boxes to push when we start the account mailbox 1482 if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 1483 cv.clear(); 1484 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1485 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1486 SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE, 1487 new String[] {Long.toString(mAccount.mId)}) > 0) { 1488 userLog("Push account; set pushable boxes to push..."); 1489 } 1490 } 1491 1492 while (!mStop) { 1493 userLog("Sending Account syncKey: ", mAccount.mSyncKey); 1494 Serializer s = new Serializer(); 1495 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 1496 .text(mAccount.mSyncKey).end().end().done(); 1497 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 1498 if (mStop) break; 1499 int code = resp.getStatusLine().getStatusCode(); 1500 if (code == HttpStatus.SC_OK) { 1501 HttpEntity entity = resp.getEntity(); 1502 int len = (int)entity.getContentLength(); 1503 if (len != 0) { 1504 InputStream is = entity.getContent(); 1505 // Returns true if we need to sync again 1506 if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this)) 1507 .parse()) { 1508 continue; 1509 } 1510 } 1511 } else if (isProvisionError(code)) { 1512 // If the sync error is a provisioning failure (perhaps the policies changed), 1513 // let's try the provisioning procedure 1514 // Provisioning must only be attempted for the account mailbox - trying to 1515 // provision any other mailbox may result in race conditions and the creation 1516 // of multiple policy keys. 1517 if (!tryProvision()) { 1518 // Set the appropriate failure status 1519 mExitStatus = EXIT_SECURITY_FAILURE; 1520 return; 1521 } else { 1522 // If we succeeded, try again... 1523 continue; 1524 } 1525 } else if (isAuthError(code)) { 1526 mExitStatus = EXIT_LOGIN_FAILURE; 1527 return; 1528 } else { 1529 userLog("FolderSync response error: ", code); 1530 } 1531 1532 // Change all push/hold boxes to push 1533 cv.clear(); 1534 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 1535 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1536 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 1537 new String[] {Long.toString(mAccount.mId)}) > 0) { 1538 userLog("Set push/hold boxes to push..."); 1539 } 1540 1541 try { 1542 SyncManager.callback() 1543 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 1544 } catch (RemoteException e1) { 1545 // Don't care if this fails 1546 } 1547 1548 // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's 1549 // active; otherwise, clear out the key/flag. This should cause a provisioning 1550 // error on the next POST, and start the security sequence over again 1551 String key = mAccount.mSecuritySyncKey; 1552 if (!TextUtils.isEmpty(key)) { 1553 PolicySet ps = new PolicySet(mAccount); 1554 SecurityPolicy sp = SecurityPolicy.getInstance(mContext); 1555 if (!sp.isActive(ps)) { 1556 cv.clear(); 1557 cv.put(AccountColumns.SECURITY_FLAGS, 0); 1558 cv.putNull(AccountColumns.SECURITY_SYNC_KEY); 1559 long accountId = mAccount.mId; 1560 mContentResolver.update(ContentUris.withAppendedId( 1561 Account.CONTENT_URI, accountId), cv, null, null); 1562 sp.policiesRequired(accountId); 1563 } 1564 } 1565 1566 // Wait for push notifications. 1567 String threadName = Thread.currentThread().getName(); 1568 try { 1569 runPingLoop(); 1570 } catch (StaleFolderListException e) { 1571 // We break out if we get told about a stale folder list 1572 userLog("Ping interrupted; folder list requires sync..."); 1573 } catch (IllegalHeartbeatException e) { 1574 // If we're sending an illegal heartbeat, reset either the min or the max to 1575 // that heartbeat 1576 resetHeartbeats(e.mLegalHeartbeat); 1577 } finally { 1578 Thread.currentThread().setName(threadName); 1579 } 1580 } 1581 } catch (IOException e) { 1582 // We catch this here to send the folder sync status callback 1583 // A folder sync failed callback will get sent from run() 1584 try { 1585 if (!mStop) { 1586 SyncManager.callback() 1587 .syncMailboxListStatus(mAccount.mId, 1588 EmailServiceStatus.CONNECTION_ERROR, 0); 1589 } 1590 } catch (RemoteException e1) { 1591 // Don't care if this fails 1592 } 1593 throw e; 1594 } 1595 } 1596 1597 /** 1598 * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal 1599 * @param legalHeartbeat a known legal heartbeat (from the EAS server) 1600 */ 1601 /*package*/ void resetHeartbeats(int legalHeartbeat) { 1602 userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat); 1603 // We are here because the current heartbeat (mPingHeartbeat) is invalid. Depending on 1604 // whether the argument is above or below the current heartbeat, we can infer the need to 1605 // change either the minimum or maximum heartbeat 1606 if (legalHeartbeat > mPingHeartbeat) { 1607 // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was 1608 // too low. We respond by raising either or both of the minimum heartbeat or the 1609 // force heartbeat to the argument value 1610 if (mPingMinHeartbeat < legalHeartbeat) { 1611 mPingMinHeartbeat = legalHeartbeat; 1612 } 1613 if (mPingForceHeartbeat < legalHeartbeat) { 1614 mPingForceHeartbeat = legalHeartbeat; 1615 } 1616 // If our minimum is now greater than the max, bring them together 1617 if (mPingMinHeartbeat > mPingMaxHeartbeat) { 1618 mPingMaxHeartbeat = legalHeartbeat; 1619 } 1620 } else if (legalHeartbeat < mPingHeartbeat) { 1621 // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was 1622 // too high. We respond by lowering the maximum to the argument value 1623 mPingMaxHeartbeat = legalHeartbeat; 1624 // If our maximum is now less than the minimum, bring them together 1625 if (mPingMaxHeartbeat < mPingMinHeartbeat) { 1626 mPingMinHeartbeat = legalHeartbeat; 1627 } 1628 } 1629 // Set current heartbeat to the legal heartbeat 1630 mPingHeartbeat = legalHeartbeat; 1631 // Allow the heartbeat logic to run 1632 mPingHeartbeatDropped = false; 1633 } 1634 1635 private void pushFallback(long mailboxId) { 1636 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1637 if (mailbox == null) { 1638 return; 1639 } 1640 ContentValues cv = new ContentValues(); 1641 int mins = PING_FALLBACK_PIM; 1642 if (mailbox.mType == Mailbox.TYPE_INBOX) { 1643 mins = PING_FALLBACK_INBOX; 1644 } 1645 cv.put(Mailbox.SYNC_INTERVAL, mins); 1646 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 1647 cv, null, null); 1648 errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync"); 1649 SyncManager.kick("push fallback"); 1650 } 1651 1652 /** 1653 * Simplistic attempt to determine a NAT timeout, based on experience with various carriers 1654 * and networks. The string "reset by peer" is very common in these situations, so we look for 1655 * that specifically. We may add additional tests here as more is learned. 1656 * @param message 1657 * @return whether this message is likely associated with a NAT failure 1658 */ 1659 private boolean isLikelyNatFailure(String message) { 1660 if (message == null) return false; 1661 if (message.contains("reset by peer")) { 1662 return true; 1663 } 1664 return false; 1665 } 1666 1667 private void runPingLoop() throws IOException, StaleFolderListException, 1668 IllegalHeartbeatException { 1669 int pingHeartbeat = mPingHeartbeat; 1670 userLog("runPingLoop"); 1671 // Do push for all sync services here 1672 long endTime = System.currentTimeMillis() + (30*MINUTES); 1673 HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>(); 1674 ArrayList<String> readyMailboxes = new ArrayList<String>(); 1675 ArrayList<String> notReadyMailboxes = new ArrayList<String>(); 1676 int pingWaitCount = 0; 1677 1678 while ((System.currentTimeMillis() < endTime) && !mStop) { 1679 // Count of pushable mailboxes 1680 int pushCount = 0; 1681 // Count of mailboxes that can be pushed right now 1682 int canPushCount = 0; 1683 // Count of uninitialized boxes 1684 int uninitCount = 0; 1685 1686 Serializer s = new Serializer(); 1687 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1688 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 1689 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 1690 notReadyMailboxes.clear(); 1691 readyMailboxes.clear(); 1692 try { 1693 // Loop through our pushed boxes seeing what is available to push 1694 while (c.moveToNext()) { 1695 pushCount++; 1696 // Two requirements for push: 1697 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 1698 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 1699 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 1700 int pingStatus = SyncManager.pingStatus(mailboxId); 1701 String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1702 if (pingStatus == SyncManager.PING_STATUS_OK) { 1703 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 1704 if ((syncKey == null) || syncKey.equals("0")) { 1705 // We can't push until the initial sync is done 1706 pushCount--; 1707 uninitCount++; 1708 continue; 1709 } 1710 1711 if (canPushCount++ == 0) { 1712 // Initialize the Ping command 1713 s.start(Tags.PING_PING) 1714 .data(Tags.PING_HEARTBEAT_INTERVAL, 1715 Integer.toString(pingHeartbeat)) 1716 .start(Tags.PING_FOLDERS); 1717 } 1718 1719 String folderClass = getTargetCollectionClassFromCursor(c); 1720 s.start(Tags.PING_FOLDER) 1721 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 1722 .data(Tags.PING_CLASS, folderClass) 1723 .end(); 1724 readyMailboxes.add(mailboxName); 1725 } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) || 1726 (pingStatus == SyncManager.PING_STATUS_WAITING)) { 1727 notReadyMailboxes.add(mailboxName); 1728 } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) { 1729 pushCount--; 1730 userLog(mailboxName, " in error state; ignore"); 1731 continue; 1732 } 1733 } 1734 } finally { 1735 c.close(); 1736 } 1737 1738 if (Eas.USER_LOG) { 1739 if (!notReadyMailboxes.isEmpty()) { 1740 userLog("Ping not ready for: " + notReadyMailboxes); 1741 } 1742 if (!readyMailboxes.isEmpty()) { 1743 userLog("Ping ready for: " + readyMailboxes); 1744 } 1745 } 1746 1747 // If we've waited 10 seconds or more, just ping with whatever boxes are ready 1748 // But use a shorter than normal heartbeat 1749 boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5); 1750 1751 if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) { 1752 // If all pingable boxes are ready for push, send Ping to the server 1753 s.end().end().done(); 1754 pingWaitCount = 0; 1755 mPostReset = false; 1756 mPostAborted = false; 1757 1758 // If we've been stopped, this is a good time to return 1759 if (mStop) return; 1760 1761 long pingTime = SystemClock.elapsedRealtime(); 1762 try { 1763 // Send the ping, wrapped by appropriate timeout/alarm 1764 if (forcePing) { 1765 userLog("Forcing ping after waiting for all boxes to be ready"); 1766 } 1767 HttpResponse res = 1768 sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat); 1769 1770 int code = res.getStatusLine().getStatusCode(); 1771 userLog("Ping response: ", code); 1772 1773 // Return immediately if we've been asked to stop during the ping 1774 if (mStop) { 1775 userLog("Stopping pingLoop"); 1776 return; 1777 } 1778 1779 if (code == HttpStatus.SC_OK) { 1780 // Make sure to clear out any pending sync errors 1781 SyncManager.removeFromSyncErrorMap(mMailboxId); 1782 HttpEntity e = res.getEntity(); 1783 int len = (int)e.getContentLength(); 1784 InputStream is = res.getEntity().getContent(); 1785 if (len != 0) { 1786 int pingResult = parsePingResult(is, mContentResolver, pingErrorMap); 1787 // If our ping completed (status = 1), and we weren't forced and we're 1788 // not at the maximum, try increasing timeout by two minutes 1789 if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) { 1790 if (pingHeartbeat > mPingHighWaterMark) { 1791 mPingHighWaterMark = pingHeartbeat; 1792 userLog("Setting high water mark at: ", mPingHighWaterMark); 1793 } 1794 if ((pingHeartbeat < mPingMaxHeartbeat) && 1795 !mPingHeartbeatDropped) { 1796 pingHeartbeat += PING_HEARTBEAT_INCREMENT; 1797 if (pingHeartbeat > mPingMaxHeartbeat) { 1798 pingHeartbeat = mPingMaxHeartbeat; 1799 } 1800 userLog("Increasing ping heartbeat to ", pingHeartbeat, "s"); 1801 } 1802 } 1803 } else { 1804 userLog("Ping returned empty result; throwing IOException"); 1805 throw new IOException(); 1806 } 1807 } else if (isAuthError(code)) { 1808 mExitStatus = EXIT_LOGIN_FAILURE; 1809 userLog("Authorization error during Ping: ", code); 1810 throw new IOException(); 1811 } 1812 } catch (IOException e) { 1813 String message = e.getMessage(); 1814 // If we get the exception that is indicative of a NAT timeout and if we 1815 // haven't yet "fixed" the timeout, back off by two minutes and "fix" it 1816 boolean hasMessage = message != null; 1817 userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]")); 1818 if (mPostReset) { 1819 // Nothing to do in this case; this is SyncManager telling us to try another 1820 // ping. 1821 } else if (mPostAborted || isLikelyNatFailure(message)) { 1822 long pingLength = SystemClock.elapsedRealtime() - pingTime; 1823 if ((pingHeartbeat > mPingMinHeartbeat) && 1824 (pingHeartbeat > mPingHighWaterMark)) { 1825 pingHeartbeat -= PING_HEARTBEAT_INCREMENT; 1826 mPingHeartbeatDropped = true; 1827 if (pingHeartbeat < mPingMinHeartbeat) { 1828 pingHeartbeat = mPingMinHeartbeat; 1829 } 1830 userLog("Decreased ping heartbeat to ", pingHeartbeat, "s"); 1831 } else if (mPostAborted) { 1832 // There's no point in throwing here; this can happen in two cases 1833 // 1) An alarm, which indicates minutes without activity; no sense 1834 // backing off 1835 // 2) SyncManager abort, due to sync of mailbox. Again, we want to 1836 // keep on trying to ping 1837 userLog("Ping aborted; retry"); 1838 } else if (pingLength < 2000) { 1839 userLog("Abort or NAT type return < 2 seconds; throwing IOException"); 1840 throw e; 1841 } else { 1842 userLog("NAT type IOException"); 1843 } 1844 } else if (hasMessage && message.contains("roken pipe")) { 1845 // The "broken pipe" error (uppercase or lowercase "b") seems to be an 1846 // internal error, so let's not throw an exception (which leads to delays) 1847 // but rather simply run through the loop again 1848 } else { 1849 throw e; 1850 } 1851 } 1852 } else if (forcePing) { 1853 // In this case, there aren't any boxes that are pingable, but there are boxes 1854 // waiting (for IOExceptions) 1855 userLog("pingLoop waiting 60s for any pingable boxes"); 1856 sleep(60*SECONDS, true); 1857 } else if (pushCount > 0) { 1858 // If we want to Ping, but can't just yet, wait a little bit 1859 // TODO Change sleep to wait and use notify from SyncManager when a sync ends 1860 sleep(2*SECONDS, false); 1861 pingWaitCount++; 1862 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)"); 1863 } else if (uninitCount > 0) { 1864 // In this case, we're doing an initial sync of at least one mailbox. Since this 1865 // is typically a one-time case, I'm ok with trying again every 10 seconds until 1866 // we're in one of the other possible states. 1867 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)"); 1868 sleep(10*SECONDS, true); 1869 } else { 1870 // We've got nothing to do, so we'll check again in 20 minutes at which time 1871 // we'll update the folder list, check for policy changes and/or remote wipe, etc. 1872 // Let the device sleep in the meantime... 1873 userLog(ACCOUNT_MAILBOX_SLEEP_TEXT); 1874 sleep(ACCOUNT_MAILBOX_SLEEP_TIME, true); 1875 } 1876 } 1877 1878 // Save away the current heartbeat 1879 mPingHeartbeat = pingHeartbeat; 1880 } 1881 1882 private void sleep(long ms, boolean runAsleep) { 1883 if (runAsleep) { 1884 SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS)); 1885 } 1886 try { 1887 Thread.sleep(ms); 1888 } catch (InterruptedException e) { 1889 // Doesn't matter whether we stop early; it's the thought that counts 1890 } finally { 1891 if (runAsleep) { 1892 SyncManager.runAwake(mMailboxId); 1893 } 1894 } 1895 } 1896 1897 private int parsePingResult(InputStream is, ContentResolver cr, 1898 HashMap<String, Integer> errorMap) 1899 throws IOException, StaleFolderListException, IllegalHeartbeatException { 1900 PingParser pp = new PingParser(is, this); 1901 if (pp.parse()) { 1902 // True indicates some mailboxes need syncing... 1903 // syncList has the serverId's of the mailboxes... 1904 mBindArguments[0] = Long.toString(mAccount.mId); 1905 mPingChangeList = pp.getSyncList(); 1906 for (String serverId: mPingChangeList) { 1907 mBindArguments[1] = serverId; 1908 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1909 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 1910 try { 1911 if (c.moveToFirst()) { 1912 1913 /** 1914 * Check the boxes reporting changes to see if there really were any... 1915 * We do this because bugs in various Exchange servers can put us into a 1916 * looping behavior by continually reporting changes in a mailbox, even when 1917 * there aren't any. 1918 * 1919 * This behavior is seemingly random, and therefore we must code defensively 1920 * by backing off of push behavior when it is detected. 1921 * 1922 * One known cause, on certain Exchange 2003 servers, is acknowledged by 1923 * Microsoft, and the server hotfix for this case can be found at 1924 * http://support.microsoft.com/kb/923282 1925 */ 1926 1927 // Check the status of the last sync 1928 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 1929 int type = SyncManager.getStatusType(status); 1930 // This check should always be true... 1931 if (type == SyncManager.SYNC_PING) { 1932 int changeCount = SyncManager.getStatusChangeCount(status); 1933 if (changeCount > 0) { 1934 errorMap.remove(serverId); 1935 } else if (changeCount == 0) { 1936 // This means that a ping reported changes in error; we keep a count 1937 // of consecutive errors of this kind 1938 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1939 Integer failures = errorMap.get(serverId); 1940 if (failures == null) { 1941 userLog("Last ping reported changes in error for: ", name); 1942 errorMap.put(serverId, 1); 1943 } else if (failures > MAX_PING_FAILURES) { 1944 // We'll back off of push for this box 1945 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); 1946 continue; 1947 } else { 1948 userLog("Last ping reported changes in error for: ", name); 1949 errorMap.put(serverId, failures + 1); 1950 } 1951 } 1952 } 1953 1954 // If there were no problems with previous sync, we'll start another one 1955 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 1956 SyncManager.SYNC_PING, null); 1957 } 1958 } finally { 1959 c.close(); 1960 } 1961 } 1962 } 1963 return pp.getSyncStatus(); 1964 } 1965 1966 private String getEmailFilter() { 1967 String filter = Eas.FILTER_1_WEEK; 1968 switch (mAccount.mSyncLookback) { 1969 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 1970 filter = Eas.FILTER_1_DAY; 1971 break; 1972 } 1973 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 1974 filter = Eas.FILTER_3_DAYS; 1975 break; 1976 } 1977 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 1978 filter = Eas.FILTER_1_WEEK; 1979 break; 1980 } 1981 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 1982 filter = Eas.FILTER_2_WEEKS; 1983 break; 1984 } 1985 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 1986 filter = Eas.FILTER_1_MONTH; 1987 break; 1988 } 1989 case com.android.email.Account.SYNC_WINDOW_ALL: { 1990 filter = Eas.FILTER_ALL; 1991 break; 1992 } 1993 } 1994 return filter; 1995 } 1996 1997 /** 1998 * Common code to sync E+PIM data 1999 * 2000 * @param target, an EasMailbox, EasContacts, or EasCalendar object 2001 */ 2002 public void sync(AbstractSyncAdapter target) throws IOException { 2003 Mailbox mailbox = target.mMailbox; 2004 2005 boolean moreAvailable = true; 2006 int loopingCount = 0; 2007 while (!mStop && moreAvailable) { 2008 // If we have no connectivity, just exit cleanly. SyncManager will start us up again 2009 // when connectivity has returned 2010 if (!hasConnectivity()) { 2011 userLog("No connectivity in sync; finishing sync"); 2012 mExitStatus = EXIT_DONE; 2013 return; 2014 } 2015 2016 // Every time through the loop we check to see if we're still syncable 2017 if (!target.isSyncable()) { 2018 mExitStatus = EXIT_DONE; 2019 return; 2020 } 2021 2022 // Now, handle various requests 2023 while (true) { 2024 Request req = null; 2025 synchronized (mRequests) { 2026 if (mRequests.isEmpty()) { 2027 break; 2028 } else { 2029 req = mRequests.get(0); 2030 } 2031 } 2032 2033 // Our two request types are PartRequest (loading attachment) and 2034 // MeetingResponseRequest (respond to a meeting request) 2035 if (req instanceof PartRequest) { 2036 getAttachment((PartRequest)req); 2037 } else if (req instanceof MeetingResponseRequest) { 2038 sendMeetingResponse((MeetingResponseRequest)req); 2039 } 2040 2041 // If there's an exception handling the request, we'll throw it 2042 // Otherwise, we remove the request 2043 synchronized(mRequests) { 2044 mRequests.remove(req); 2045 } 2046 } 2047 2048 Serializer s = new Serializer(); 2049 2050 String className = target.getCollectionName(); 2051 String syncKey = target.getSyncKey(); 2052 userLog("sync, sending ", className, " syncKey: ", syncKey); 2053 s.start(Tags.SYNC_SYNC) 2054 .start(Tags.SYNC_COLLECTIONS) 2055 .start(Tags.SYNC_COLLECTION) 2056 .data(Tags.SYNC_CLASS, className) 2057 .data(Tags.SYNC_SYNC_KEY, syncKey) 2058 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId); 2059 2060 // Start with the default timeout 2061 int timeout = COMMAND_TIMEOUT; 2062 if (!syncKey.equals("0")) { 2063 // EAS doesn't allow GetChanges in an initial sync; sending other options 2064 // appears to cause the server to delay its response in some cases, and this delay 2065 // can be long enough to result in an IOException and total failure to sync. 2066 // Therefore, we don't send any options with the initial sync. 2067 s.tag(Tags.SYNC_DELETES_AS_MOVES); 2068 s.tag(Tags.SYNC_GET_CHANGES); 2069 s.data(Tags.SYNC_WINDOW_SIZE, 2070 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 2071 // Handle options 2072 s.start(Tags.SYNC_OPTIONS); 2073 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts 2074 if (className.equals("Email")) { 2075 s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter()); 2076 } else if (className.equals("Calendar")) { 2077 // TODO Force two weeks for calendar until we can set this! 2078 s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_2_WEEKS); 2079 } 2080 // Set the truncation amount for all classes 2081 if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 2082 s.start(Tags.BASE_BODY_PREFERENCE) 2083 // HTML for email; plain text for everything else 2084 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 2085 : Eas.BODY_PREFERENCE_TEXT)) 2086 .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE) 2087 .end(); 2088 } else { 2089 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 2090 } 2091 s.end(); 2092 } else { 2093 // Use enormous timeout for initial sync, which empirically can take a while longer 2094 timeout = 120*SECONDS; 2095 } 2096 // Send our changes up to the server 2097 target.sendLocalChanges(s); 2098 2099 s.end().end().end().done(); 2100 HttpResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()), 2101 timeout); 2102 int code = resp.getStatusLine().getStatusCode(); 2103 if (code == HttpStatus.SC_OK) { 2104 InputStream is = resp.getEntity().getContent(); 2105 if (is != null) { 2106 moreAvailable = target.parse(is); 2107 if (target.isLooping()) { 2108 loopingCount++; 2109 userLog("** Looping: " + loopingCount); 2110 // After the maximum number of loops, we'll set moreAvailable to false and 2111 // allow the sync loop to terminate 2112 if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) { 2113 userLog("** Looping force stopped"); 2114 moreAvailable = false; 2115 } 2116 } else { 2117 loopingCount = 0; 2118 } 2119 target.cleanup(); 2120 } else { 2121 userLog("Empty input stream in sync command response"); 2122 } 2123 } else { 2124 userLog("Sync response error: ", code); 2125 if (isProvisionError(code)) { 2126 mExitStatus = EXIT_SECURITY_FAILURE; 2127 } else if (isAuthError(code)) { 2128 mExitStatus = EXIT_LOGIN_FAILURE; 2129 } else { 2130 mExitStatus = EXIT_IO_ERROR; 2131 } 2132 return; 2133 } 2134 } 2135 mExitStatus = EXIT_DONE; 2136 } 2137 2138 protected boolean setupService() { 2139 // Make sure account and mailbox are always the latest from the database 2140 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 2141 if (mAccount == null) return false; 2142 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 2143 if (mMailbox == null) return false; 2144 mThread = Thread.currentThread(); 2145 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 2146 TAG = mThread.getName(); 2147 2148 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 2149 if (ha == null) return false; 2150 mHostAddress = ha.mAddress; 2151 mUserName = ha.mLogin; 2152 mPassword = ha.mPassword; 2153 2154 // Set up our protocol version from the Account 2155 mProtocolVersion = mAccount.mProtocolVersion; 2156 // If it hasn't been set up, start with default version 2157 if (mProtocolVersion == null) { 2158 mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION; 2159 } 2160 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 2161 return true; 2162 } 2163 2164 /* (non-Javadoc) 2165 * @see java.lang.Runnable#run() 2166 */ 2167 public void run() { 2168 if (!setupService()) return; 2169 2170 try { 2171 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 2172 } catch (RemoteException e1) { 2173 // Don't care if this fails 2174 } 2175 2176 // Whether or not we're the account mailbox 2177 try { 2178 mDeviceId = SyncManager.getDeviceId(); 2179 if ((mMailbox == null) || (mAccount == null)) { 2180 return; 2181 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 2182 runAccountMailbox(); 2183 } else { 2184 AbstractSyncAdapter target; 2185 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 2186 target = new ContactsSyncAdapter(mMailbox, this); 2187 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) { 2188 target = new CalendarSyncAdapter(mMailbox, this); 2189 } else { 2190 target = new EmailSyncAdapter(mMailbox, this); 2191 } 2192 // We loop here because someone might have put a request in while we were syncing 2193 // and we've missed that opportunity... 2194 do { 2195 if (mRequestTime != 0) { 2196 userLog("Looping for user request..."); 2197 mRequestTime = 0; 2198 } 2199 sync(target); 2200 } while (mRequestTime != 0); 2201 } 2202 } catch (EasAuthenticationException e) { 2203 userLog("Caught authentication error"); 2204 mExitStatus = EXIT_LOGIN_FAILURE; 2205 } catch (IOException e) { 2206 String message = e.getMessage(); 2207 userLog("Caught IOException: ", (message == null) ? "No message" : message); 2208 mExitStatus = EXIT_IO_ERROR; 2209 } catch (Exception e) { 2210 userLog("Uncaught exception in EasSyncService", e); 2211 } finally { 2212 int status; 2213 2214 if (!mStop) { 2215 userLog("Sync finished"); 2216 SyncManager.done(this); 2217 switch (mExitStatus) { 2218 case EXIT_IO_ERROR: 2219 status = EmailServiceStatus.CONNECTION_ERROR; 2220 break; 2221 case EXIT_DONE: 2222 status = EmailServiceStatus.SUCCESS; 2223 ContentValues cv = new ContentValues(); 2224 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 2225 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 2226 cv.put(Mailbox.SYNC_STATUS, s); 2227 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 2228 mMailboxId), cv, null, null); 2229 break; 2230 case EXIT_LOGIN_FAILURE: 2231 status = EmailServiceStatus.LOGIN_FAILED; 2232 break; 2233 case EXIT_SECURITY_FAILURE: 2234 status = EmailServiceStatus.SECURITY_FAILURE; 2235 // Ask for a new folder list. This should wake up the account mailbox; a 2236 // security error in account mailbox should start the provisioning process 2237 SyncManager.reloadFolderList(mContext, mAccount.mId, true); 2238 break; 2239 default: 2240 status = EmailServiceStatus.REMOTE_EXCEPTION; 2241 errorLog("Sync ended due to an exception."); 2242 break; 2243 } 2244 } else { 2245 userLog("Stopped sync finished."); 2246 status = EmailServiceStatus.SUCCESS; 2247 } 2248 2249 try { 2250 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 2251 } catch (RemoteException e1) { 2252 // Don't care if this fails 2253 } 2254 2255 // Make sure SyncManager knows about this 2256 SyncManager.kick("sync finished"); 2257 } 2258 } 2259} 2260