EasSyncService.java revision 9cbd5664b81e31772377aa91cbbad29301e547ee
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.SecurityPolicy.PolicySet; 22import com.android.email.mail.AuthenticationFailedException; 23import com.android.email.mail.MessagingException; 24import com.android.email.provider.EmailContent.Account; 25import com.android.email.provider.EmailContent.AccountColumns; 26import com.android.email.provider.EmailContent.Attachment; 27import com.android.email.provider.EmailContent.AttachmentColumns; 28import com.android.email.provider.EmailContent.HostAuth; 29import com.android.email.provider.EmailContent.Mailbox; 30import com.android.email.provider.EmailContent.MailboxColumns; 31import com.android.email.provider.EmailContent.Message; 32import com.android.email.service.EmailServiceProxy; 33import com.android.email.service.EmailServiceStatus; 34import com.android.exchange.adapter.AbstractSyncAdapter; 35import com.android.exchange.adapter.AccountSyncAdapter; 36import com.android.exchange.adapter.CalendarSyncAdapter; 37import com.android.exchange.adapter.ContactsSyncAdapter; 38import com.android.exchange.adapter.EmailSyncAdapter; 39import com.android.exchange.adapter.FolderSyncParser; 40import com.android.exchange.adapter.MeetingResponseParser; 41import com.android.exchange.adapter.Parser.EasParserException; 42import com.android.exchange.adapter.PingParser; 43import com.android.exchange.adapter.ProvisionParser; 44import com.android.exchange.adapter.Serializer; 45import com.android.exchange.adapter.Tags; 46 47import org.apache.http.Header; 48import org.apache.http.HttpEntity; 49import org.apache.http.HttpResponse; 50import org.apache.http.HttpStatus; 51import org.apache.http.client.ClientProtocolException; 52import org.apache.http.client.HttpClient; 53import org.apache.http.client.methods.HttpOptions; 54import org.apache.http.client.methods.HttpPost; 55import org.apache.http.client.methods.HttpRequestBase; 56import org.apache.http.conn.ClientConnectionManager; 57import org.apache.http.entity.ByteArrayEntity; 58import org.apache.http.entity.StringEntity; 59import org.apache.http.impl.client.DefaultHttpClient; 60import org.apache.http.params.BasicHttpParams; 61import org.apache.http.params.HttpConnectionParams; 62import org.apache.http.params.HttpParams; 63import org.xmlpull.v1.XmlPullParser; 64import org.xmlpull.v1.XmlPullParserException; 65import org.xmlpull.v1.XmlPullParserFactory; 66import org.xmlpull.v1.XmlSerializer; 67 68import android.content.ContentResolver; 69import android.content.ContentUris; 70import android.content.ContentValues; 71import android.content.Context; 72import android.database.Cursor; 73import android.os.Bundle; 74import android.os.RemoteException; 75import android.os.SystemClock; 76import android.util.Log; 77import android.util.Xml; 78import android.util.base64.Base64; 79 80import java.io.ByteArrayOutputStream; 81import java.io.File; 82import java.io.FileOutputStream; 83import java.io.IOException; 84import java.io.InputStream; 85import java.net.URI; 86import java.net.URLEncoder; 87import java.security.cert.CertificateException; 88import java.util.ArrayList; 89import java.util.HashMap; 90 91public class EasSyncService extends AbstractSyncService { 92 private static final String EMAIL_WINDOW_SIZE = "5"; 93 public static final String PIM_WINDOW_SIZE = "5"; 94 private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = 95 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; 96 private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING = 97 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 98 '=' + Mailbox.CHECK_INTERVAL_PING; 99 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 100 MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING + 101 ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 102 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 103 private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX = 104 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 105 '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD; 106 static private final int CHUNK_SIZE = 16*1024; 107 108 static private final String PING_COMMAND = "Ping"; 109 static private final int COMMAND_TIMEOUT = 20*SECONDS; 110 111 // Define our default protocol version as 2.5 (Exchange 2003) 112 static private final String DEFAULT_PROTOCOL_VERSION = "2.5"; 113 114 static private final String AUTO_DISCOVER_SCHEMA_PREFIX = 115 "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/"; 116 static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml"; 117 static private final int AUTO_DISCOVER_REDIRECT_CODE = 451; 118 119 static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; 120 static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; 121 122 /** 123 * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's 124 * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let 125 * the ping exception out. The maximum I use is 17 minutes, which is really an empirical 126 * choice; too long and we risk silent connection loss and loss of push for that period. Too 127 * short and we lose efficiency/battery life. 128 * 129 * If we ever have to drop the ping timeout, we'll never increase it again. There's no point 130 * going into hysteresis; the NAT timeout isn't going to change without a change in connection, 131 * which will cause the sync service to be restarted at the starting heartbeat and going through 132 * the process again. 133 */ 134 static private final int PING_MINUTES = 60; // in seconds 135 static private final int PING_FUDGE_LOW = 10; 136 static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW; 137 static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW; 138 static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW; 139 static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES; 140 static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES; 141 142 static private final int PROTOCOL_PING_STATUS_COMPLETED = 1; 143 144 // Fallbacks (in minutes) for ping loop failures 145 static private final int MAX_PING_FAILURES = 1; 146 static private final int PING_FALLBACK_INBOX = 5; 147 static private final int PING_FALLBACK_PIM = 25; 148 149 // MSFT's custom HTTP result code indicating the need to provision 150 static private final int HTTP_NEED_PROVISIONING = 449; 151 152 // Reasonable default 153 public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 154 public Double mProtocolVersionDouble; 155 protected String mDeviceId = null; 156 private String mDeviceType = "Android"; 157 private String mAuthString = null; 158 private String mCmdString = null; 159 public String mHostAddress; 160 public String mUserName; 161 public String mPassword; 162 private boolean mSsl = true; 163 private boolean mTrustSsl = false; 164 public ContentResolver mContentResolver; 165 private String[] mBindArguments = new String[2]; 166 private ArrayList<String> mPingChangeList; 167 private HttpPost mPendingPost = null; 168 // The ping time (in seconds) 169 private int mPingHeartbeat = PING_STARTING_HEARTBEAT; 170 // The longest successful ping heartbeat 171 private int mPingHighWaterMark = 0; 172 // Whether we've ever lowered the heartbeat 173 private boolean mPingHeartbeatDropped = false; 174 // Whether a POST was aborted due to watchdog timeout 175 private boolean mPostAborted = false; 176 // Whether or not the sync service is valid (usable) 177 public boolean mIsValid = true; 178 179 public EasSyncService(Context _context, Mailbox _mailbox) { 180 super(_context, _mailbox); 181 mContentResolver = _context.getContentResolver(); 182 if (mAccount == null) { 183 mIsValid = false; 184 return; 185 } 186 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 187 if (ha == null) { 188 mIsValid = false; 189 return; 190 } 191 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 192 mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; 193 } 194 195 private EasSyncService(String prefix) { 196 super(prefix); 197 } 198 199 public EasSyncService() { 200 this("EAS Validation"); 201 } 202 203 @Override 204 public void ping() { 205 userLog("Alarm ping received!"); 206 synchronized(getSynchronizer()) { 207 if (mPendingPost != null) { 208 userLog("Aborting pending POST!"); 209 mPostAborted = true; 210 mPendingPost.abort(); 211 } 212 } 213 } 214 215 @Override 216 public void stop() { 217 mStop = true; 218 synchronized(getSynchronizer()) { 219 if (mPendingPost != null) { 220 mPendingPost.abort(); 221 } 222 } 223 } 224 225 /** 226 * Determine whether an HTTP code represents an authentication error 227 * @param code the HTTP code returned by the server 228 * @return whether or not the code represents an authentication error 229 */ 230 protected boolean isAuthError(int code) { 231 return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN); 232 } 233 234 /** 235 * Determine whether an HTTP code represents a provisioning error 236 * @param code the HTTP code returned by the server 237 * @return whether or not the code represents an provisioning error 238 */ 239 protected boolean isProvisionError(int code) { 240 return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN); 241 } 242 243 private void setupProtocolVersion(EasSyncService service, Header versionHeader) { 244 String versions = versionHeader.getValue(); 245 if (versions != null) { 246 if (versions.contains("12.0")) { 247 service.mProtocolVersion = "12.0"; 248 } 249 service.mProtocolVersionDouble = Double.parseDouble(service.mProtocolVersion); 250 if (service.mAccount != null) { 251 service.mAccount.mProtocolVersion = service.mProtocolVersion; 252 } 253 } 254 } 255 256 @Override 257 public void validateAccount(String hostAddress, String userName, String password, int port, 258 boolean ssl, boolean trustCertificates, Context context) throws MessagingException { 259 try { 260 userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0"); 261 EasSyncService svc = new EasSyncService("%TestAccount%"); 262 svc.mContext = context; 263 svc.mHostAddress = hostAddress; 264 svc.mUserName = userName; 265 svc.mPassword = password; 266 svc.mSsl = ssl; 267 svc.mTrustSsl = trustCertificates; 268 // We mustn't use the "real" device id or we'll screw up current accounts 269 // Any string will do, but we'll go for "validate" 270 svc.mDeviceId = "validate"; 271 HttpResponse resp = svc.sendHttpClientOptions(); 272 int code = resp.getStatusLine().getStatusCode(); 273 userLog("Validation (OPTIONS) response: " + code); 274 if (code == HttpStatus.SC_OK) { 275 // No exception means successful validation 276 Header commands = resp.getFirstHeader("MS-ASProtocolCommands"); 277 Header versions = resp.getFirstHeader("ms-asprotocolversions"); 278 if (commands == null || versions == null) { 279 userLog("OPTIONS response without commands or versions; reporting I/O error"); 280 throw new MessagingException(MessagingException.IOERROR); 281 } 282 283 // Make sure we've got the right protocol version set up 284 setupProtocolVersion(svc, versions); 285 286 // Run second test here for provisioning failures... 287 Serializer s = new Serializer(); 288 userLog("Try folder sync"); 289 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0") 290 .end().end().done(); 291 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray()); 292 code = resp.getStatusLine().getStatusCode(); 293 // We'll get one of the following responses if policies are required by the server 294 if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) { 295 // Get the policies and see if we are able to support them 296 if (svc.canProvision() != null) { 297 // If so, send the advisory Exception (the account may be created later) 298 throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 299 } else 300 // If not, send the unsupported Exception (the account won't be created) 301 throw new MessagingException( 302 MessagingException.SECURITY_POLICIES_UNSUPPORTED); 303 } 304 userLog("Validation successful"); 305 return; 306 } 307 if (isAuthError(code)) { 308 userLog("Authentication failed"); 309 throw new AuthenticationFailedException("Validation failed"); 310 } else { 311 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 312 userLog("Validation failed, reporting I/O error: ", code); 313 throw new MessagingException(MessagingException.IOERROR); 314 } 315 } catch (IOException e) { 316 Throwable cause = e.getCause(); 317 if (cause != null && cause instanceof CertificateException) { 318 userLog("CertificateException caught: ", e.getMessage()); 319 throw new MessagingException(MessagingException.GENERAL_SECURITY); 320 } 321 userLog("IOException caught: ", e.getMessage()); 322 throw new MessagingException(MessagingException.IOERROR); 323 } 324 325 } 326 327 /** 328 * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that 329 * it can be reused 330 * 331 * @param resp the HttpResponse that indicates a redirect (451) 332 * @param post the HttpPost that was originally sent to the server 333 * @return the HttpPost, updated with the redirect location 334 */ 335 private HttpPost getRedirect(HttpResponse resp, HttpPost post) { 336 Header locHeader = resp.getFirstHeader("X-MS-Location"); 337 if (locHeader != null) { 338 String loc = locHeader.getValue(); 339 // If we've gotten one and it shows signs of looking like an address, we try 340 // sending our request there 341 if (loc != null && loc.startsWith("http")) { 342 post.setURI(URI.create(loc)); 343 return post; 344 } 345 } 346 return null; 347 } 348 349 /** 350 * Send the POST command to the autodiscover server, handling a redirect, if necessary, and 351 * return the HttpResponse 352 * 353 * @param client the HttpClient to be used for the request 354 * @param post the HttpPost we're going to send 355 * @return an HttpResponse from the original or redirect server 356 * @throws IOException on any IOException within the HttpClient code 357 * @throws MessagingException 358 */ 359 private HttpResponse postAutodiscover(HttpClient client, HttpPost post) 360 throws IOException, MessagingException { 361 userLog("Posting autodiscover to: " + post.getURI()); 362 HttpResponse resp = client.execute(post); 363 int code = resp.getStatusLine().getStatusCode(); 364 // On a redirect, try the new location 365 if (code == AUTO_DISCOVER_REDIRECT_CODE) { 366 post = getRedirect(resp, post); 367 if (post != null) { 368 userLog("Posting autodiscover to redirect: " + post.getURI()); 369 return client.execute(post); 370 } 371 } else if (code == HttpStatus.SC_UNAUTHORIZED) { 372 // 401 (Unauthorized) is for true auth errors when used in Autodiscover 373 // 403 (and others) we'll just punt on 374 throw new MessagingException(MessagingException.AUTHENTICATION_FAILED); 375 } else if (code != HttpStatus.SC_OK) { 376 // We'll try the next address if this doesn't work 377 userLog("Code: " + code + ", throwing IOException"); 378 throw new IOException(); 379 } 380 return resp; 381 } 382 383 /** 384 * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using 385 * only an email address and the password 386 * 387 * @param userName the user's email address 388 * @param password the user's password 389 * @return a HostAuth ready to be saved in an Account or null (failure) 390 */ 391 public Bundle tryAutodiscover(String userName, String password) throws RemoteException { 392 XmlSerializer s = Xml.newSerializer(); 393 ByteArrayOutputStream os = new ByteArrayOutputStream(1024); 394 HostAuth hostAuth = new HostAuth(); 395 Bundle bundle = new Bundle(); 396 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 397 MessagingException.NO_ERROR); 398 try { 399 // Build the XML document that's sent to the autodiscover server(s) 400 s.setOutput(os, "UTF-8"); 401 s.startDocument("UTF-8", false); 402 s.startTag(null, "Autodiscover"); 403 s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006"); 404 s.startTag(null, "Request"); 405 s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress"); 406 s.startTag(null, "AcceptableResponseSchema"); 407 s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006"); 408 s.endTag(null, "AcceptableResponseSchema"); 409 s.endTag(null, "Request"); 410 s.endTag(null, "Autodiscover"); 411 s.endDocument(); 412 String req = os.toString(); 413 414 // Initialize the user name and password 415 mUserName = userName; 416 mPassword = password; 417 // Make sure the authentication string is created (mAuthString) 418 makeUriString("foo", null); 419 420 // Split out the domain name 421 int amp = userName.indexOf('@'); 422 // The UI ensures that userName is a valid email address 423 if (amp < 0) { 424 throw new RemoteException(); 425 } 426 String domain = userName.substring(amp + 1); 427 428 // There are up to four attempts here; the two URLs that we're supposed to try per the 429 // specification, and up to one redirect for each (handled in postAutodiscover) 430 431 // Try the domain first and see if we can get a response 432 HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE); 433 setHeaders(post, false); 434 post.setHeader("Content-Type", "text/xml"); 435 post.setEntity(new StringEntity(req)); 436 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 437 HttpResponse resp; 438 try { 439 resp = postAutodiscover(client, post); 440 } catch (ClientProtocolException e1) { 441 return null; 442 } catch (IOException e1) { 443 // We catch the IOException here because we have an alternate address to try 444 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE)); 445 // If we fail here, we're out of options, so we let the outer try catch the 446 // IOException and return null 447 resp = postAutodiscover(client, post); 448 } 449 450 // Get the "final" code; if it's not 200, just return null 451 int code = resp.getStatusLine().getStatusCode(); 452 userLog("Code: " + code); 453 if (code != HttpStatus.SC_OK) return null; 454 455 // At this point, we have a 200 response (SC_OK) 456 HttpEntity e = resp.getEntity(); 457 InputStream is = e.getContent(); 458 try { 459 // The response to Autodiscover is regular XML (not WBXML) 460 // If we ever get an error in this process, we'll just punt and return null 461 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 462 XmlPullParser parser = factory.newPullParser(); 463 parser.setInput(is, "UTF-8"); 464 int type = parser.getEventType(); 465 if (type == XmlPullParser.START_DOCUMENT) { 466 type = parser.next(); 467 if (type == XmlPullParser.START_TAG) { 468 String name = parser.getName(); 469 if (name.equals("Autodiscover")) { 470 hostAuth = new HostAuth(); 471 parseAutodiscover(parser, hostAuth); 472 // On success, we'll have a server address and login 473 if (hostAuth.mAddress != null && hostAuth.mLogin != null) { 474 // Fill in the rest of the HostAuth 475 hostAuth.mPassword = password; 476 hostAuth.mPort = 443; 477 hostAuth.mProtocol = "eas"; 478 hostAuth.mFlags = 479 HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 480 bundle.putParcelable( 481 EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth); 482 } else { 483 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 484 MessagingException.UNSPECIFIED_EXCEPTION); 485 } 486 } 487 } 488 } 489 } catch (XmlPullParserException e1) { 490 // This would indicate an I/O error of some sort 491 // We will simply return null and user can configure manually 492 } 493 // There's no reason at all for exceptions to be thrown, and it's ok if so. 494 // We just won't do auto-discover; user can configure manually 495 } catch (IllegalArgumentException e) { 496 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 497 MessagingException.UNSPECIFIED_EXCEPTION); 498 } catch (IllegalStateException e) { 499 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 500 MessagingException.UNSPECIFIED_EXCEPTION); 501 } catch (IOException e) { 502 userLog("IOException in Autodiscover", e); 503 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 504 MessagingException.IOERROR); 505 } catch (MessagingException e) { 506 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 507 MessagingException.AUTHENTICATION_FAILED); 508 } 509 return bundle; 510 } 511 512 void parseServer(XmlPullParser parser, HostAuth hostAuth) 513 throws XmlPullParserException, IOException { 514 boolean mobileSync = false; 515 while (true) { 516 int type = parser.next(); 517 if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) { 518 break; 519 } else if (type == XmlPullParser.START_TAG) { 520 String name = parser.getName(); 521 if (name.equals("Type")) { 522 if (parser.nextText().equals("MobileSync")) { 523 mobileSync = true; 524 } 525 } else if (mobileSync && name.equals("Url")) { 526 String url = parser.nextText().toLowerCase(); 527 // This will look like https://<server address>/Microsoft-Server-ActiveSync 528 // We need to extract the <server address> 529 if (url.startsWith("https://") && 530 url.endsWith("/microsoft-server-activesync")) { 531 int lastSlash = url.lastIndexOf('/'); 532 hostAuth.mAddress = url.substring(8, lastSlash); 533 userLog("Autodiscover, server: " + hostAuth.mAddress); 534 } 535 } 536 } 537 } 538 } 539 540 void parseSettings(XmlPullParser parser, HostAuth hostAuth) 541 throws XmlPullParserException, IOException { 542 while (true) { 543 int type = parser.next(); 544 if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) { 545 break; 546 } else if (type == XmlPullParser.START_TAG) { 547 String name = parser.getName(); 548 if (name.equals("Server")) { 549 parseServer(parser, hostAuth); 550 } 551 } 552 } 553 } 554 555 void parseAction(XmlPullParser parser, HostAuth hostAuth) 556 throws XmlPullParserException, IOException { 557 while (true) { 558 int type = parser.next(); 559 if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) { 560 break; 561 } else if (type == XmlPullParser.START_TAG) { 562 String name = parser.getName(); 563 if (name.equals("Error")) { 564 // Should parse the error 565 } else if (name.equals("Redirect")) { 566 Log.d(TAG, "Redirect: " + parser.nextText()); 567 } else if (name.equals("Settings")) { 568 parseSettings(parser, hostAuth); 569 } 570 } 571 } 572 } 573 574 void parseUser(XmlPullParser parser, HostAuth hostAuth) 575 throws XmlPullParserException, IOException { 576 while (true) { 577 int type = parser.next(); 578 if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) { 579 break; 580 } else if (type == XmlPullParser.START_TAG) { 581 String name = parser.getName(); 582 if (name.equals("EMailAddress")) { 583 String addr = parser.nextText(); 584 hostAuth.mLogin = addr; 585 userLog("Autodiscover, login: " + addr); 586 } else if (name.equals("DisplayName")) { 587 String dn = parser.nextText(); 588 userLog("Autodiscover, user: " + dn); 589 } 590 } 591 } 592 } 593 594 void parseResponse(XmlPullParser parser, HostAuth hostAuth) 595 throws XmlPullParserException, IOException { 596 while (true) { 597 int type = parser.next(); 598 if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) { 599 break; 600 } else if (type == XmlPullParser.START_TAG) { 601 String name = parser.getName(); 602 if (name.equals("User")) { 603 parseUser(parser, hostAuth); 604 } else if (name.equals("Action")) { 605 parseAction(parser, hostAuth); 606 } 607 } 608 } 609 } 610 611 void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth) 612 throws XmlPullParserException, IOException { 613 while (true) { 614 int type = parser.nextTag(); 615 if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) { 616 break; 617 } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) { 618 parseResponse(parser, hostAuth); 619 } 620 } 621 } 622 623 private void doStatusCallback(long messageId, long attachmentId, int status) { 624 try { 625 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 626 } catch (RemoteException e) { 627 // No danger if the client is no longer around 628 } 629 } 630 631 private void doProgressCallback(long messageId, long attachmentId, int progress) { 632 try { 633 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 634 EmailServiceStatus.IN_PROGRESS, progress); 635 } catch (RemoteException e) { 636 // No danger if the client is no longer around 637 } 638 } 639 640 public File createUniqueFileInternal(String dir, String filename) { 641 File directory; 642 if (dir == null) { 643 directory = mContext.getFilesDir(); 644 } else { 645 directory = new File(dir); 646 } 647 if (!directory.exists()) { 648 directory.mkdirs(); 649 } 650 File file = new File(directory, filename); 651 if (!file.exists()) { 652 return file; 653 } 654 // Get the extension of the file, if any. 655 int index = filename.lastIndexOf('.'); 656 String name = filename; 657 String extension = ""; 658 if (index != -1) { 659 name = filename.substring(0, index); 660 extension = filename.substring(index); 661 } 662 for (int i = 2; i < Integer.MAX_VALUE; i++) { 663 file = new File(directory, name + '-' + i + extension); 664 if (!file.exists()) { 665 return file; 666 } 667 } 668 return null; 669 } 670 671 /** 672 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 673 * wrapper for Attachment 674 * @param req the part (attachment) to be retrieved 675 * @throws IOException 676 */ 677 protected void getAttachment(PartRequest req) throws IOException { 678 Attachment att = req.mAttachment; 679 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 680 doProgressCallback(msg.mId, att.mId, 0); 681 682 String cmd = "GetAttachment&AttachmentName=" + att.mLocation; 683 HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT); 684 685 int status = res.getStatusLine().getStatusCode(); 686 if (status == HttpStatus.SC_OK) { 687 HttpEntity e = res.getEntity(); 688 int len = (int)e.getContentLength(); 689 InputStream is = res.getEntity().getContent(); 690 File f = (req.mDestination != null) 691 ? new File(req.mDestination) 692 : createUniqueFileInternal(req.mDestination, att.mFileName); 693 if (f != null) { 694 // Ensure that the target directory exists 695 File destDir = f.getParentFile(); 696 if (!destDir.exists()) { 697 destDir.mkdirs(); 698 } 699 FileOutputStream os = new FileOutputStream(f); 700 // len > 0 means that Content-Length was set in the headers 701 // len < 0 means "chunked" transfer-encoding 702 if (len != 0) { 703 try { 704 mPendingRequest = req; 705 byte[] bytes = new byte[CHUNK_SIZE]; 706 int length = len; 707 // Loop terminates 1) when EOF is reached or 2) if an IOException occurs 708 // One of these is guaranteed to occur 709 int totalRead = 0; 710 userLog("Attachment content-length: ", len); 711 while (true) { 712 int read = is.read(bytes, 0, CHUNK_SIZE); 713 714 // read < 0 means that EOF was reached 715 if (read < 0) { 716 userLog("Attachment load reached EOF, totalRead: ", totalRead); 717 break; 718 } 719 720 // Keep track of how much we've read for progress callback 721 totalRead += read; 722 723 // Write these bytes out 724 os.write(bytes, 0, read); 725 726 // We can't report percentages if this is chunked; by definition, the 727 // length of incoming data is unknown 728 if (length > 0) { 729 // Belt and suspenders check to prevent runaway reading 730 if (totalRead > length) { 731 errorLog("totalRead is greater than attachment length?"); 732 break; 733 } 734 int pct = (totalRead * 100) / length; 735 doProgressCallback(msg.mId, att.mId, pct); 736 } 737 } 738 } finally { 739 mPendingRequest = null; 740 } 741 } 742 os.flush(); 743 os.close(); 744 745 // EmailProvider will throw an exception if we try to update an unsaved attachment 746 if (att.isSaved()) { 747 String contentUriString = (req.mContentUriString != null) 748 ? req.mContentUriString 749 : "file://" + f.getAbsolutePath(); 750 ContentValues cv = new ContentValues(); 751 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 752 att.update(mContext, cv); 753 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 754 } 755 } 756 } else { 757 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 758 } 759 } 760 761 /** 762 * Responds to a meeting request. The MeetingResponseRequest is basically our 763 * wrapper for the meetingResponse service call 764 * @param req the request (message id and response code) 765 * @throws IOException 766 */ 767 protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException { 768 Message msg = Message.restoreMessageWithId(mContext, req.mMessageId); 769 Serializer s = new Serializer(); 770 s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST); 771 s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse)); 772 s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey)); 773 s.data(Tags.MREQ_REQ_ID, msg.mServerId); 774 s.end().end().done(); 775 HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray()); 776 int status = res.getStatusLine().getStatusCode(); 777 if (status == HttpStatus.SC_OK) { 778 HttpEntity e = res.getEntity(); 779 int len = (int)e.getContentLength(); 780 InputStream is = res.getEntity().getContent(); 781 if (len != 0) { 782 new MeetingResponseParser(is, this).parse(); 783 } 784 } else if (isAuthError(status)) { 785 throw new EasAuthenticationException(); 786 } else { 787 userLog("Meeting response request failed, code: " + status); 788 throw new IOException(); 789 } 790 } 791 792 @SuppressWarnings("deprecation") 793 private String makeUriString(String cmd, String extra) throws IOException { 794 // Cache the authentication string and the command string 795 String safeUserName = URLEncoder.encode(mUserName); 796 if (mAuthString == null) { 797 String cs = mUserName + ':' + mPassword; 798 mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); 799 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType=" 800 + mDeviceType; 801 } 802 String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress + 803 "/Microsoft-Server-ActiveSync"; 804 if (cmd != null) { 805 us += "?Cmd=" + cmd + mCmdString; 806 } 807 if (extra != null) { 808 us += extra; 809 } 810 return us; 811 } 812 813 /** 814 * Set standard HTTP headers, using a policy key if required 815 * @param method the method we are going to send 816 * @param usePolicyKey whether or not a policy key should be sent in the headers 817 */ 818 private void setHeaders(HttpRequestBase method, boolean usePolicyKey) { 819 method.setHeader("Authorization", mAuthString); 820 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 821 method.setHeader("Connection", "keep-alive"); 822 method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); 823 if (usePolicyKey && (mAccount != null)) { 824 String key = mAccount.mSecuritySyncKey; 825 if (key == null || key.length() == 0) { 826 return; 827 } 828 method.setHeader("X-MS-PolicyKey", key); 829 } 830 } 831 832 private ClientConnectionManager getClientConnectionManager() { 833 return SyncManager.getClientConnectionManager(); 834 } 835 836 private HttpClient getHttpClient(int timeout) { 837 HttpParams params = new BasicHttpParams(); 838 HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS); 839 HttpConnectionParams.setSoTimeout(params, timeout); 840 HttpConnectionParams.setSocketBufferSize(params, 8192); 841 HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params); 842 return client; 843 } 844 845 protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 846 return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT); 847 } 848 849 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException { 850 return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT); 851 } 852 853 protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException { 854 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 855 if (Eas.USER_LOG) { 856 userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's'); 857 } 858 return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS); 859 } 860 861 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout) 862 throws IOException { 863 HttpClient client = getHttpClient(timeout); 864 boolean sleepAllowed = cmd.equals(PING_COMMAND); 865 866 // Split the mail sending commands 867 String extra = null; 868 boolean msg = false; 869 if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) { 870 int cmdLength = cmd.indexOf('&'); 871 extra = cmd.substring(cmdLength); 872 cmd = cmd.substring(0, cmdLength); 873 msg = true; 874 } else if (cmd.startsWith("SendMail&")) { 875 msg = true; 876 } 877 878 String us = makeUriString(cmd, extra); 879 HttpPost method = new HttpPost(URI.create(us)); 880 // Send the proper Content-Type header 881 // If entity is null (e.g. for attachments), don't set this header 882 if (msg) { 883 method.setHeader("Content-Type", "message/rfc822"); 884 } else if (entity != null) { 885 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 886 } 887 setHeaders(method, !cmd.equals(PING_COMMAND)); 888 method.setEntity(entity); 889 synchronized(getSynchronizer()) { 890 mPendingPost = method; 891 if (sleepAllowed) { 892 SyncManager.runAsleep(mMailboxId, timeout+(10*SECONDS)); 893 } 894 } 895 try { 896 return client.execute(method); 897 } finally { 898 synchronized(getSynchronizer()) { 899 if (sleepAllowed) { 900 SyncManager.runAwake(mMailboxId); 901 } 902 mPendingPost = null; 903 } 904 } 905 } 906 907 protected HttpResponse sendHttpClientOptions() throws IOException { 908 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 909 String us = makeUriString("OPTIONS", null); 910 HttpOptions method = new HttpOptions(URI.create(us)); 911 setHeaders(method, false); 912 return client.execute(method); 913 } 914 915 String getTargetCollectionClassFromCursor(Cursor c) { 916 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 917 if (type == Mailbox.TYPE_CONTACTS) { 918 return "Contacts"; 919 } else if (type == Mailbox.TYPE_CALENDAR) { 920 return "Calendar"; 921 } else { 922 return "Email"; 923 } 924 } 925 926 /** 927 * Negotiate provisioning with the server. First, get policies form the server and see if 928 * the policies are supported by the device. Then, write the policies to the account and 929 * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are 930 * active; if so, acknowledge the policies to the server and get a final policy key that we 931 * use in future EAS commands and write this key to the account. 932 * @return whether or not provisioning has been successful 933 * @throws IOException 934 */ 935 private boolean tryProvision() throws IOException { 936 // First, see if provisioning is even possible, i.e. do we support the policies required 937 // by the server 938 ProvisionParser pp = canProvision(); 939 if (pp != null) { 940 SecurityPolicy sp = SecurityPolicy.getInstance(mContext); 941 // Get the policies from ProvisionParser 942 PolicySet ps = pp.getPolicySet(); 943 // Update the account with a null policyKey (the key we've gotten is 944 // temporary and cannot be used for syncing) 945 if (ps.writeAccount(mAccount, null, true, mContext)) { 946 sp.updatePolicies(mAccount.mId); 947 } 948 if (pp.getRemoteWipe()) { 949 // We've gotten a remote wipe command 950 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that 951 // we wipe the device regardless of any errors in acknowledgment 952 try { 953 acknowledgeRemoteWipe(pp.getPolicyKey()); 954 } catch (Exception e) { 955 // Because remote wipe is such a high priority task, we don't want to 956 // circumvent it if there's an exception in acknowledgment 957 } 958 // Then, tell SecurityPolicy to wipe the device 959 sp.remoteWipe(); 960 return false; 961 } else if (sp.isActive(ps)) { 962 // See if the required policies are in force; if they are, acknowledge the policies 963 // to the server and get the final policy key 964 String policyKey = acknowledgeProvision(pp.getPolicyKey()); 965 if (policyKey != null) { 966 // Write the final policy key to the Account and say we've been successful 967 ps.writeAccount(mAccount, policyKey, true, mContext); 968 return true; 969 } 970 } else { 971 // Notify that we are blocked because of policies 972 sp.policiesRequired(mAccount.mId); 973 } 974 } 975 return false; 976 } 977 978 private String getPolicyType() { 979 return (mProtocolVersionDouble >= 12.0) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; 980 } 981 982 // TODO This is Exchange 2007 only at this point 983 /** 984 * Obtain a set of policies from the server and determine whether those policies are supported 985 * by the device. 986 * @return the ProvisionParser (holds policies and key) if we receive policies and they are 987 * supported by the device; null otherwise 988 * @throws IOException 989 */ 990 private ProvisionParser canProvision() throws IOException { 991 Serializer s = new Serializer(); 992 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 993 s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()) 994 .end().end().end().done(); 995 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 996 int code = resp.getStatusLine().getStatusCode(); 997 if (code == HttpStatus.SC_OK) { 998 InputStream is = resp.getEntity().getContent(); 999 ProvisionParser pp = new ProvisionParser(is, this); 1000 if (pp.parse()) { 1001 // If true, we received policies from the server; see if they are supported by 1002 // the framework; if so, return the ProvisionParser containing the policy set and 1003 // temporary key 1004 PolicySet ps = pp.getPolicySet(); 1005 if (SecurityPolicy.getInstance(mContext).isSupported(ps)) { 1006 return pp; 1007 } 1008 } 1009 } 1010 // On failures, simply return null 1011 return null; 1012 } 1013 1014 // TODO This is Exchange 2007 only at this point 1015 /** 1016 * Acknowledge that we support the policies provided by the server, and that these policies 1017 * are in force. 1018 * @param tempKey the initial (temporary) policy key sent by the server 1019 * @return the final policy key, which can be used for syncing 1020 * @throws IOException 1021 */ 1022 private void acknowledgeRemoteWipe(String tempKey) throws IOException { 1023 acknowledgeProvisionImpl(tempKey, true); 1024 } 1025 1026 private String acknowledgeProvision(String tempKey) throws IOException { 1027 return acknowledgeProvisionImpl(tempKey, false); 1028 } 1029 1030 private String acknowledgeProvisionImpl(String tempKey, boolean remoteWipe) throws IOException { 1031 Serializer s = new Serializer(); 1032 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1033 s.start(Tags.PROVISION_POLICY); 1034 1035 // Use the proper policy type, depending on EAS version 1036 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType()); 1037 1038 s.data(Tags.PROVISION_POLICY_KEY, tempKey); 1039 s.data(Tags.PROVISION_STATUS, "1"); 1040 if (remoteWipe) { 1041 s.start(Tags.PROVISION_REMOTE_WIPE); 1042 s.data(Tags.PROVISION_STATUS, "1"); 1043 s.end(); 1044 } 1045 s.end(); // PROVISION_POLICY 1046 s.end().end().done(); // PROVISION_POLICIES, PROVISION_PROVISION 1047 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1048 int code = resp.getStatusLine().getStatusCode(); 1049 if (code == HttpStatus.SC_OK) { 1050 InputStream is = resp.getEntity().getContent(); 1051 ProvisionParser pp = new ProvisionParser(is, this); 1052 if (pp.parse()) { 1053 // Return the final polic key from the ProvisionParser 1054 return pp.getPolicyKey(); 1055 } 1056 } 1057 // On failures, return null 1058 return null; 1059 } 1060 1061 /** 1062 * Performs FolderSync 1063 * 1064 * @throws IOException 1065 * @throws EasParserException 1066 */ 1067 public void runAccountMailbox() throws IOException, EasParserException { 1068 // Initialize exit status to success 1069 mExitStatus = EmailServiceStatus.SUCCESS; 1070 try { 1071 try { 1072 SyncManager.callback() 1073 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 1074 } catch (RemoteException e1) { 1075 // Don't care if this fails 1076 } 1077 1078 if (mAccount.mSyncKey == null) { 1079 mAccount.mSyncKey = "0"; 1080 userLog("Account syncKey INIT to 0"); 1081 ContentValues cv = new ContentValues(); 1082 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 1083 mAccount.update(mContext, cv); 1084 } 1085 1086 boolean firstSync = mAccount.mSyncKey.equals("0"); 1087 if (firstSync) { 1088 userLog("Initial FolderSync"); 1089 } 1090 1091 // When we first start up, change all mailboxes to push. 1092 ContentValues cv = new ContentValues(); 1093 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1094 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1095 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 1096 new String[] {Long.toString(mAccount.mId)}) > 0) { 1097 SyncManager.kick("change ping boxes to push"); 1098 } 1099 1100 // Determine our protocol version, if we haven't already and save it in the Account 1101 if (mAccount.mProtocolVersion == null) { 1102 userLog("Determine EAS protocol version"); 1103 HttpResponse resp = sendHttpClientOptions(); 1104 int code = resp.getStatusLine().getStatusCode(); 1105 userLog("OPTIONS response: ", code); 1106 if (code == HttpStatus.SC_OK) { 1107 Header header = resp.getFirstHeader("MS-ASProtocolCommands"); 1108 userLog(header.getValue()); 1109 header = resp.getFirstHeader("ms-asprotocolversions"); 1110 String versions = header.getValue(); 1111 if (versions != null) { 1112 if (versions.contains("12.0")) { 1113 mProtocolVersion = "12.0"; 1114 } 1115 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 1116 mAccount.mProtocolVersion = mProtocolVersion; 1117 // Save the protocol version 1118 cv.clear(); 1119 cv.put(Account.PROTOCOL_VERSION, mProtocolVersion); 1120 mAccount.update(mContext, cv); 1121 userLog(versions); 1122 userLog("Using version ", mProtocolVersion); 1123 } else { 1124 errorLog("No protocol versions in OPTIONS response"); 1125 throw new IOException(); 1126 } 1127 } else { 1128 errorLog("OPTIONS command failed; throwing IOException"); 1129 throw new IOException(); 1130 } 1131 } 1132 1133 // Change all pushable boxes to push when we start the account mailbox 1134 if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 1135 cv.clear(); 1136 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1137 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1138 SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE, 1139 new String[] {Long.toString(mAccount.mId)}) > 0) { 1140 userLog("Push account; set pushable boxes to push..."); 1141 } 1142 } 1143 1144 while (!mStop) { 1145 userLog("Sending Account syncKey: ", mAccount.mSyncKey); 1146 Serializer s = new Serializer(); 1147 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 1148 .text(mAccount.mSyncKey).end().end().done(); 1149 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 1150 if (mStop) break; 1151 int code = resp.getStatusLine().getStatusCode(); 1152 if (code == HttpStatus.SC_OK) { 1153 HttpEntity entity = resp.getEntity(); 1154 int len = (int)entity.getContentLength(); 1155 if (len != 0) { 1156 InputStream is = entity.getContent(); 1157 // Returns true if we need to sync again 1158 if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this)) 1159 .parse()) { 1160 continue; 1161 } 1162 } 1163 } else if (isProvisionError(code)) { 1164 // If the sync error is a provisioning failure (perhaps the policies changed), 1165 // let's try the provisining procedure 1166 if (!tryProvision()) { 1167 // Set the appropriate failure status 1168 mExitStatus = EXIT_SECURITY_FAILURE; 1169 return; 1170 } 1171 } else if (isAuthError(code)) { 1172 mExitStatus = EXIT_LOGIN_FAILURE; 1173 return; 1174 } else { 1175 userLog("FolderSync response error: ", code); 1176 } 1177 1178 // Change all push/hold boxes to push 1179 cv.clear(); 1180 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 1181 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1182 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 1183 new String[] {Long.toString(mAccount.mId)}) > 0) { 1184 userLog("Set push/hold boxes to push..."); 1185 } 1186 1187 try { 1188 SyncManager.callback() 1189 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 1190 } catch (RemoteException e1) { 1191 // Don't care if this fails 1192 } 1193 1194 // Wait for push notifications. 1195 String threadName = Thread.currentThread().getName(); 1196 try { 1197 runPingLoop(); 1198 } catch (StaleFolderListException e) { 1199 // We break out if we get told about a stale folder list 1200 userLog("Ping interrupted; folder list requires sync..."); 1201 } finally { 1202 Thread.currentThread().setName(threadName); 1203 } 1204 } 1205 } catch (IOException e) { 1206 // We catch this here to send the folder sync status callback 1207 // A folder sync failed callback will get sent from run() 1208 try { 1209 if (!mStop) { 1210 SyncManager.callback() 1211 .syncMailboxListStatus(mAccount.mId, 1212 EmailServiceStatus.CONNECTION_ERROR, 0); 1213 } 1214 } catch (RemoteException e1) { 1215 // Don't care if this fails 1216 } 1217 throw e; 1218 } 1219 } 1220 1221 void pushFallback(long mailboxId) { 1222 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1223 ContentValues cv = new ContentValues(); 1224 int mins = PING_FALLBACK_PIM; 1225 if (mailbox.mType == Mailbox.TYPE_INBOX) { 1226 mins = PING_FALLBACK_INBOX; 1227 } 1228 cv.put(Mailbox.SYNC_INTERVAL, mins); 1229 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 1230 cv, null, null); 1231 errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync"); 1232 SyncManager.kick("push fallback"); 1233 } 1234 1235 void runPingLoop() throws IOException, StaleFolderListException { 1236 int pingHeartbeat = mPingHeartbeat; 1237 userLog("runPingLoop"); 1238 // Do push for all sync services here 1239 long endTime = System.currentTimeMillis() + (30*MINUTES); 1240 HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>(); 1241 ArrayList<String> readyMailboxes = new ArrayList<String>(); 1242 ArrayList<String> notReadyMailboxes = new ArrayList<String>(); 1243 int pingWaitCount = 0; 1244 1245 while ((System.currentTimeMillis() < endTime) && !mStop) { 1246 // Count of pushable mailboxes 1247 int pushCount = 0; 1248 // Count of mailboxes that can be pushed right now 1249 int canPushCount = 0; 1250 // Count of uninitialized boxes 1251 int uninitCount = 0; 1252 1253 Serializer s = new Serializer(); 1254 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1255 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 1256 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 1257 notReadyMailboxes.clear(); 1258 readyMailboxes.clear(); 1259 try { 1260 // Loop through our pushed boxes seeing what is available to push 1261 while (c.moveToNext()) { 1262 pushCount++; 1263 // Two requirements for push: 1264 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 1265 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 1266 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 1267 int pingStatus = SyncManager.pingStatus(mailboxId); 1268 String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1269 if (pingStatus == SyncManager.PING_STATUS_OK) { 1270 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 1271 if ((syncKey == null) || syncKey.equals("0")) { 1272 // We can't push until the initial sync is done 1273 pushCount--; 1274 uninitCount++; 1275 continue; 1276 } 1277 1278 if (canPushCount++ == 0) { 1279 // Initialize the Ping command 1280 s.start(Tags.PING_PING) 1281 .data(Tags.PING_HEARTBEAT_INTERVAL, 1282 Integer.toString(pingHeartbeat)) 1283 .start(Tags.PING_FOLDERS); 1284 } 1285 1286 String folderClass = getTargetCollectionClassFromCursor(c); 1287 s.start(Tags.PING_FOLDER) 1288 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 1289 .data(Tags.PING_CLASS, folderClass) 1290 .end(); 1291 readyMailboxes.add(mailboxName); 1292 } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) || 1293 (pingStatus == SyncManager.PING_STATUS_WAITING)) { 1294 notReadyMailboxes.add(mailboxName); 1295 } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) { 1296 pushCount--; 1297 userLog(mailboxName, " in error state; ignore"); 1298 continue; 1299 } 1300 } 1301 } finally { 1302 c.close(); 1303 } 1304 1305 if (Eas.USER_LOG) { 1306 if (!notReadyMailboxes.isEmpty()) { 1307 userLog("Ping not ready for: " + notReadyMailboxes); 1308 } 1309 if (!readyMailboxes.isEmpty()) { 1310 userLog("Ping ready for: " + readyMailboxes); 1311 } 1312 } 1313 1314 // If we've waited 10 seconds or more, just ping with whatever boxes are ready 1315 // But use a shorter than normal heartbeat 1316 boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5); 1317 1318 if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) { 1319 // If all pingable boxes are ready for push, send Ping to the server 1320 s.end().end().done(); 1321 pingWaitCount = 0; 1322 1323 // If we've been stopped, this is a good time to return 1324 if (mStop) return; 1325 1326 long pingTime = SystemClock.elapsedRealtime(); 1327 try { 1328 // Send the ping, wrapped by appropriate timeout/alarm 1329 if (forcePing) { 1330 userLog("Forcing ping after waiting for all boxes to be ready"); 1331 } 1332 HttpResponse res = 1333 sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat); 1334 1335 int code = res.getStatusLine().getStatusCode(); 1336 userLog("Ping response: ", code); 1337 1338 // Return immediately if we've been asked to stop during the ping 1339 if (mStop) { 1340 userLog("Stopping pingLoop"); 1341 return; 1342 } 1343 1344 if (code == HttpStatus.SC_OK) { 1345 HttpEntity e = res.getEntity(); 1346 int len = (int)e.getContentLength(); 1347 InputStream is = res.getEntity().getContent(); 1348 if (len != 0) { 1349 int pingResult = parsePingResult(is, mContentResolver, pingErrorMap); 1350 // If our ping completed (status = 1), and we weren't forced and we're 1351 // not at the maximum, try increasing timeout by two minutes 1352 if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) { 1353 if (pingHeartbeat > mPingHighWaterMark) { 1354 mPingHighWaterMark = pingHeartbeat; 1355 userLog("Setting high water mark at: ", mPingHighWaterMark); 1356 } 1357 if ((pingHeartbeat < PING_MAX_HEARTBEAT) && 1358 !mPingHeartbeatDropped) { 1359 pingHeartbeat += PING_HEARTBEAT_INCREMENT; 1360 if (pingHeartbeat > PING_MAX_HEARTBEAT) { 1361 pingHeartbeat = PING_MAX_HEARTBEAT; 1362 } 1363 userLog("Increasing ping heartbeat to ", pingHeartbeat, "s"); 1364 } 1365 } 1366 } else { 1367 userLog("Ping returned empty result; throwing IOException"); 1368 throw new IOException(); 1369 } 1370 } else if (isAuthError(code)) { 1371 mExitStatus = EXIT_LOGIN_FAILURE; 1372 userLog("Authorization error during Ping: ", code); 1373 throw new IOException(); 1374 } 1375 } catch (IOException e) { 1376 String message = e.getMessage(); 1377 // If we get the exception that is indicative of a NAT timeout and if we 1378 // haven't yet "fixed" the timeout, back off by two minutes and "fix" it 1379 boolean hasMessage = message != null; 1380 userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]")); 1381 if (mPostAborted || (hasMessage && message.contains("reset by peer"))) { 1382 long pingLength = SystemClock.elapsedRealtime() - pingTime; 1383 if ((pingHeartbeat > PING_MIN_HEARTBEAT) && 1384 (pingHeartbeat > mPingHighWaterMark)) { 1385 pingHeartbeat -= PING_HEARTBEAT_INCREMENT; 1386 mPingHeartbeatDropped = true; 1387 if (pingHeartbeat < PING_MIN_HEARTBEAT) { 1388 pingHeartbeat = PING_MIN_HEARTBEAT; 1389 } 1390 userLog("Decreased ping heartbeat to ", pingHeartbeat, "s"); 1391 } else if (mPostAborted || (pingLength < 2000)) { 1392 userLog("Abort or NAT type return < 2 seconds; throwing IOException"); 1393 throw e; 1394 } else { 1395 userLog("NAT type IOException > 2 seconds?"); 1396 } 1397 } else { 1398 throw e; 1399 } 1400 } 1401 } else if (forcePing) { 1402 // In this case, there aren't any boxes that are pingable, but there are boxes 1403 // waiting (for IOExceptions) 1404 userLog("pingLoop waiting 60s for any pingable boxes"); 1405 sleep(60*SECONDS, true); 1406 } else if (pushCount > 0) { 1407 // If we want to Ping, but can't just yet, wait a little bit 1408 // TODO Change sleep to wait and use notify from SyncManager when a sync ends 1409 sleep(2*SECONDS, false); 1410 pingWaitCount++; 1411 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)"); 1412 } else if (uninitCount > 0) { 1413 // In this case, we're doing an initial sync of at least one mailbox. Since this 1414 // is typically a one-time case, I'm ok with trying again every 10 seconds until 1415 // we're in one of the other possible states. 1416 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)"); 1417 sleep(10*SECONDS, true); 1418 } else { 1419 // We've got nothing to do, so we'll check again in 30 minutes at which time 1420 // we'll update the folder list. Let the device sleep in the meantime... 1421 userLog("pingLoop sleeping for 30m"); 1422 sleep(30*MINUTES, true); 1423 } 1424 } 1425 } 1426 1427 void sleep(long ms, boolean runAsleep) { 1428 if (runAsleep) { 1429 SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS)); 1430 } 1431 try { 1432 Thread.sleep(ms); 1433 } catch (InterruptedException e) { 1434 // Doesn't matter whether we stop early; it's the thought that counts 1435 } finally { 1436 if (runAsleep) { 1437 SyncManager.runAwake(mMailboxId); 1438 } 1439 } 1440 } 1441 1442 private int parsePingResult(InputStream is, ContentResolver cr, 1443 HashMap<String, Integer> errorMap) 1444 throws IOException, StaleFolderListException { 1445 PingParser pp = new PingParser(is, this); 1446 if (pp.parse()) { 1447 // True indicates some mailboxes need syncing... 1448 // syncList has the serverId's of the mailboxes... 1449 mBindArguments[0] = Long.toString(mAccount.mId); 1450 mPingChangeList = pp.getSyncList(); 1451 for (String serverId: mPingChangeList) { 1452 mBindArguments[1] = serverId; 1453 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1454 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 1455 try { 1456 if (c.moveToFirst()) { 1457 1458 /** 1459 * Check the boxes reporting changes to see if there really were any... 1460 * We do this because bugs in various Exchange servers can put us into a 1461 * looping behavior by continually reporting changes in a mailbox, even when 1462 * there aren't any. 1463 * 1464 * This behavior is seemingly random, and therefore we must code defensively 1465 * by backing off of push behavior when it is detected. 1466 * 1467 * One known cause, on certain Exchange 2003 servers, is acknowledged by 1468 * Microsoft, and the server hotfix for this case can be found at 1469 * http://support.microsoft.com/kb/923282 1470 */ 1471 1472 // Check the status of the last sync 1473 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 1474 int type = SyncManager.getStatusType(status); 1475 // This check should always be true... 1476 if (type == SyncManager.SYNC_PING) { 1477 int changeCount = SyncManager.getStatusChangeCount(status); 1478 if (changeCount > 0) { 1479 errorMap.remove(serverId); 1480 } else if (changeCount == 0) { 1481 // This means that a ping reported changes in error; we keep a count 1482 // of consecutive errors of this kind 1483 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1484 Integer failures = errorMap.get(serverId); 1485 if (failures == null) { 1486 userLog("Last ping reported changes in error for: ", name); 1487 errorMap.put(serverId, 1); 1488 } else if (failures > MAX_PING_FAILURES) { 1489 // We'll back off of push for this box 1490 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); 1491 continue; 1492 } else { 1493 userLog("Last ping reported changes in error for: ", name); 1494 errorMap.put(serverId, failures + 1); 1495 } 1496 } 1497 } 1498 1499 // If there were no problems with previous sync, we'll start another one 1500 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 1501 SyncManager.SYNC_PING, null); 1502 } 1503 } finally { 1504 c.close(); 1505 } 1506 } 1507 } 1508 return pp.getSyncStatus(); 1509 } 1510 1511 private String getEmailFilter() { 1512 String filter = Eas.FILTER_1_WEEK; 1513 switch (mAccount.mSyncLookback) { 1514 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 1515 filter = Eas.FILTER_1_DAY; 1516 break; 1517 } 1518 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 1519 filter = Eas.FILTER_3_DAYS; 1520 break; 1521 } 1522 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 1523 filter = Eas.FILTER_1_WEEK; 1524 break; 1525 } 1526 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 1527 filter = Eas.FILTER_2_WEEKS; 1528 break; 1529 } 1530 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 1531 filter = Eas.FILTER_1_MONTH; 1532 break; 1533 } 1534 case com.android.email.Account.SYNC_WINDOW_ALL: { 1535 filter = Eas.FILTER_ALL; 1536 break; 1537 } 1538 } 1539 return filter; 1540 } 1541 1542 /** 1543 * Common code to sync E+PIM data 1544 * 1545 * @param target, an EasMailbox, EasContacts, or EasCalendar object 1546 */ 1547 public void sync(AbstractSyncAdapter target) throws IOException { 1548 Mailbox mailbox = target.mMailbox; 1549 1550 boolean moreAvailable = true; 1551 while (!mStop && moreAvailable) { 1552 // If we have no connectivity, just exit cleanly. SyncManager will start us up again 1553 // when connectivity has returned 1554 if (!hasConnectivity()) { 1555 userLog("No connectivity in sync; finishing sync"); 1556 mExitStatus = EXIT_DONE; 1557 return; 1558 } 1559 1560 // Now, handle various requests 1561 while (true) { 1562 Request req = null; 1563 synchronized (mRequests) { 1564 if (mRequests.isEmpty()) { 1565 break; 1566 } else { 1567 req = mRequests.get(0); 1568 } 1569 } 1570 1571 // Our two request types are PartRequest (loading attachment) and 1572 // MeetingResponseRequest (respond to a meeting request) 1573 if (req instanceof PartRequest) { 1574 getAttachment((PartRequest)req); 1575 } else if (req instanceof MeetingResponseRequest) { 1576 sendMeetingResponse((MeetingResponseRequest)req); 1577 } 1578 1579 // If there's an exception handling the request, we'll throw it 1580 // Otherwise, we remove the request 1581 synchronized(mRequests) { 1582 mRequests.remove(req); 1583 } 1584 } 1585 1586 Serializer s = new Serializer(); 1587 String className = target.getCollectionName(); 1588 String syncKey = target.getSyncKey(); 1589 userLog("sync, sending ", className, " syncKey: ", syncKey); 1590 s.start(Tags.SYNC_SYNC) 1591 .start(Tags.SYNC_COLLECTIONS) 1592 .start(Tags.SYNC_COLLECTION) 1593 .data(Tags.SYNC_CLASS, className) 1594 .data(Tags.SYNC_SYNC_KEY, syncKey) 1595 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId) 1596 .tag(Tags.SYNC_DELETES_AS_MOVES); 1597 1598 // EAS doesn't like GetChanges if the syncKey is "0"; not documented 1599 if (!syncKey.equals("0")) { 1600 s.tag(Tags.SYNC_GET_CHANGES); 1601 } 1602 s.data(Tags.SYNC_WINDOW_SIZE, 1603 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 1604 1605 // Handle options 1606 s.start(Tags.SYNC_OPTIONS); 1607 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts 1608 if (className.equals("Email")) { 1609 s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter()); 1610 } else if (className.equals("Calendar")) { 1611 // TODO Force one month for calendar until we can set this! 1612 s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH); 1613 } 1614 // Set the truncation amount for all classes 1615 if (mProtocolVersionDouble >= 12.0) { 1616 s.start(Tags.BASE_BODY_PREFERENCE) 1617 // HTML for email; plain text for everything else 1618 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 1619 : Eas.BODY_PREFERENCE_TEXT)) 1620 .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE) 1621 .end(); 1622 } else { 1623 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 1624 } 1625 s.end(); 1626 1627 // Send our changes up to the server 1628 target.sendLocalChanges(s); 1629 1630 s.end().end().end().done(); 1631 HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray()); 1632 int code = resp.getStatusLine().getStatusCode(); 1633 if (code == HttpStatus.SC_OK) { 1634 InputStream is = resp.getEntity().getContent(); 1635 if (is != null) { 1636 moreAvailable = target.parse(is); 1637 target.cleanup(); 1638 } else { 1639 userLog("Empty input stream in sync command response"); 1640 } 1641 } else { 1642 userLog("Sync response error: ", code); 1643 if (isProvisionError(code)) { 1644 if (!tryProvision()) { 1645 mExitStatus = EXIT_SECURITY_FAILURE; 1646 return; 1647 } 1648 } else if (isAuthError(code)) { 1649 mExitStatus = EXIT_LOGIN_FAILURE; 1650 } else { 1651 mExitStatus = EXIT_IO_ERROR; 1652 } 1653 return; 1654 } 1655 } 1656 mExitStatus = EXIT_DONE; 1657 } 1658 1659 protected boolean setupService() { 1660 // Make sure account and mailbox are always the latest from the database 1661 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 1662 if (mAccount == null) return false; 1663 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 1664 if (mMailbox == null) return false; 1665 mThread = Thread.currentThread(); 1666 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 1667 TAG = mThread.getName(); 1668 1669 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 1670 if (ha == null) return false; 1671 mHostAddress = ha.mAddress; 1672 mUserName = ha.mLogin; 1673 mPassword = ha.mPassword; 1674 1675 // Set up our protocol version from the Account 1676 mProtocolVersion = mAccount.mProtocolVersion; 1677 // If it hasn't been set up, start with default version 1678 if (mProtocolVersion == null) { 1679 mProtocolVersion = DEFAULT_PROTOCOL_VERSION; 1680 } 1681 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion); 1682 return true; 1683 } 1684 1685 /* (non-Javadoc) 1686 * @see java.lang.Runnable#run() 1687 */ 1688 public void run() { 1689 if (!setupService()) return; 1690 1691 try { 1692 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 1693 } catch (RemoteException e1) { 1694 // Don't care if this fails 1695 } 1696 1697 // Whether or not we're the account mailbox 1698 try { 1699 mDeviceId = SyncManager.getDeviceId(); 1700 if ((mMailbox == null) || (mAccount == null)) { 1701 return; 1702 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 1703 runAccountMailbox(); 1704 } else { 1705 AbstractSyncAdapter target; 1706 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 1707 target = new ContactsSyncAdapter(mMailbox, this); 1708 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) { 1709 target = new CalendarSyncAdapter(mMailbox, this); 1710 } else { 1711 target = new EmailSyncAdapter(mMailbox, this); 1712 } 1713 // We loop here because someone might have put a request in while we were syncing 1714 // and we've missed that opportunity... 1715 do { 1716 if (mRequestTime != 0) { 1717 userLog("Looping for user request..."); 1718 mRequestTime = 0; 1719 } 1720 sync(target); 1721 } while (mRequestTime != 0); 1722 } 1723 } catch (EasAuthenticationException e) { 1724 userLog("Caught authentication error"); 1725 mExitStatus = EXIT_LOGIN_FAILURE; 1726 } catch (IOException e) { 1727 String message = e.getMessage(); 1728 userLog("Caught IOException: ", (message == null) ? "No message" : message); 1729 mExitStatus = EXIT_IO_ERROR; 1730 } catch (Exception e) { 1731 userLog("Uncaught exception in EasSyncService", e); 1732 } finally { 1733 int status; 1734 1735 if (!mStop) { 1736 userLog("Sync finished"); 1737 SyncManager.done(this); 1738 switch (mExitStatus) { 1739 case EXIT_IO_ERROR: 1740 status = EmailServiceStatus.CONNECTION_ERROR; 1741 break; 1742 case EXIT_DONE: 1743 status = EmailServiceStatus.SUCCESS; 1744 ContentValues cv = new ContentValues(); 1745 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1746 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 1747 cv.put(Mailbox.SYNC_STATUS, s); 1748 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 1749 mMailboxId), cv, null, null); 1750 break; 1751 case EXIT_LOGIN_FAILURE: 1752 status = EmailServiceStatus.LOGIN_FAILED; 1753 break; 1754 case EXIT_SECURITY_FAILURE: 1755 status = EmailServiceStatus.SECURITY_FAILURE; 1756 break; 1757 default: 1758 status = EmailServiceStatus.REMOTE_EXCEPTION; 1759 errorLog("Sync ended due to an exception."); 1760 break; 1761 } 1762 } else { 1763 userLog("Stopped sync finished."); 1764 status = EmailServiceStatus.SUCCESS; 1765 } 1766 1767 try { 1768 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 1769 } catch (RemoteException e1) { 1770 // Don't care if this fails 1771 } 1772 1773 // Make sure SyncManager knows about this 1774 SyncManager.kick("sync finished"); 1775 } 1776 } 1777} 1778